mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 14:38:41 +00:00 
			
		
		
		
	Compare commits
	
		
			7 Commits
		
	
	
		
			template_l
			...
			select_fix
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 3291488a8b | ||
|   | 75550b39f4 | ||
|   | 02e1ed2130 | ||
|   | 2948264917 | ||
|   | 660411ac42 | ||
|   | 88e3f02c9c | ||
|   | f3f419077b | 
| @@ -1 +1 @@ | ||||
| 3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c | ||||
| d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248 | ||||
|   | ||||
							
								
								
									
										7
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -53,7 +53,6 @@ jobs: | ||||
|               'new-target-platform', | ||||
|               'merging-to-release', | ||||
|               'merging-to-beta', | ||||
|               'chained-pr', | ||||
|               'core', | ||||
|               'small-pr', | ||||
|               'dashboard', | ||||
| @@ -141,8 +140,6 @@ jobs: | ||||
|                 labels.add('merging-to-release'); | ||||
|               } else if (baseRef === 'beta') { | ||||
|                 labels.add('merging-to-beta'); | ||||
|               } else if (baseRef !== 'dev') { | ||||
|                 labels.add('chained-pr'); | ||||
|               } | ||||
|  | ||||
|               return labels; | ||||
| @@ -531,8 +528,8 @@ jobs: | ||||
|             const apiData = await fetchApiData(); | ||||
|             const baseRef = context.payload.pull_request.base.ref; | ||||
|  | ||||
|             // Early exit for release and beta branches only | ||||
|             if (baseRef === 'release' || baseRef === 'beta') { | ||||
|             // Early exit for non-dev branches | ||||
|             if (baseRef !== 'dev') { | ||||
|               const branchLabels = await detectMergeBranch(); | ||||
|               const finalLabels = Array.from(branchLabels); | ||||
|  | ||||
|   | ||||
							
								
								
									
										31
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										31
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -178,8 +178,6 @@ jobs: | ||||
|       component-test-count: ${{ steps.determine.outputs.component-test-count }} | ||||
|       changed-cpp-file-count: ${{ steps.determine.outputs.changed-cpp-file-count }} | ||||
|       memory_impact: ${{ steps.determine.outputs.memory-impact }} | ||||
|       cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }} | ||||
|       cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }} | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
| @@ -212,8 +210,6 @@ jobs: | ||||
|           echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT | ||||
|           echo "changed-cpp-file-count=$(echo "$output" | jq -r '.changed_cpp_file_count')" >> $GITHUB_OUTPUT | ||||
|           echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT | ||||
|           echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT | ||||
|           echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT | ||||
|  | ||||
|   integration-tests: | ||||
|     name: Run integration tests | ||||
| @@ -251,33 +247,6 @@ jobs: | ||||
|           . venv/bin/activate | ||||
|           pytest -vv --no-cov --tb=native -n auto tests/integration/ | ||||
|  | ||||
|   cpp-unit-tests: | ||||
|     name: Run C++ unit tests | ||||
|     runs-on: ubuntu-24.04 | ||||
|     needs: | ||||
|       - common | ||||
|       - determine-jobs | ||||
|     if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]') | ||||
|     steps: | ||||
|       - name: Check out code from GitHub | ||||
|         uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 | ||||
|  | ||||
|       - name: Restore Python | ||||
|         uses: ./.github/actions/restore-python | ||||
|         with: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|  | ||||
|       - name: Run cpp_unit_test.py | ||||
|         run: | | ||||
|           . venv/bin/activate | ||||
|           if [ "${{ needs.determine-jobs.outputs.cpp-unit-tests-run-all }}" = "true" ]; then | ||||
|             script/cpp_unit_test.py --all | ||||
|           else | ||||
|             ARGS=$(echo '${{ needs.determine-jobs.outputs.cpp-unit-tests-components }}' | jq -r '.[] | @sh' | xargs) | ||||
|             script/cpp_unit_test.py $ARGS | ||||
|           fi | ||||
|  | ||||
|   clang-tidy-single: | ||||
|     name: ${{ matrix.name }} | ||||
|     runs-on: ubuntu-24.04 | ||||
|   | ||||
							
								
								
									
										1
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							| @@ -14,7 +14,6 @@ jobs: | ||||
|         label: | ||||
|           - needs-docs | ||||
|           - merge-after-release | ||||
|           - chained-pr | ||||
|     steps: | ||||
|       - name: Check for ${{ matrix.label }} label | ||||
|         uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 | ||||
|   | ||||
| @@ -201,7 +201,6 @@ esphome/components/havells_solar/* @sourabhjaiswal | ||||
| esphome/components/hbridge/fan/* @WeekendWarrior | ||||
| esphome/components/hbridge/light/* @DotNetDann | ||||
| esphome/components/hbridge/switch/* @dwmw2 | ||||
| esphome/components/hdc2010/* @optimusprimespace @ssieb | ||||
| esphome/components/he60r/* @clydebarrow | ||||
| esphome/components/heatpumpir/* @rob-deutsch | ||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||
|   | ||||
| @@ -16,12 +16,7 @@ from esphome.const import ( | ||||
|     CONF_UPDATE_INTERVAL, | ||||
| ) | ||||
| from esphome.core import ID | ||||
| from esphome.cpp_generator import ( | ||||
|     LambdaExpression, | ||||
|     MockObj, | ||||
|     MockObjClass, | ||||
|     TemplateArgsType, | ||||
| ) | ||||
| from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
| from esphome.types import ConfigType | ||||
| from esphome.util import Registry | ||||
| @@ -105,36 +100,6 @@ LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition) | ||||
| ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component) | ||||
|  | ||||
|  | ||||
| def new_lambda_pvariable( | ||||
|     id_obj: ID, | ||||
|     lambda_expr: LambdaExpression, | ||||
|     stateless_class: MockObjClass, | ||||
|     template_arg: cg.TemplateArguments | None = None, | ||||
| ) -> MockObj: | ||||
|     """Create Pvariable for lambda, using stateless class if applicable. | ||||
|  | ||||
|     Combines ID selection and Pvariable creation in one call. For stateless | ||||
|     lambdas (empty capture), uses function pointer instead of std::function. | ||||
|  | ||||
|     Args: | ||||
|         id_obj: The ID object (action_id, condition_id, or filter_id) | ||||
|         lambda_expr: The lambda expression object | ||||
|         stateless_class: The stateless class to use for stateless lambdas | ||||
|         template_arg: Optional template arguments (for actions/conditions) | ||||
|  | ||||
|     Returns: | ||||
|         The created Pvariable | ||||
|     """ | ||||
|     # For stateless lambdas, use function pointer instead of std::function | ||||
|     if lambda_expr.capture == "": | ||||
|         id_obj = id_obj.copy() | ||||
|         id_obj.type = stateless_class | ||||
|  | ||||
|     if template_arg is not None: | ||||
|         return cg.new_Pvariable(id_obj, template_arg, lambda_expr) | ||||
|     return cg.new_Pvariable(id_obj, lambda_expr) | ||||
|  | ||||
|  | ||||
| def validate_automation(extra_schema=None, extra_validators=None, single=False): | ||||
|     if extra_schema is None: | ||||
|         extra_schema = {} | ||||
|   | ||||
| @@ -9,7 +9,7 @@ static const char *const TAG = "adalight_light_effect"; | ||||
| static const uint32_t ADALIGHT_ACK_INTERVAL = 1000; | ||||
| static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000; | ||||
|  | ||||
| AdalightLightEffect::AdalightLightEffect(const char *name) : AddressableLightEffect(name) {} | ||||
| AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|  | ||||
| void AdalightLightEffect::start() { | ||||
|   AddressableLightEffect::start(); | ||||
|   | ||||
| @@ -11,7 +11,7 @@ namespace adalight { | ||||
|  | ||||
| class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice { | ||||
|  public: | ||||
|   AdalightLightEffect(const char *name); | ||||
|   AdalightLightEffect(const std::string &name); | ||||
|  | ||||
|   void start() override; | ||||
|   void stop() override; | ||||
|   | ||||
| @@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse { | ||||
|   reserved 4; // Deprecated: was string unique_id | ||||
|  | ||||
|   string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"]; | ||||
|   repeated string options = 6 [(container_pointer) = "std::vector"]; | ||||
|   repeated string options = 6 [(container_pointer) = "FixedVector"]; | ||||
|   bool disabled_by_default = 7; | ||||
|   EntityCategory entity_category = 8; | ||||
|   uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"]; | ||||
|   | ||||
| @@ -486,7 +486,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c | ||||
|   if (light->supports_effects()) { | ||||
|     msg.effects.emplace_back("None"); | ||||
|     for (auto *effect : light->get_effects()) { | ||||
|       msg.effects.emplace_back(effect->get_name()); | ||||
|       msg.effects.push_back(effect->get_name()); | ||||
|     } | ||||
|   } | ||||
|   return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size, | ||||
| @@ -1572,13 +1572,7 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption | ||||
|   resp.success = false; | ||||
|  | ||||
|   psk_t psk{}; | ||||
|   if (msg.key.empty()) { | ||||
|     if (this->parent_->clear_noise_psk(true)) { | ||||
|       resp.success = true; | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Failed to clear encryption key"); | ||||
|     } | ||||
|   } else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { | ||||
|   if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) { | ||||
|     ESP_LOGW(TAG, "Invalid encryption key length"); | ||||
|   } else if (!this->parent_->save_noise_psk(psk, true)) { | ||||
|     ESP_LOGW(TAG, "Failed to save encryption key"); | ||||
|   | ||||
| @@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   const char *message_name() const override { return "list_entities_select_response"; } | ||||
| #endif | ||||
|   const std::vector<std::string> *options{}; | ||||
|   const FixedVector<std::string> *options{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(ProtoSize &size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   | ||||
| @@ -468,31 +468,6 @@ uint16_t APIServer::get_port() const { return this->port_; } | ||||
| void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; } | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
| bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, | ||||
|                                   const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) { | ||||
|   if (!this->noise_pref_.save(&new_psk)) { | ||||
|     ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg)); | ||||
|     return false; | ||||
|   } | ||||
|   // ensure it's written immediately | ||||
|   if (!global_preferences->sync()) { | ||||
|     ESP_LOGW(TAG, "Failed to sync preferences"); | ||||
|     return false; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg)); | ||||
|   if (make_active) { | ||||
|     this->set_timeout(100, [this, active_psk]() { | ||||
|       ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); | ||||
|       this->set_noise_psk(active_psk); | ||||
|       for (auto &c : this->clients_) { | ||||
|         DisconnectRequest req; | ||||
|         c->send_message(req, DisconnectRequest::MESSAGE_TYPE); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   return true; | ||||
| } | ||||
|  | ||||
| bool APIServer::save_noise_psk(psk_t psk, bool make_active) { | ||||
| #ifdef USE_API_NOISE_PSK_FROM_YAML | ||||
|   // When PSK is set from YAML, this function should never be called | ||||
| @@ -507,21 +482,27 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) { | ||||
|   } | ||||
|  | ||||
|   SavedNoisePsk new_saved_psk{psk}; | ||||
|   return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk, | ||||
|                                  make_active); | ||||
| #endif | ||||
| } | ||||
| bool APIServer::clear_noise_psk(bool make_active) { | ||||
| #ifdef USE_API_NOISE_PSK_FROM_YAML | ||||
|   // When PSK is set from YAML, this function should never be called | ||||
|   // but if it is, reject the change | ||||
|   ESP_LOGW(TAG, "Key set in YAML"); | ||||
|   return false; | ||||
| #else | ||||
|   SavedNoisePsk empty_psk{}; | ||||
|   psk_t empty{}; | ||||
|   return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty, | ||||
|                                  make_active); | ||||
|   if (!this->noise_pref_.save(&new_saved_psk)) { | ||||
|     ESP_LOGW(TAG, "Failed to save Noise PSK"); | ||||
|     return false; | ||||
|   } | ||||
|   // ensure it's written immediately | ||||
|   if (!global_preferences->sync()) { | ||||
|     ESP_LOGW(TAG, "Failed to sync preferences"); | ||||
|     return false; | ||||
|   } | ||||
|   ESP_LOGD(TAG, "Noise PSK saved"); | ||||
|   if (make_active) { | ||||
|     this->set_timeout(100, [this, psk]() { | ||||
|       ESP_LOGW(TAG, "Disconnecting all clients to reset PSK"); | ||||
|       this->set_noise_psk(psk); | ||||
|       for (auto &c : this->clients_) { | ||||
|         DisconnectRequest req; | ||||
|         c->send_message(req, DisconnectRequest::MESSAGE_TYPE); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|   return true; | ||||
| #endif | ||||
| } | ||||
| #endif | ||||
|   | ||||
| @@ -53,7 +53,6 @@ class APIServer : public Component, public Controller { | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
|   bool save_noise_psk(psk_t psk, bool make_active = true); | ||||
|   bool clear_noise_psk(bool make_active = true); | ||||
|   void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); } | ||||
|   std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; } | ||||
| #endif  // USE_API_NOISE | ||||
| @@ -175,10 +174,6 @@ class APIServer : public Component, public Controller { | ||||
|  | ||||
|  protected: | ||||
|   void schedule_reboot_timeout_(); | ||||
| #ifdef USE_API_NOISE | ||||
|   bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg, | ||||
|                          const psk_t &active_psk, bool make_active); | ||||
| #endif  // USE_API_NOISE | ||||
|   // Pointers and pointer-like types first (4 bytes each) | ||||
|   std::unique_ptr<socket::Socket> socket_ = nullptr; | ||||
| #ifdef USE_API_CLIENT_CONNECTED_TRIGGER | ||||
|   | ||||
| @@ -2,11 +2,11 @@ | ||||
|  | ||||
| #include <cinttypes> | ||||
| #include <utility> | ||||
| #include <vector> | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/components/binary_sensor/binary_sensor.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -92,8 +92,8 @@ class DoubleClickTrigger : public Trigger<> { | ||||
|  | ||||
| class MultiClickTrigger : public Trigger<>, public Component { | ||||
|  public: | ||||
|   explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing) | ||||
|       : parent_(parent), timing_(timing) {} | ||||
|   explicit MultiClickTrigger(BinarySensor *parent, std::vector<MultiClickTriggerEvent> timing) | ||||
|       : parent_(parent), timing_(std::move(timing)) {} | ||||
|  | ||||
|   void setup() override { | ||||
|     this->last_state_ = this->parent_->get_state_default(false); | ||||
| @@ -115,7 +115,7 @@ class MultiClickTrigger : public Trigger<>, public Component { | ||||
|   void trigger_(); | ||||
|  | ||||
|   BinarySensor *parent_; | ||||
|   FixedVector<MultiClickTriggerEvent> timing_; | ||||
|   std::vector<MultiClickTriggerEvent> timing_; | ||||
|   uint32_t invalid_cooldown_{1000}; | ||||
|   optional<size_t> at_index_{}; | ||||
|   bool last_state_{false}; | ||||
|   | ||||
| @@ -524,23 +524,13 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { | ||||
|     call.set_target_humidity(this->target_humidity); | ||||
|   } | ||||
|   if (this->uses_custom_fan_mode) { | ||||
|     if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { | ||||
|       call.fan_mode_.reset(); | ||||
|       call.custom_fan_mode_ = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode); | ||||
|     } | ||||
|   } else if (traits.supports_fan_mode(this->fan_mode)) { | ||||
|   if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) { | ||||
|     call.set_fan_mode(this->fan_mode); | ||||
|   } | ||||
|   if (this->uses_custom_preset) { | ||||
|     if (this->custom_preset < traits.get_supported_custom_presets().size()) { | ||||
|       call.preset_.reset(); | ||||
|       call.custom_preset_ = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset); | ||||
|     } | ||||
|   } else if (traits.supports_preset(this->preset)) { | ||||
|   if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) { | ||||
|     call.set_preset(this->preset); | ||||
|   } | ||||
|   if (traits.supports_swing_mode(this->swing_mode)) { | ||||
|   if (traits.get_supports_swing_modes()) { | ||||
|     call.set_swing_mode(this->swing_mode); | ||||
|   } | ||||
|   return call; | ||||
| @@ -559,25 +549,41 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { | ||||
|   if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) { | ||||
|     climate->target_humidity = this->target_humidity; | ||||
|   } | ||||
|   if (this->uses_custom_fan_mode) { | ||||
|     if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) { | ||||
|       climate->fan_mode.reset(); | ||||
|       climate->custom_fan_mode = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode); | ||||
|     } | ||||
|   } else if (traits.supports_fan_mode(this->fan_mode)) { | ||||
|   if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) { | ||||
|     climate->fan_mode = this->fan_mode; | ||||
|     climate->custom_fan_mode.reset(); | ||||
|   } | ||||
|   if (this->uses_custom_preset) { | ||||
|     if (this->custom_preset < traits.get_supported_custom_presets().size()) { | ||||
|       climate->preset.reset(); | ||||
|       climate->custom_preset = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset); | ||||
|   if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) { | ||||
|     // std::set has consistent order (lexicographic for strings) | ||||
|     const auto &modes = traits.get_supported_custom_fan_modes(); | ||||
|     if (custom_fan_mode < modes.size()) { | ||||
|       size_t i = 0; | ||||
|       for (const auto &mode : modes) { | ||||
|         if (i == this->custom_fan_mode) { | ||||
|           climate->custom_fan_mode = mode; | ||||
|           break; | ||||
|         } | ||||
|         i++; | ||||
|       } | ||||
|     } | ||||
|   } else if (traits.supports_preset(this->preset)) { | ||||
|     climate->preset = this->preset; | ||||
|     climate->custom_preset.reset(); | ||||
|   } | ||||
|   if (traits.supports_swing_mode(this->swing_mode)) { | ||||
|   if (traits.get_supports_presets() && !this->uses_custom_preset) { | ||||
|     climate->preset = this->preset; | ||||
|   } | ||||
|   if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) { | ||||
|     // std::set has consistent order (lexicographic for strings) | ||||
|     const auto &presets = traits.get_supported_custom_presets(); | ||||
|     if (custom_preset < presets.size()) { | ||||
|       size_t i = 0; | ||||
|       for (const auto &preset : presets) { | ||||
|         if (i == this->custom_preset) { | ||||
|           climate->custom_preset = preset; | ||||
|           break; | ||||
|         } | ||||
|         i++; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   if (traits.get_supports_swing_modes()) { | ||||
|     climate->swing_mode = this->swing_mode; | ||||
|   } | ||||
|   climate->publish_state(); | ||||
|   | ||||
| @@ -33,7 +33,6 @@ class Climate; | ||||
| class ClimateCall { | ||||
|  public: | ||||
|   explicit ClimateCall(Climate *parent) : parent_(parent) {} | ||||
|   friend struct ClimateDeviceRestoreState; | ||||
|  | ||||
|   /// Set the mode of the climate device. | ||||
|   ClimateCall &set_mode(ClimateMode mode); | ||||
|   | ||||
| @@ -9,7 +9,8 @@ static const char *const TAG = "copy.select"; | ||||
| void CopySelect::setup() { | ||||
|   source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); | ||||
|  | ||||
|   traits.set_options(source_->traits.get_options()); | ||||
|   // Copy options from source select | ||||
|   this->traits.copy_options(source_->traits.get_options()); | ||||
|  | ||||
|   if (source_->has_state()) | ||||
|     this->publish_state(source_->state); | ||||
|   | ||||
| @@ -80,8 +80,8 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), | ||||
|            light_effect->get_last_universe()); | ||||
|   ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(), | ||||
|            light_effect->get_first_universe(), light_effect->get_last_universe()); | ||||
|  | ||||
|   light_effects_.insert(light_effect); | ||||
|  | ||||
| @@ -95,8 +95,8 @@ void E131Component::remove_effect(E131AddressableLightEffect *light_effect) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(), | ||||
|            light_effect->get_last_universe()); | ||||
|   ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(), | ||||
|            light_effect->get_first_universe(), light_effect->get_last_universe()); | ||||
|  | ||||
|   light_effects_.erase(light_effect); | ||||
|  | ||||
|   | ||||
| @@ -9,7 +9,7 @@ namespace e131 { | ||||
| static const char *const TAG = "e131_addressable_light_effect"; | ||||
| static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1); | ||||
|  | ||||
| E131AddressableLightEffect::E131AddressableLightEffect(const char *name) : AddressableLightEffect(name) {} | ||||
| E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|  | ||||
| int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; } | ||||
|  | ||||
| @@ -58,8 +58,8 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet | ||||
|       std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1)); | ||||
|   auto *input_data = packet.values + 1; | ||||
|  | ||||
|   ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name(), universe, output_offset, | ||||
|            output_end); | ||||
|   ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name().c_str(), universe, | ||||
|            output_offset, output_end); | ||||
|  | ||||
|   switch (channels_) { | ||||
|     case E131_MONO: | ||||
|   | ||||
| @@ -13,7 +13,7 @@ enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 }; | ||||
|  | ||||
| class E131AddressableLightEffect : public light::AddressableLightEffect { | ||||
|  public: | ||||
|   E131AddressableLightEffect(const char *name); | ||||
|   E131AddressableLightEffect(const std::string &name); | ||||
|  | ||||
|   void start() override; | ||||
|   void stop() override; | ||||
|   | ||||
| @@ -304,13 +304,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str: | ||||
| def _format_framework_espidf_version(ver: cv.Version, release: str) -> str: | ||||
|     # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to | ||||
|     # a PIO platformio/framework-espidf value | ||||
|     if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1): | ||||
|         ext = "tar.xz" | ||||
|     else: | ||||
|         ext = "zip" | ||||
|     if release: | ||||
|         return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}" | ||||
|     return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}" | ||||
|         return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip" | ||||
|     return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip" | ||||
|  | ||||
|  | ||||
| def _is_framework_url(source: str) -> str: | ||||
| @@ -359,7 +355,6 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = { | ||||
| ESP_IDF_PLATFORM_VERSION_LOOKUP = { | ||||
|     cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"), | ||||
|     cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"), | ||||
|     cv.Version(5, 4, 3): cv.Version(55, 3, 32), | ||||
|     cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"), | ||||
|     cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"), | ||||
|     cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"), | ||||
| @@ -555,32 +550,6 @@ CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface" | ||||
| CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking" | ||||
| CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety" | ||||
| CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram" | ||||
| CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios" | ||||
| CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select" | ||||
| CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir" | ||||
|  | ||||
| # VFS requirement tracking | ||||
| # Components that need VFS features can call require_vfs_select() or require_vfs_dir() | ||||
| KEY_VFS_SELECT_REQUIRED = "vfs_select_required" | ||||
| KEY_VFS_DIR_REQUIRED = "vfs_dir_required" | ||||
|  | ||||
|  | ||||
| def require_vfs_select() -> None: | ||||
|     """Mark that VFS select support is required by a component. | ||||
|  | ||||
|     Call this from components that use esp_vfs_eventfd or other VFS select features. | ||||
|     This prevents CONFIG_VFS_SUPPORT_SELECT from being disabled. | ||||
|     """ | ||||
|     CORE.data[KEY_VFS_SELECT_REQUIRED] = True | ||||
|  | ||||
|  | ||||
| def require_vfs_dir() -> None: | ||||
|     """Mark that VFS directory support is required by a component. | ||||
|  | ||||
|     Call this from components that use directory functions (opendir, readdir, mkdir, etc.). | ||||
|     This prevents CONFIG_VFS_SUPPORT_DIR from being disabled. | ||||
|     """ | ||||
|     CORE.data[KEY_VFS_DIR_REQUIRED] = True | ||||
|  | ||||
|  | ||||
| def _validate_idf_component(config: ConfigType) -> ConfigType: | ||||
| @@ -646,13 +615,6 @@ FRAMEWORK_SCHEMA = cv.All( | ||||
|                     cv.Optional( | ||||
|                         CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True | ||||
|                     ): cv.boolean, | ||||
|                     cv.Optional( | ||||
|                         CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True | ||||
|                     ): cv.boolean, | ||||
|                     cv.Optional( | ||||
|                         CONF_DISABLE_VFS_SUPPORT_SELECT, default=True | ||||
|                     ): cv.boolean, | ||||
|                     cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean, | ||||
|                     cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean, | ||||
|                 } | ||||
|             ), | ||||
| @@ -882,11 +844,6 @@ async def to_code(config): | ||||
|     for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"): | ||||
|         os.environ.pop(clean_var, None) | ||||
|  | ||||
|     # Set the location of the IDF component manager cache | ||||
|     os.environ["IDF_COMPONENT_CACHE_PATH"] = str( | ||||
|         CORE.relative_internal_path(".espressif") | ||||
|     ) | ||||
|  | ||||
|     add_extra_script( | ||||
|         "post", | ||||
|         "post_build.py", | ||||
| @@ -1005,43 +962,6 @@ async def to_code(config): | ||||
|     if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True): | ||||
|         add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False) | ||||
|  | ||||
|     # Disable VFS support for termios (terminal I/O functions) | ||||
|     # ESPHome doesn't use termios functions on ESP32 (only used in host UART driver). | ||||
|     # Saves approximately 1.8KB of flash when disabled (default). | ||||
|     add_idf_sdkconfig_option( | ||||
|         "CONFIG_VFS_SUPPORT_TERMIOS", | ||||
|         not advanced.get(CONF_DISABLE_VFS_SUPPORT_TERMIOS, True), | ||||
|     ) | ||||
|  | ||||
|     # Disable VFS support for select() with file descriptors | ||||
|     # ESPHome only uses select() with sockets via lwip_select(), which still works. | ||||
|     # VFS select is only needed for UART/eventfd file descriptors. | ||||
|     # Components that need it (e.g., openthread) call require_vfs_select(). | ||||
|     # Saves approximately 2.7KB of flash when disabled (default). | ||||
|     if CORE.data.get(KEY_VFS_SELECT_REQUIRED, False): | ||||
|         # Component requires VFS select - force enable regardless of user setting | ||||
|         add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_SELECT", True) | ||||
|     else: | ||||
|         # No component needs it - allow user to control (default: disabled) | ||||
|         add_idf_sdkconfig_option( | ||||
|             "CONFIG_VFS_SUPPORT_SELECT", | ||||
|             not advanced.get(CONF_DISABLE_VFS_SUPPORT_SELECT, True), | ||||
|         ) | ||||
|  | ||||
|     # Disable VFS support for directory functions (opendir, readdir, mkdir, etc.) | ||||
|     # ESPHome doesn't use directory functions on ESP32. | ||||
|     # Components that need it (e.g., storage components) call require_vfs_dir(). | ||||
|     # Saves approximately 0.5KB+ of flash when disabled (default). | ||||
|     if CORE.data.get(KEY_VFS_DIR_REQUIRED, False): | ||||
|         # Component requires VFS directory support - force enable regardless of user setting | ||||
|         add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_DIR", True) | ||||
|     else: | ||||
|         # No component needs it - allow user to control (default: disabled) | ||||
|         add_idf_sdkconfig_option( | ||||
|             "CONFIG_VFS_SUPPORT_DIR", | ||||
|             not advanced.get(CONF_DISABLE_VFS_SUPPORT_DIR, True), | ||||
|         ) | ||||
|  | ||||
|     cg.add_platformio_option("board_build.partitions", "partitions.csv") | ||||
|     if CONF_PARTITIONS in config: | ||||
|         add_extra_build_file( | ||||
|   | ||||
| @@ -40,13 +40,13 @@ class ESP32InternalGPIOPin : public InternalGPIOPin { | ||||
|   // - 3 bytes for members below | ||||
|   // - 1 byte padding for alignment | ||||
|   // - 4 bytes for vtable pointer | ||||
|   uint8_t pin_;          // GPIO pin number (0-255, actual max ~54 on ESP32) | ||||
|   gpio::Flags flags_{};  // GPIO flags (1 byte) | ||||
|   uint8_t pin_;        // GPIO pin number (0-255, actual max ~54 on ESP32) | ||||
|   gpio::Flags flags_;  // GPIO flags (1 byte) | ||||
|   struct PinFlags { | ||||
|     uint8_t inverted : 1;        // Invert pin logic (1 bit) | ||||
|     uint8_t drive_strength : 2;  // Drive strength 0-3 (2 bits) | ||||
|     uint8_t reserved : 5;        // Reserved for future use (5 bits) | ||||
|   } pin_flags_{};                // Total: 1 byte | ||||
|   } pin_flags_;                  // Total: 1 byte | ||||
|   // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   static bool isr_service_installed; | ||||
| }; | ||||
|   | ||||
| @@ -223,10 +223,7 @@ async def esp32_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}"))) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     if CONF_DRIVE_STRENGTH in config: | ||||
|         cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH])) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|   | ||||
| @@ -29,8 +29,8 @@ class ESP8266GPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
| }; | ||||
|  | ||||
| }  // namespace esp8266 | ||||
|   | ||||
| @@ -165,10 +165,7 @@ async def esp8266_pin_to_code(config): | ||||
|     num = config[CONF_NUMBER] | ||||
|     mode = config[CONF_MODE] | ||||
|     cg.add(var.set_pin(num)) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(mode))) | ||||
|     if num < 16: | ||||
|         initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][ | ||||
|   | ||||
| @@ -32,7 +32,6 @@ from esphome.const import ( | ||||
|     CONF_MISO_PIN, | ||||
|     CONF_MODE, | ||||
|     CONF_MOSI_PIN, | ||||
|     CONF_NUMBER, | ||||
|     CONF_PAGE_ID, | ||||
|     CONF_PIN, | ||||
|     CONF_POLLING_INTERVAL, | ||||
| @@ -53,36 +52,12 @@ from esphome.core import ( | ||||
|     coroutine_with_priority, | ||||
| ) | ||||
| import esphome.final_validate as fv | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| CONFLICTS_WITH = ["wifi"] | ||||
| DEPENDENCIES = ["esp32"] | ||||
| AUTO_LOAD = ["network"] | ||||
| LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| # RMII pins that are hardcoded on ESP32 classic and cannot be changed | ||||
| # These pins are used by the internal Ethernet MAC when using RMII PHYs | ||||
| ESP32_RMII_FIXED_PINS = { | ||||
|     19: "EMAC_TXD0", | ||||
|     21: "EMAC_TX_EN", | ||||
|     22: "EMAC_TXD1", | ||||
|     25: "EMAC_RXD0", | ||||
|     26: "EMAC_RXD1", | ||||
|     27: "EMAC_RX_CRS_DV", | ||||
| } | ||||
|  | ||||
| # RMII default pins for ESP32-P4 | ||||
| # These are the default pins used by ESP-IDF and are configurable in principle, | ||||
| # but ESPHome's ethernet component currently has no way to change them | ||||
| ESP32P4_RMII_DEFAULT_PINS = { | ||||
|     34: "EMAC_TXD0", | ||||
|     35: "EMAC_TXD1", | ||||
|     28: "EMAC_RX_CRS_DV", | ||||
|     29: "EMAC_RXD0", | ||||
|     30: "EMAC_RXD1", | ||||
|     49: "EMAC_TX_EN", | ||||
| } | ||||
|  | ||||
| ethernet_ns = cg.esphome_ns.namespace("ethernet") | ||||
| PHYRegister = ethernet_ns.struct("PHYRegister") | ||||
| CONF_PHY_ADDR = "phy_addr" | ||||
| @@ -298,7 +273,7 @@ CONFIG_SCHEMA = cv.All( | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _final_validate_spi(config): | ||||
| def _final_validate(config): | ||||
|     if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: | ||||
|         return | ||||
|     if spi_configs := fv.full_config.get().get(CONF_SPI): | ||||
| @@ -317,6 +292,9 @@ def _final_validate_spi(config): | ||||
|                     ) | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = _final_validate | ||||
|  | ||||
|  | ||||
| def manual_ip(config): | ||||
|     return cg.StructInitializer( | ||||
|         ManualIP, | ||||
| @@ -405,57 +383,3 @@ async def to_code(config): | ||||
|  | ||||
|     if CORE.using_arduino: | ||||
|         cg.add_library("WiFi", None) | ||||
|  | ||||
|  | ||||
| def _final_validate_rmii_pins(config: ConfigType) -> None: | ||||
|     """Validate that RMII pins are not used by other components.""" | ||||
|     # Only validate for RMII-based PHYs on ESP32/ESP32P4 | ||||
|     if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH": | ||||
|         return  # SPI and OPENETH don't use RMII | ||||
|  | ||||
|     variant = get_esp32_variant() | ||||
|     if variant == VARIANT_ESP32: | ||||
|         rmii_pins = ESP32_RMII_FIXED_PINS | ||||
|         is_configurable = False | ||||
|     elif variant == VARIANT_ESP32P4: | ||||
|         rmii_pins = ESP32P4_RMII_DEFAULT_PINS | ||||
|         is_configurable = True | ||||
|     else: | ||||
|         return  # No RMII validation needed for other variants | ||||
|  | ||||
|     # Check all used pins against RMII reserved pins | ||||
|     for pin_list in pins.PIN_SCHEMA_REGISTRY.pins_used.values(): | ||||
|         for pin_path, _, pin_config in pin_list: | ||||
|             pin_num = pin_config.get(CONF_NUMBER) | ||||
|             if pin_num not in rmii_pins: | ||||
|                 continue | ||||
|             # Found a conflict - show helpful error message | ||||
|             pin_function = rmii_pins[pin_num] | ||||
|             component_path = ".".join(str(p) for p in pin_path) | ||||
|             if is_configurable: | ||||
|                 error_msg = ( | ||||
|                     f"GPIO{pin_num} is used by Ethernet RMII " | ||||
|                     f"({pin_function}) with the current default " | ||||
|                     f"configuration. This conflicts with '{component_path}'. " | ||||
|                     f"Please choose a different GPIO pin for " | ||||
|                     f"'{component_path}'." | ||||
|                 ) | ||||
|             else: | ||||
|                 error_msg = ( | ||||
|                     f"GPIO{pin_num} is reserved for Ethernet RMII " | ||||
|                     f"({pin_function}) and cannot be used. This pin is " | ||||
|                     f"hardcoded by ESP-IDF and cannot be changed when using " | ||||
|                     f"RMII Ethernet PHYs. Please choose a different GPIO pin " | ||||
|                     f"for '{component_path}'." | ||||
|                 ) | ||||
|             raise cv.Invalid(error_msg, path=pin_path) | ||||
|  | ||||
|  | ||||
| def _final_validate(config: ConfigType) -> ConfigType: | ||||
|     """Final validation for Ethernet component.""" | ||||
|     _final_validate_spi(config) | ||||
|     _final_validate_rmii_pins(config) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = _final_validate | ||||
|   | ||||
| @@ -8,19 +8,12 @@ namespace event { | ||||
| static const char *const TAG = "event"; | ||||
|  | ||||
| void Event::trigger(const std::string &event_type) { | ||||
|   // Linear search - faster than std::set for small datasets (1-5 items typical) | ||||
|   const std::string *found = nullptr; | ||||
|   for (const auto &type : this->types_) { | ||||
|     if (type == event_type) { | ||||
|       found = &type; | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
|   if (found == nullptr) { | ||||
|   auto found = types_.find(event_type); | ||||
|   if (found == types_.end()) { | ||||
|     ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str()); | ||||
|     return; | ||||
|   } | ||||
|   last_event_type = found; | ||||
|   last_event_type = &(*found); | ||||
|   ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str()); | ||||
|   this->event_callback_.call(event_type); | ||||
| } | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <set> | ||||
| #include <string> | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| @@ -25,13 +26,13 @@ class Event : public EntityBase, public EntityBase_DeviceClass { | ||||
|   const std::string *last_event_type; | ||||
|  | ||||
|   void trigger(const std::string &event_type); | ||||
|   void set_event_types(const std::initializer_list<std::string> &event_types) { this->types_ = event_types; } | ||||
|   const FixedVector<std::string> &get_event_types() const { return this->types_; } | ||||
|   void set_event_types(const std::set<std::string> &event_types) { this->types_ = event_types; } | ||||
|   std::set<std::string> get_event_types() const { return this->types_; } | ||||
|   void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback); | ||||
|  | ||||
|  protected: | ||||
|   CallbackManager<void(const std::string &event_type)> event_callback_; | ||||
|   FixedVector<std::string> types_; | ||||
|   std::set<std::string> types_; | ||||
| }; | ||||
|  | ||||
| }  // namespace event | ||||
|   | ||||
| @@ -67,7 +67,7 @@ void GPIOSwitch::write_state(bool state) { | ||||
|   this->pin_->digital_write(state); | ||||
|   this->publish_state(state); | ||||
| } | ||||
| void GPIOSwitch::set_interlock(const std::initializer_list<Switch *> &interlock) { this->interlock_ = interlock; } | ||||
| void GPIOSwitch::set_interlock(const std::vector<Switch *> &interlock) { this->interlock_ = interlock; } | ||||
|  | ||||
| }  // namespace gpio | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -2,9 +2,10 @@ | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/components/switch/switch.h" | ||||
|  | ||||
| #include <vector> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace gpio { | ||||
|  | ||||
| @@ -18,14 +19,14 @@ class GPIOSwitch : public switch_::Switch, public Component { | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   void set_interlock(const std::initializer_list<Switch *> &interlock); | ||||
|   void set_interlock(const std::vector<Switch *> &interlock); | ||||
|   void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; } | ||||
|  | ||||
|  protected: | ||||
|   void write_state(bool state) override; | ||||
|  | ||||
|   GPIOPin *pin_; | ||||
|   FixedVector<Switch *> interlock_; | ||||
|   std::vector<Switch *> interlock_; | ||||
|   uint32_t interlock_wait_time_{0}; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -1 +0,0 @@ | ||||
| CODEOWNERS = ["@optimusprimespace", "@ssieb"] | ||||
| @@ -1,111 +0,0 @@ | ||||
| #include "esphome/core/hal.h" | ||||
| #include "hdc2010.h" | ||||
| // https://github.com/vigsterkr/homebridge-hdc2010/blob/main/src/hdc2010.js | ||||
| // https://github.com/lime-labs/HDC2080-Arduino/blob/master/src/HDC2080.cpp | ||||
| namespace esphome { | ||||
| namespace hdc2010 { | ||||
|  | ||||
| static const char *const TAG = "hdc2010"; | ||||
|  | ||||
| static const uint8_t HDC2010_ADDRESS = 0x40;  // 0b1000000 or 0b1000001 from datasheet | ||||
| static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F; | ||||
| static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9; | ||||
| static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00; | ||||
| static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01; | ||||
| static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02; | ||||
| static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03; | ||||
| static const uint8_t CONFIG = 0x0E; | ||||
| static const uint8_t MEASUREMENT_CONFIG = 0x0F; | ||||
|  | ||||
| void HDC2010Component::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
|  | ||||
|   const uint8_t data[2] = { | ||||
|       0b00000000,  // resolution 14bit for both humidity and temperature | ||||
|       0b00000000   // reserved | ||||
|   }; | ||||
|  | ||||
|   if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) { | ||||
|     ESP_LOGW(TAG, "Initial config instruction error"); | ||||
|     this->status_set_warning(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // Set measurement mode to temperature and humidity | ||||
|   uint8_t config_contents; | ||||
|   this->read_register(MEASUREMENT_CONFIG, &config_contents, 1); | ||||
|   config_contents = (config_contents & 0xF9);  // Always set to TEMP_AND_HUMID mode | ||||
|   this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); | ||||
|  | ||||
|   // Set rate to manual | ||||
|   this->read_register(CONFIG, &config_contents, 1); | ||||
|   config_contents &= 0x8F; | ||||
|   this->write_bytes(CONFIG, &config_contents, 1); | ||||
|  | ||||
|   // Set temperature resolution to 14bit | ||||
|   this->read_register(CONFIG, &config_contents, 1); | ||||
|   config_contents &= 0x3F; | ||||
|   this->write_bytes(CONFIG, &config_contents, 1); | ||||
|  | ||||
|   // Set humidity resolution to 14bit | ||||
|   this->read_register(CONFIG, &config_contents, 1); | ||||
|   config_contents &= 0xCF; | ||||
|   this->write_bytes(CONFIG, &config_contents, 1); | ||||
| } | ||||
|  | ||||
| void HDC2010Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "HDC2010:"); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   if (this->is_failed()) { | ||||
|     ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL); | ||||
|   } | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||
|   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); | ||||
| } | ||||
|  | ||||
| void HDC2010Component::update() { | ||||
|   // Trigger measurement | ||||
|   uint8_t config_contents; | ||||
|   this->read_register(CONFIG, &config_contents, 1); | ||||
|   config_contents |= 0x01; | ||||
|   this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1); | ||||
|  | ||||
|   // 1ms delay after triggering the sample | ||||
|   set_timeout(1, [this]() { | ||||
|     if (this->temperature_sensor_ != nullptr) { | ||||
|       float temp = this->read_temp(); | ||||
|       this->temperature_sensor_->publish_state(temp); | ||||
|       ESP_LOGD(TAG, "Temp=%.1f°C", temp); | ||||
|     } | ||||
|  | ||||
|     if (this->humidity_sensor_ != nullptr) { | ||||
|       float humidity = this->read_humidity(); | ||||
|       this->humidity_sensor_->publish_state(humidity); | ||||
|       ESP_LOGD(TAG, "Humidity=%.1f%%", humidity); | ||||
|     } | ||||
|   }); | ||||
| } | ||||
|  | ||||
| float HDC2010Component::read_temp() { | ||||
|   uint8_t byte[2]; | ||||
|  | ||||
|   this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1); | ||||
|   this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1); | ||||
|  | ||||
|   uint16_t temp = encode_uint16(byte[1], byte[0]); | ||||
|   return (float) temp * 0.0025177f - 40.0f; | ||||
| } | ||||
|  | ||||
| float HDC2010Component::read_humidity() { | ||||
|   uint8_t byte[2]; | ||||
|  | ||||
|   this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1); | ||||
|   this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1); | ||||
|  | ||||
|   uint16_t humidity = encode_uint16(byte[1], byte[0]); | ||||
|   return (float) humidity * 0.001525879f; | ||||
| } | ||||
|  | ||||
| }  // namespace hdc2010 | ||||
| }  // namespace esphome | ||||
| @@ -1,32 +0,0 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
| #include "esphome/components/i2c/i2c.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace hdc2010 { | ||||
|  | ||||
| class HDC2010Component : public PollingComponent, public i2c::I2CDevice { | ||||
|  public: | ||||
|   void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; } | ||||
|  | ||||
|   void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; } | ||||
|  | ||||
|   /// Setup the sensor and check for connection. | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   /// Retrieve the latest sensor values. This operation takes approximately 16ms. | ||||
|   void update() override; | ||||
|  | ||||
|   float read_temp(); | ||||
|  | ||||
|   float read_humidity(); | ||||
|  | ||||
|  protected: | ||||
|   sensor::Sensor *temperature_sensor_{nullptr}; | ||||
|   sensor::Sensor *humidity_sensor_{nullptr}; | ||||
| }; | ||||
|  | ||||
| }  // namespace hdc2010 | ||||
| }  // namespace esphome | ||||
| @@ -1,56 +0,0 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import i2c, sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_HUMIDITY, | ||||
|     CONF_ID, | ||||
|     CONF_TEMPERATURE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_PERCENT, | ||||
| ) | ||||
|  | ||||
| DEPENDENCIES = ["i2c"] | ||||
|  | ||||
| hdc2010_ns = cg.esphome_ns.namespace("hdc2010") | ||||
| HDC2010Component = hdc2010_ns.class_( | ||||
|     "HDC2010Component", cg.PollingComponent, i2c.I2CDevice | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     cv.Schema( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(HDC2010Component), | ||||
|             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.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||
|                 unit_of_measurement=UNIT_PERCENT, | ||||
|                 accuracy_decimals=0, | ||||
|                 device_class=DEVICE_CLASS_HUMIDITY, | ||||
|                 state_class=STATE_CLASS_MEASUREMENT, | ||||
|             ), | ||||
|         } | ||||
|     ) | ||||
|     .extend(cv.polling_component_schema("60s")) | ||||
|     .extend(i2c.i2c_device_schema(0x40)) | ||||
| ) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await i2c.register_i2c_device(var, config) | ||||
|  | ||||
|     if temperature_config := config.get(CONF_TEMPERATURE): | ||||
|         sens = await sensor.new_sensor(temperature_config) | ||||
|         cg.add(var.set_temperature_sensor(sens)) | ||||
|  | ||||
|     if humidity_config := config.get(CONF_HUMIDITY): | ||||
|         sens = await sensor.new_sensor(humidity_config) | ||||
|         cg.add(var.set_humidity_sensor(sens)) | ||||
| @@ -28,8 +28,8 @@ class HostGPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
| }; | ||||
|  | ||||
| }  // namespace host | ||||
|   | ||||
| @@ -57,9 +57,6 @@ async def host_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(num)) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
|   | ||||
| @@ -169,7 +169,7 @@ class HttpRequestComponent : public Component { | ||||
|  protected: | ||||
|   virtual std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, | ||||
|                                                  const std::string &body, const std::list<Header> &request_headers, | ||||
|                                                  const std::set<std::string> &collect_headers) = 0; | ||||
|                                                  std::set<std::string> collect_headers) = 0; | ||||
|   const char *useragent_{nullptr}; | ||||
|   bool follow_redirects_{}; | ||||
|   uint16_t redirect_limit_{}; | ||||
|   | ||||
| @@ -17,7 +17,7 @@ static const char *const TAG = "http_request.arduino"; | ||||
| std::shared_ptr<HttpContainer> HttpRequestArduino::perform(const std::string &url, const std::string &method, | ||||
|                                                            const std::string &body, | ||||
|                                                            const std::list<Header> &request_headers, | ||||
|                                                            const std::set<std::string> &collect_headers) { | ||||
|                                                            std::set<std::string> collect_headers) { | ||||
|   if (!network::is_connected()) { | ||||
|     this->status_momentary_error("failed", 1000); | ||||
|     ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); | ||||
|   | ||||
| @@ -33,7 +33,7 @@ class HttpRequestArduino : public HttpRequestComponent { | ||||
|  protected: | ||||
|   std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body, | ||||
|                                          const std::list<Header> &request_headers, | ||||
|                                          const std::set<std::string> &collect_headers) override; | ||||
|                                          std::set<std::string> collect_headers) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace http_request | ||||
|   | ||||
| @@ -20,7 +20,7 @@ static const char *const TAG = "http_request.host"; | ||||
| std::shared_ptr<HttpContainer> HttpRequestHost::perform(const std::string &url, const std::string &method, | ||||
|                                                         const std::string &body, | ||||
|                                                         const std::list<Header> &request_headers, | ||||
|                                                         const std::set<std::string> &response_headers) { | ||||
|                                                         std::set<std::string> response_headers) { | ||||
|   if (!network::is_connected()) { | ||||
|     this->status_momentary_error("failed", 1000); | ||||
|     ESP_LOGW(TAG, "HTTP Request failed; Not connected to network"); | ||||
|   | ||||
| @@ -20,7 +20,7 @@ class HttpRequestHost : public HttpRequestComponent { | ||||
|  public: | ||||
|   std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body, | ||||
|                                          const std::list<Header> &request_headers, | ||||
|                                          const std::set<std::string> &response_headers) override; | ||||
|                                          std::set<std::string> response_headers) override; | ||||
|   void set_ca_path(const char *ca_path) { this->ca_path_ = ca_path; } | ||||
|  | ||||
|  protected: | ||||
|   | ||||
| @@ -55,7 +55,7 @@ esp_err_t HttpRequestIDF::http_event_handler(esp_http_client_event_t *evt) { | ||||
| std::shared_ptr<HttpContainer> HttpRequestIDF::perform(const std::string &url, const std::string &method, | ||||
|                                                        const std::string &body, | ||||
|                                                        const std::list<Header> &request_headers, | ||||
|                                                        const std::set<std::string> &collect_headers) { | ||||
|                                                        std::set<std::string> collect_headers) { | ||||
|   if (!network::is_connected()) { | ||||
|     this->status_momentary_error("failed", 1000); | ||||
|     ESP_LOGE(TAG, "HTTP Request failed; Not connected to network"); | ||||
|   | ||||
| @@ -39,7 +39,7 @@ class HttpRequestIDF : public HttpRequestComponent { | ||||
|  protected: | ||||
|   std::shared_ptr<HttpContainer> perform(const std::string &url, const std::string &method, const std::string &body, | ||||
|                                          const std::list<Header> &request_headers, | ||||
|                                          const std::set<std::string> &collect_headers) override; | ||||
|                                          std::set<std::string> collect_headers) override; | ||||
|   // if zero ESP-IDF will use DEFAULT_HTTP_BUF_SIZE | ||||
|   uint16_t buffer_size_rx_{}; | ||||
|   uint16_t buffer_size_tx_{}; | ||||
|   | ||||
| @@ -28,38 +28,6 @@ void ImprovSerialComponent::setup() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::loop() { | ||||
|   if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { | ||||
|     this->last_read_byte_ = 0; | ||||
|     this->rx_buffer_.clear(); | ||||
|     ESP_LOGV(TAG, "Timeout"); | ||||
|   } | ||||
|  | ||||
|   auto byte = this->read_byte_(); | ||||
|   while (byte.has_value()) { | ||||
|     if (this->parse_improv_serial_byte_(byte.value())) { | ||||
|       this->last_read_byte_ = millis(); | ||||
|     } else { | ||||
|       this->last_read_byte_ = 0; | ||||
|       this->rx_buffer_.clear(); | ||||
|     } | ||||
|     byte = this->read_byte_(); | ||||
|   } | ||||
|  | ||||
|   if (this->state_ == improv::STATE_PROVISIONING) { | ||||
|     if (wifi::global_wifi_component->is_connected()) { | ||||
|       wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), | ||||
|                                                  this->connecting_sta_.get_password()); | ||||
|       this->connecting_sta_ = {}; | ||||
|       this->cancel_timeout("wifi-connect-timeout"); | ||||
|       this->set_state_(improv::STATE_PROVISIONED); | ||||
|  | ||||
|       std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); | ||||
|       this->send_response_(url); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::dump_config() { ESP_LOGCONFIG(TAG, "Improv Serial:"); } | ||||
|  | ||||
| optional<uint8_t> ImprovSerialComponent::read_byte_() { | ||||
| @@ -110,28 +78,8 @@ optional<uint8_t> ImprovSerialComponent::read_byte_() { | ||||
|   return byte; | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) { | ||||
|   // First, set length field | ||||
|   this->tx_header_[TX_LENGTH_IDX] = this->tx_header_[TX_TYPE_IDX] == TYPE_RPC_RESPONSE ? size : 1; | ||||
|  | ||||
|   const bool there_is_data = data != nullptr && size > 0; | ||||
|   // If there_is_data, checksum must not include our optional data byte | ||||
|   const uint8_t header_checksum_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE - 2; | ||||
|   // Only transmit the full buffer length if there is no data (only state/error byte is provided in this case) | ||||
|   const uint8_t header_tx_len = there_is_data ? TX_BUFFER_SIZE - 3 : TX_BUFFER_SIZE; | ||||
|   // Calculate checksum for message | ||||
|   uint8_t checksum = 0; | ||||
|   for (uint8_t i = 0; i < header_checksum_len; i++) { | ||||
|     checksum += this->tx_header_[i]; | ||||
|   } | ||||
|   if (there_is_data) { | ||||
|     // Include data in checksum | ||||
|     for (size_t i = 0; i < size; i++) { | ||||
|       checksum += data[i]; | ||||
|     } | ||||
|   } | ||||
|   this->tx_header_[TX_CHECKSUM_IDX] = checksum; | ||||
|  | ||||
| void ImprovSerialComponent::write_data_(std::vector<uint8_t> &data) { | ||||
|   data.push_back('\n'); | ||||
| #ifdef USE_ESP32 | ||||
|   switch (logger::global_logger->get_uart()) { | ||||
|     case logger::UART_SELECTION_UART0: | ||||
| @@ -139,45 +87,63 @@ void ImprovSerialComponent::write_data_(const uint8_t *data, const size_t size) | ||||
| #if !defined(USE_ESP32_VARIANT_ESP32C3) && !defined(USE_ESP32_VARIANT_ESP32C6) && \ | ||||
|     !defined(USE_ESP32_VARIANT_ESP32S2) && !defined(USE_ESP32_VARIANT_ESP32S3) | ||||
|     case logger::UART_SELECTION_UART2: | ||||
| #endif | ||||
|       uart_write_bytes(this->uart_num_, this->tx_header_, header_tx_len); | ||||
|       if (there_is_data) { | ||||
|         uart_write_bytes(this->uart_num_, data, size); | ||||
|         uart_write_bytes(this->uart_num_, &this->tx_header_[TX_CHECKSUM_IDX], 2);  // Footer: checksum and newline | ||||
|       } | ||||
| #endif  // !USE_ESP32_VARIANT_ESP32C3 && !USE_ESP32_VARIANT_ESP32S2 && !USE_ESP32_VARIANT_ESP32S3 | ||||
|       uart_write_bytes(this->uart_num_, data.data(), data.size()); | ||||
|       break; | ||||
| #if defined(USE_LOGGER_USB_CDC) && defined(CONFIG_ESP_CONSOLE_USB_CDC) | ||||
|     case logger::UART_SELECTION_USB_CDC: | ||||
|       esp_usb_console_write_buf((const char *) this->tx_header_, header_tx_len); | ||||
|       if (there_is_data) { | ||||
|         esp_usb_console_write_buf((const char *) data, size); | ||||
|         esp_usb_console_write_buf((const char *) &this->tx_header_[TX_CHECKSUM_IDX], | ||||
|                                   2);  // Footer: checksum and newline | ||||
|       } | ||||
|     case logger::UART_SELECTION_USB_CDC: { | ||||
|       const char *msg = (char *) data.data(); | ||||
|       esp_usb_console_write_buf(msg, data.size()); | ||||
|       break; | ||||
| #endif | ||||
|     } | ||||
| #endif  // USE_LOGGER_USB_CDC | ||||
| #ifdef USE_LOGGER_USB_SERIAL_JTAG | ||||
|     case logger::UART_SELECTION_USB_SERIAL_JTAG: | ||||
|       usb_serial_jtag_write_bytes((const char *) this->tx_header_, header_tx_len, 20 / portTICK_PERIOD_MS); | ||||
|       if (there_is_data) { | ||||
|         usb_serial_jtag_write_bytes((const char *) data, size, 20 / portTICK_PERIOD_MS); | ||||
|         usb_serial_jtag_write_bytes((const char *) &this->tx_header_[TX_CHECKSUM_IDX], 2, | ||||
|                                     20 / portTICK_PERIOD_MS);  // Footer: checksum and newline | ||||
|       } | ||||
|       usb_serial_jtag_write_bytes((char *) data.data(), data.size(), 20 / portTICK_PERIOD_MS); | ||||
|       delay(10); | ||||
|       usb_serial_jtag_ll_txfifo_flush();  // fixes for issue in IDF 4.4.7 | ||||
|       break; | ||||
| #endif | ||||
| #endif  // USE_LOGGER_USB_SERIAL_JTAG | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| #elif defined(USE_ARDUINO) | ||||
|   this->hw_serial_->write(this->tx_header_, header_tx_len); | ||||
|   if (there_is_data) { | ||||
|     this->hw_serial_->write(data, size); | ||||
|     this->hw_serial_->write(&this->tx_header_[TX_CHECKSUM_IDX], 2);  // Footer: checksum and newline | ||||
|   } | ||||
|   this->hw_serial_->write(data.data(), data.size()); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::loop() { | ||||
|   if (this->last_read_byte_ && (millis() - this->last_read_byte_ > IMPROV_SERIAL_TIMEOUT)) { | ||||
|     this->last_read_byte_ = 0; | ||||
|     this->rx_buffer_.clear(); | ||||
|     ESP_LOGV(TAG, "Improv Serial timeout"); | ||||
|   } | ||||
|  | ||||
|   auto byte = this->read_byte_(); | ||||
|   while (byte.has_value()) { | ||||
|     if (this->parse_improv_serial_byte_(byte.value())) { | ||||
|       this->last_read_byte_ = millis(); | ||||
|     } else { | ||||
|       this->last_read_byte_ = 0; | ||||
|       this->rx_buffer_.clear(); | ||||
|     } | ||||
|     byte = this->read_byte_(); | ||||
|   } | ||||
|  | ||||
|   if (this->state_ == improv::STATE_PROVISIONING) { | ||||
|     if (wifi::global_wifi_component->is_connected()) { | ||||
|       wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), | ||||
|                                                  this->connecting_sta_.get_password()); | ||||
|       this->connecting_sta_ = {}; | ||||
|       this->cancel_timeout("wifi-connect-timeout"); | ||||
|       this->set_state_(improv::STATE_PROVISIONED); | ||||
|  | ||||
|       std::vector<uint8_t> url = this->build_rpc_settings_response_(improv::WIFI_SETTINGS); | ||||
|       this->send_response_(url); | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
| std::vector<uint8_t> ImprovSerialComponent::build_rpc_settings_response_(improv::Command command) { | ||||
|   std::vector<std::string> urls; | ||||
| #ifdef USE_IMPROV_SERIAL_NEXT_URL | ||||
| @@ -211,13 +177,13 @@ std::vector<uint8_t> ImprovSerialComponent::build_version_info_() { | ||||
| bool ImprovSerialComponent::parse_improv_serial_byte_(uint8_t byte) { | ||||
|   size_t at = this->rx_buffer_.size(); | ||||
|   this->rx_buffer_.push_back(byte); | ||||
|   ESP_LOGV(TAG, "Byte: 0x%02X", byte); | ||||
|   ESP_LOGV(TAG, "Improv Serial byte: 0x%02X", byte); | ||||
|   const uint8_t *raw = &this->rx_buffer_[0]; | ||||
|  | ||||
|   return improv::parse_improv_serial_byte( | ||||
|       at, byte, raw, [this](improv::ImprovCommand command) -> bool { return this->parse_improv_payload_(command); }, | ||||
|       [this](improv::Error error) -> void { | ||||
|         ESP_LOGW(TAG, "Error decoding payload"); | ||||
|         ESP_LOGW(TAG, "Error decoding Improv payload"); | ||||
|         this->set_error_(error); | ||||
|       }); | ||||
| } | ||||
| @@ -233,7 +199,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command | ||||
|       wifi::global_wifi_component->set_sta(sta); | ||||
|       wifi::global_wifi_component->start_connecting(sta, false); | ||||
|       this->set_state_(improv::STATE_PROVISIONING); | ||||
|       ESP_LOGD(TAG, "Received settings: SSID=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), | ||||
|       ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(), | ||||
|                command.password.c_str()); | ||||
|  | ||||
|       auto f = std::bind(&ImprovSerialComponent::on_wifi_connect_timeout_, this); | ||||
| @@ -274,7 +240,7 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command | ||||
|       return true; | ||||
|     } | ||||
|     default: { | ||||
|       ESP_LOGW(TAG, "Unknown payload"); | ||||
|       ESP_LOGW(TAG, "Unknown Improv payload"); | ||||
|       this->set_error_(improv::ERROR_UNKNOWN_RPC); | ||||
|       return false; | ||||
|     } | ||||
| @@ -283,26 +249,57 @@ bool ImprovSerialComponent::parse_improv_payload_(improv::ImprovCommand &command | ||||
|  | ||||
| void ImprovSerialComponent::set_state_(improv::State state) { | ||||
|   this->state_ = state; | ||||
|   this->tx_header_[TX_TYPE_IDX] = TYPE_CURRENT_STATE; | ||||
|   this->tx_header_[TX_DATA_IDX] = state; | ||||
|   this->write_data_(); | ||||
|  | ||||
|   std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; | ||||
|   data.resize(11); | ||||
|   data[6] = IMPROV_SERIAL_VERSION; | ||||
|   data[7] = TYPE_CURRENT_STATE; | ||||
|   data[8] = 1; | ||||
|   data[9] = state; | ||||
|  | ||||
|   uint8_t checksum = 0x00; | ||||
|   for (uint8_t d : data) | ||||
|     checksum += d; | ||||
|   data[10] = checksum; | ||||
|  | ||||
|   this->write_data_(data); | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::set_error_(improv::Error error) { | ||||
|   this->tx_header_[TX_TYPE_IDX] = TYPE_ERROR_STATE; | ||||
|   this->tx_header_[TX_DATA_IDX] = error; | ||||
|   this->write_data_(); | ||||
|   std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; | ||||
|   data.resize(11); | ||||
|   data[6] = IMPROV_SERIAL_VERSION; | ||||
|   data[7] = TYPE_ERROR_STATE; | ||||
|   data[8] = 1; | ||||
|   data[9] = error; | ||||
|  | ||||
|   uint8_t checksum = 0x00; | ||||
|   for (uint8_t d : data) | ||||
|     checksum += d; | ||||
|   data[10] = checksum; | ||||
|   this->write_data_(data); | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::send_response_(std::vector<uint8_t> &response) { | ||||
|   this->tx_header_[TX_TYPE_IDX] = TYPE_RPC_RESPONSE; | ||||
|   this->write_data_(response.data(), response.size()); | ||||
|   std::vector<uint8_t> data = {'I', 'M', 'P', 'R', 'O', 'V'}; | ||||
|   data.resize(9); | ||||
|   data[6] = IMPROV_SERIAL_VERSION; | ||||
|   data[7] = TYPE_RPC_RESPONSE; | ||||
|   data[8] = response.size(); | ||||
|   data.insert(data.end(), response.begin(), response.end()); | ||||
|  | ||||
|   uint8_t checksum = 0x00; | ||||
|   for (uint8_t d : data) | ||||
|     checksum += d; | ||||
|   data.push_back(checksum); | ||||
|  | ||||
|   this->write_data_(data); | ||||
| } | ||||
|  | ||||
| void ImprovSerialComponent::on_wifi_connect_timeout_() { | ||||
|   this->set_error_(improv::ERROR_UNABLE_TO_CONNECT); | ||||
|   this->set_state_(improv::STATE_AUTHORIZED); | ||||
|   ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network"); | ||||
|   ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network"); | ||||
|   wifi::global_wifi_component->clear_sta(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -26,16 +26,6 @@ | ||||
| namespace esphome { | ||||
| namespace improv_serial { | ||||
|  | ||||
| // TX buffer layout constants | ||||
| static constexpr uint8_t TX_HEADER_SIZE = 6;  // Bytes 0-5 = "IMPROV" | ||||
| static constexpr uint8_t TX_VERSION_IDX = 6; | ||||
| static constexpr uint8_t TX_TYPE_IDX = 7; | ||||
| static constexpr uint8_t TX_LENGTH_IDX = 8; | ||||
| static constexpr uint8_t TX_DATA_IDX = 9;  // For state/error messages only | ||||
| static constexpr uint8_t TX_CHECKSUM_IDX = 10; | ||||
| static constexpr uint8_t TX_NEWLINE_IDX = 11; | ||||
| static constexpr uint8_t TX_BUFFER_SIZE = 12; | ||||
|  | ||||
| enum ImprovSerialType : uint8_t { | ||||
|   TYPE_CURRENT_STATE = 0x01, | ||||
|   TYPE_ERROR_STATE = 0x02, | ||||
| @@ -67,22 +57,7 @@ class ImprovSerialComponent : public Component, public improv_base::ImprovBase { | ||||
|   std::vector<uint8_t> build_version_info_(); | ||||
|  | ||||
|   optional<uint8_t> read_byte_(); | ||||
|   void write_data_(const uint8_t *data = nullptr, size_t size = 0); | ||||
|  | ||||
|   uint8_t tx_header_[TX_BUFFER_SIZE] = { | ||||
|       'I',                    // 0: Header | ||||
|       'M',                    // 1: Header | ||||
|       'P',                    // 2: Header | ||||
|       'R',                    // 3: Header | ||||
|       'O',                    // 4: Header | ||||
|       'V',                    // 5: Header | ||||
|       IMPROV_SERIAL_VERSION,  // 6: Version | ||||
|       0,                      // 7: ImprovSerialType | ||||
|       0,                      // 8: Length | ||||
|       0,                      // 9...X: Data (here, one byte reserved for state/error) | ||||
|       0,                      // X + 10: Checksum | ||||
|       '\n', | ||||
|   }; | ||||
|   void write_data_(std::vector<uint8_t> &data); | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   uart_port_t uart_num_; | ||||
|   | ||||
| @@ -199,9 +199,6 @@ async def component_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(num)) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
|   | ||||
| @@ -27,8 +27,8 @@ class ArduinoInternalGPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
| }; | ||||
|  | ||||
| }  // namespace libretiny | ||||
|   | ||||
| @@ -61,10 +61,6 @@ void AddressableLightTransformer::start() { | ||||
|   this->target_color_ *= to_uint8_scale(end_values.get_brightness() * end_values.get_state()); | ||||
| } | ||||
|  | ||||
| inline constexpr uint8_t subtract_scaled_difference(uint8_t a, uint8_t b, int32_t scale) { | ||||
|   return uint8_t(int32_t(a) - (((int32_t(a) - int32_t(b)) * scale) / 256)); | ||||
| } | ||||
|  | ||||
| optional<LightColorValues> AddressableLightTransformer::apply() { | ||||
|   float smoothed_progress = LightTransformer::smoothed_progress(this->get_progress_()); | ||||
|  | ||||
| @@ -78,37 +74,38 @@ optional<LightColorValues> AddressableLightTransformer::apply() { | ||||
|   // all LEDs, we use the current state of each LED as the start. | ||||
|  | ||||
|   // We can't use a direct lerp smoothing here though - that would require creating a copy of the original | ||||
|   // state of each LED at the start of the transition. Instead, we "fake" the look of lerp by calculating | ||||
|   // the delta between the current state and the target state, assuming that the delta represents the rest | ||||
|   // of the transition that was to be applied as of the previous transition step, and scaling the delta for | ||||
|   // what should be left after the current transition step. In this manner, the delta decays to zero as the | ||||
|   // transition progresses. | ||||
|   // | ||||
|   // Here's an example of how the algorithm progresses in discrete steps: | ||||
|   // | ||||
|   // At time = 0.00, 0% complete, 100% remaining, 100% will remain after this step, so the scale is 100% / 100% = 100%. | ||||
|   // At time = 0.10, 0% complete, 100% remaining, 90% will remain after this step, so the scale is 90% / 100% = 90%. | ||||
|   // At time = 0.20, 10% complete, 90% remaining, 80% will remain after this step, so the scale is 80% / 90% = 88.9%. | ||||
|   // At time = 0.50, 20% complete, 80% remaining, 50% will remain after this step, so the scale is 50% / 80% = 62.5%. | ||||
|   // At time = 0.90, 50% complete, 50% remaining, 10% will remain after this step, so the scale is 10% / 50% = 20%. | ||||
|   // At time = 0.91, 90% complete, 10% remaining, 9% will remain after this step, so the scale is 9% / 10% = 90%. | ||||
|   // At time = 1.00, 91% complete, 9% remaining, 0% will remain after this step, so the scale is 0% / 9% = 0%. | ||||
|   // | ||||
|   // Because the color values are quantized to 8 bit resolution after each step, the transition may appear | ||||
|   // non-linear when applying small deltas. | ||||
|   // state of each LED at the start of the transition. | ||||
|   // Instead, we "fake" the look of the LERP by using an exponential average over time and using | ||||
|   // dynamically-calculated alpha values to match the look. | ||||
|  | ||||
|   if (smoothed_progress > this->last_transition_progress_ && this->last_transition_progress_ < 1.f) { | ||||
|     int32_t scale = int32_t(256.f * std::max((1.f - smoothed_progress) / (1.f - this->last_transition_progress_), 0.f)); | ||||
|     for (auto led : this->light_) { | ||||
|       led.set_rgbw(subtract_scaled_difference(this->target_color_.red, led.get_red(), scale), | ||||
|                    subtract_scaled_difference(this->target_color_.green, led.get_green(), scale), | ||||
|                    subtract_scaled_difference(this->target_color_.blue, led.get_blue(), scale), | ||||
|                    subtract_scaled_difference(this->target_color_.white, led.get_white(), scale)); | ||||
|     } | ||||
|     this->last_transition_progress_ = smoothed_progress; | ||||
|     this->light_.schedule_show(); | ||||
|   float denom = (1.0f - smoothed_progress); | ||||
|   float alpha = denom == 0.0f ? 1.0f : (smoothed_progress - this->last_transition_progress_) / denom; | ||||
|  | ||||
|   // We need to use a low-resolution alpha here which makes the transition set in only after ~half of the length | ||||
|   // We solve this by accumulating the fractional part of the alpha over time. | ||||
|   float alpha255 = alpha * 255.0f; | ||||
|   float alpha255int = floorf(alpha255); | ||||
|   float alpha255remainder = alpha255 - alpha255int; | ||||
|  | ||||
|   this->accumulated_alpha_ += alpha255remainder; | ||||
|   float alpha_add = floorf(this->accumulated_alpha_); | ||||
|   this->accumulated_alpha_ -= alpha_add; | ||||
|  | ||||
|   alpha255 += alpha_add; | ||||
|   alpha255 = clamp(alpha255, 0.0f, 255.0f); | ||||
|   auto alpha8 = static_cast<uint8_t>(alpha255); | ||||
|  | ||||
|   if (alpha8 != 0) { | ||||
|     uint8_t inv_alpha8 = 255 - alpha8; | ||||
|     Color add = this->target_color_ * alpha8; | ||||
|  | ||||
|     for (auto led : this->light_) | ||||
|       led.set(add + led.get() * inv_alpha8); | ||||
|   } | ||||
|  | ||||
|   this->last_transition_progress_ = smoothed_progress; | ||||
|   this->light_.schedule_show(); | ||||
|  | ||||
|   return {}; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -113,6 +113,7 @@ class AddressableLightTransformer : public LightTransformer { | ||||
|  protected: | ||||
|   AddressableLight &light_; | ||||
|   float last_transition_progress_{0.0f}; | ||||
|   float accumulated_alpha_{0.0f}; | ||||
|   Color target_color_{}; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <utility> | ||||
| #include <vector> | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/components/light/light_state.h" | ||||
| #include "esphome/components/light/addressable_light.h" | ||||
|  | ||||
| @@ -30,7 +30,7 @@ inline static uint8_t half_sin8(uint8_t v) { return sin16_c(uint16_t(v) * 128u) | ||||
|  | ||||
| class AddressableLightEffect : public LightEffect { | ||||
|  public: | ||||
|   explicit AddressableLightEffect(const char *name) : LightEffect(name) {} | ||||
|   explicit AddressableLightEffect(const std::string &name) : LightEffect(name) {} | ||||
|   void start_internal() override { | ||||
|     this->get_addressable_()->set_effect_active(true); | ||||
|     this->get_addressable_()->clear_effect_data(); | ||||
| @@ -57,7 +57,8 @@ class AddressableLightEffect : public LightEffect { | ||||
|  | ||||
| class AddressableLambdaLightEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   AddressableLambdaLightEffect(const char *name, std::function<void(AddressableLight &, Color, bool initial_run)> f, | ||||
|   AddressableLambdaLightEffect(const std::string &name, | ||||
|                                std::function<void(AddressableLight &, Color, bool initial_run)> f, | ||||
|                                uint32_t update_interval) | ||||
|       : AddressableLightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} | ||||
|   void start() override { this->initial_run_ = true; } | ||||
| @@ -80,7 +81,7 @@ class AddressableLambdaLightEffect : public AddressableLightEffect { | ||||
|  | ||||
| class AddressableRainbowLightEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   explicit AddressableRainbowLightEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|   explicit AddressableRainbowLightEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|   void apply(AddressableLight &it, const Color ¤t_color) override { | ||||
|     ESPHSVColor hsv; | ||||
|     hsv.value = 255; | ||||
| @@ -111,8 +112,8 @@ struct AddressableColorWipeEffectColor { | ||||
|  | ||||
| class AddressableColorWipeEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   explicit AddressableColorWipeEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|   void set_colors(const std::initializer_list<AddressableColorWipeEffectColor> &colors) { this->colors_ = colors; } | ||||
|   explicit AddressableColorWipeEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|   void set_colors(const std::vector<AddressableColorWipeEffectColor> &colors) { this->colors_ = colors; } | ||||
|   void set_add_led_interval(uint32_t add_led_interval) { this->add_led_interval_ = add_led_interval; } | ||||
|   void set_reverse(bool reverse) { this->reverse_ = reverse; } | ||||
|   void apply(AddressableLight &it, const Color ¤t_color) override { | ||||
| @@ -154,7 +155,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   FixedVector<AddressableColorWipeEffectColor> colors_; | ||||
|   std::vector<AddressableColorWipeEffectColor> colors_; | ||||
|   size_t at_color_{0}; | ||||
|   uint32_t last_add_{0}; | ||||
|   uint32_t add_led_interval_{}; | ||||
| @@ -164,7 +165,7 @@ class AddressableColorWipeEffect : public AddressableLightEffect { | ||||
|  | ||||
| class AddressableScanEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   explicit AddressableScanEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|   explicit AddressableScanEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|   void set_move_interval(uint32_t move_interval) { this->move_interval_ = move_interval; } | ||||
|   void set_scan_width(uint32_t scan_width) { this->scan_width_ = scan_width; } | ||||
|   void apply(AddressableLight &it, const Color ¤t_color) override { | ||||
| @@ -201,7 +202,7 @@ class AddressableScanEffect : public AddressableLightEffect { | ||||
|  | ||||
| class AddressableTwinkleEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   explicit AddressableTwinkleEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|   explicit AddressableTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|   void apply(AddressableLight &addressable, const Color ¤t_color) override { | ||||
|     const uint32_t now = millis(); | ||||
|     uint8_t pos_add = 0; | ||||
| @@ -243,7 +244,7 @@ class AddressableTwinkleEffect : public AddressableLightEffect { | ||||
|  | ||||
| class AddressableRandomTwinkleEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   explicit AddressableRandomTwinkleEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|   explicit AddressableRandomTwinkleEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|   void apply(AddressableLight &it, const Color ¤t_color) override { | ||||
|     const uint32_t now = millis(); | ||||
|     uint8_t pos_add = 0; | ||||
| @@ -292,7 +293,7 @@ class AddressableRandomTwinkleEffect : public AddressableLightEffect { | ||||
|  | ||||
| class AddressableFireworksEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   explicit AddressableFireworksEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|   explicit AddressableFireworksEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|   void start() override { | ||||
|     auto &it = *this->get_addressable_(); | ||||
|     it.all() = Color::BLACK; | ||||
| @@ -341,7 +342,7 @@ class AddressableFireworksEffect : public AddressableLightEffect { | ||||
|  | ||||
| class AddressableFlickerEffect : public AddressableLightEffect { | ||||
|  public: | ||||
|   explicit AddressableFlickerEffect(const char *name) : AddressableLightEffect(name) {} | ||||
|   explicit AddressableFlickerEffect(const std::string &name) : AddressableLightEffect(name) {} | ||||
|   void apply(AddressableLight &it, const Color ¤t_color) override { | ||||
|     const uint32_t now = millis(); | ||||
|     const uint8_t intensity = this->intensity_; | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <utility> | ||||
| #include <vector> | ||||
|  | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "light_effect.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -17,7 +17,7 @@ inline static float random_cubic_float() { | ||||
| /// Pulse effect. | ||||
| class PulseLightEffect : public LightEffect { | ||||
|  public: | ||||
|   explicit PulseLightEffect(const char *name) : LightEffect(name) {} | ||||
|   explicit PulseLightEffect(const std::string &name) : LightEffect(name) {} | ||||
|  | ||||
|   void apply() override { | ||||
|     const uint32_t now = millis(); | ||||
| @@ -60,7 +60,7 @@ class PulseLightEffect : public LightEffect { | ||||
| /// Random effect. Sets random colors every 10 seconds and slowly transitions between them. | ||||
| class RandomLightEffect : public LightEffect { | ||||
|  public: | ||||
|   explicit RandomLightEffect(const char *name) : LightEffect(name) {} | ||||
|   explicit RandomLightEffect(const std::string &name) : LightEffect(name) {} | ||||
|  | ||||
|   void apply() override { | ||||
|     const uint32_t now = millis(); | ||||
| @@ -112,7 +112,7 @@ class RandomLightEffect : public LightEffect { | ||||
|  | ||||
| class LambdaLightEffect : public LightEffect { | ||||
|  public: | ||||
|   LambdaLightEffect(const char *name, std::function<void(bool initial_run)> f, uint32_t update_interval) | ||||
|   LambdaLightEffect(const std::string &name, std::function<void(bool initial_run)> f, uint32_t update_interval) | ||||
|       : LightEffect(name), f_(std::move(f)), update_interval_(update_interval) {} | ||||
|  | ||||
|   void start() override { this->initial_run_ = true; } | ||||
| @@ -138,7 +138,7 @@ class LambdaLightEffect : public LightEffect { | ||||
|  | ||||
| class AutomationLightEffect : public LightEffect { | ||||
|  public: | ||||
|   AutomationLightEffect(const char *name) : LightEffect(name) {} | ||||
|   AutomationLightEffect(const std::string &name) : LightEffect(name) {} | ||||
|   void stop() override { this->trig_->stop_action(); } | ||||
|   void apply() override { | ||||
|     if (!this->trig_->is_action_running()) { | ||||
| @@ -163,7 +163,7 @@ struct StrobeLightEffectColor { | ||||
|  | ||||
| class StrobeLightEffect : public LightEffect { | ||||
|  public: | ||||
|   explicit StrobeLightEffect(const char *name) : LightEffect(name) {} | ||||
|   explicit StrobeLightEffect(const std::string &name) : LightEffect(name) {} | ||||
|   void apply() override { | ||||
|     const uint32_t now = millis(); | ||||
|     if (now - this->last_switch_ < this->colors_[this->at_color_].duration) | ||||
| @@ -188,17 +188,17 @@ class StrobeLightEffect : public LightEffect { | ||||
|     this->last_switch_ = now; | ||||
|   } | ||||
|  | ||||
|   void set_colors(const std::initializer_list<StrobeLightEffectColor> &colors) { this->colors_ = colors; } | ||||
|   void set_colors(const std::vector<StrobeLightEffectColor> &colors) { this->colors_ = colors; } | ||||
|  | ||||
|  protected: | ||||
|   FixedVector<StrobeLightEffectColor> colors_; | ||||
|   std::vector<StrobeLightEffectColor> colors_; | ||||
|   uint32_t last_switch_{0}; | ||||
|   size_t at_color_{0}; | ||||
| }; | ||||
|  | ||||
| class FlickerLightEffect : public LightEffect { | ||||
|  public: | ||||
|   explicit FlickerLightEffect(const char *name) : LightEffect(name) {} | ||||
|   explicit FlickerLightEffect(const std::string &name) : LightEffect(name) {} | ||||
|  | ||||
|   void apply() override { | ||||
|     LightColorValues remote = this->state_->remote_values; | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <cstdint> | ||||
| #include "esphome/core/finite_set_mask.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace light { | ||||
| @@ -108,9 +107,13 @@ constexpr ColorModeHelper operator|(ColorModeHelper lhs, ColorMode rhs) { | ||||
| // Type alias for raw color mode bitmask values | ||||
| using color_mode_bitmask_t = uint16_t; | ||||
|  | ||||
| // Lookup table for ColorMode bit mapping | ||||
| // This array defines the canonical order of color modes (bit 0-9) | ||||
| constexpr ColorMode COLOR_MODE_LOOKUP[] = { | ||||
| // Constants for ColorMode count and bit range | ||||
| static constexpr int COLOR_MODE_COUNT = 10;                             // UNKNOWN through RGB_COLD_WARM_WHITE | ||||
| static constexpr int MAX_BIT_INDEX = sizeof(color_mode_bitmask_t) * 8;  // Number of bits in bitmask type | ||||
|  | ||||
| // Compile-time array of all ColorMode values in declaration order | ||||
| // Bit positions (0-9) map directly to enum declaration order | ||||
| static constexpr ColorMode COLOR_MODES[COLOR_MODE_COUNT] = { | ||||
|     ColorMode::UNKNOWN,                // bit 0 | ||||
|     ColorMode::ON_OFF,                 // bit 1 | ||||
|     ColorMode::BRIGHTNESS,             // bit 2 | ||||
| @@ -123,42 +126,33 @@ constexpr ColorMode COLOR_MODE_LOOKUP[] = { | ||||
|     ColorMode::RGB_COLD_WARM_WHITE,    // bit 9 | ||||
| }; | ||||
|  | ||||
| /// Bit mapping policy for ColorMode | ||||
| /// Uses lookup table for non-contiguous enum values | ||||
| struct ColorModeBitPolicy { | ||||
|   using mask_t = uint16_t;  // 10 bits requires uint16_t | ||||
|   static constexpr int MAX_BITS = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]); | ||||
|  | ||||
|   static constexpr unsigned to_bit(ColorMode mode) { | ||||
|     // Linear search through lookup table | ||||
|     // Compiler optimizes this to efficient code since array is constexpr | ||||
|     for (int i = 0; i < MAX_BITS; ++i) { | ||||
|       if (COLOR_MODE_LOOKUP[i] == mode) | ||||
|         return i; | ||||
|     } | ||||
|     return 0; | ||||
| /// Map ColorMode enum values to bit positions (0-9) | ||||
| /// Bit positions follow the enum declaration order | ||||
| static constexpr int mode_to_bit(ColorMode mode) { | ||||
|   // Linear search through COLOR_MODES array | ||||
|   // Compiler optimizes this to efficient code since array is constexpr | ||||
|   for (int i = 0; i < COLOR_MODE_COUNT; ++i) { | ||||
|     if (COLOR_MODES[i] == mode) | ||||
|       return i; | ||||
|   } | ||||
|   return 0; | ||||
| } | ||||
|  | ||||
|   static constexpr ColorMode from_bit(unsigned bit) { | ||||
|     return (bit < MAX_BITS) ? COLOR_MODE_LOOKUP[bit] : ColorMode::UNKNOWN; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // Type alias for ColorMode bitmask using policy-based design | ||||
| using ColorModeMask = FiniteSetMask<ColorMode, ColorModeBitPolicy>; | ||||
|  | ||||
| // Number of ColorCapability enum values | ||||
| constexpr int COLOR_CAPABILITY_COUNT = 6; | ||||
| /// Map bit positions (0-9) to ColorMode enum values | ||||
| /// Bit positions follow the enum declaration order | ||||
| static constexpr ColorMode bit_to_mode(int bit) { | ||||
|   // Direct lookup in COLOR_MODES array | ||||
|   return (bit >= 0 && bit < COLOR_MODE_COUNT) ? COLOR_MODES[bit] : ColorMode::UNKNOWN; | ||||
| } | ||||
|  | ||||
| /// Helper to compute capability bitmask at compile time | ||||
| constexpr uint16_t compute_capability_bitmask(ColorCapability capability) { | ||||
|   uint16_t mask = 0; | ||||
| static constexpr color_mode_bitmask_t compute_capability_bitmask(ColorCapability capability) { | ||||
|   color_mode_bitmask_t mask = 0; | ||||
|   uint8_t cap_bit = static_cast<uint8_t>(capability); | ||||
|  | ||||
|   // Check each ColorMode to see if it has this capability | ||||
|   constexpr int color_mode_count = sizeof(COLOR_MODE_LOOKUP) / sizeof(COLOR_MODE_LOOKUP[0]); | ||||
|   for (int bit = 0; bit < color_mode_count; ++bit) { | ||||
|     uint8_t mode_val = static_cast<uint8_t>(COLOR_MODE_LOOKUP[bit]); | ||||
|   for (int bit = 0; bit < COLOR_MODE_COUNT; ++bit) { | ||||
|     uint8_t mode_val = static_cast<uint8_t>(bit_to_mode(bit)); | ||||
|     if ((mode_val & cap_bit) != 0) { | ||||
|       mask |= (1 << bit); | ||||
|     } | ||||
| @@ -166,9 +160,12 @@ constexpr uint16_t compute_capability_bitmask(ColorCapability capability) { | ||||
|   return mask; | ||||
| } | ||||
|  | ||||
| // Number of ColorCapability enum values | ||||
| static constexpr int COLOR_CAPABILITY_COUNT = 6; | ||||
|  | ||||
| /// Compile-time lookup table mapping ColorCapability to bitmask | ||||
| /// This array is computed at compile time using constexpr | ||||
| constexpr uint16_t CAPABILITY_BITMASKS[] = { | ||||
| static constexpr color_mode_bitmask_t CAPABILITY_BITMASKS[] = { | ||||
|     compute_capability_bitmask(ColorCapability::ON_OFF),             // 1 << 0 | ||||
|     compute_capability_bitmask(ColorCapability::BRIGHTNESS),         // 1 << 1 | ||||
|     compute_capability_bitmask(ColorCapability::WHITE),              // 1 << 2 | ||||
| @@ -177,38 +174,130 @@ constexpr uint16_t CAPABILITY_BITMASKS[] = { | ||||
|     compute_capability_bitmask(ColorCapability::RGB),                // 1 << 5 | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * @brief Helper function to convert a power-of-2 ColorCapability value to an array index for CAPABILITY_BITMASKS | ||||
|  * lookup. | ||||
|  * | ||||
|  * This function maps ColorCapability values (1, 2, 4, 8, 16, 32) to array indices (0, 1, 2, 3, 4, 5). | ||||
|  * Used to index into the CAPABILITY_BITMASKS lookup table. | ||||
|  * | ||||
|  * @param capability A ColorCapability enum value (must be a power of 2). | ||||
|  * @return The corresponding array index (0-based). | ||||
|  */ | ||||
| inline int capability_to_index(ColorCapability capability) { | ||||
|   uint8_t cap_val = static_cast<uint8_t>(capability); | ||||
| #if defined(__GNUC__) || defined(__clang__) | ||||
|   // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n)) | ||||
|   return __builtin_ctz(cap_val); | ||||
| #else | ||||
|   // Fallback for compilers without __builtin_ctz | ||||
|   int index = 0; | ||||
|   while (cap_val > 1) { | ||||
|     cap_val >>= 1; | ||||
|     ++index; | ||||
|   } | ||||
|   return index; | ||||
| #endif | ||||
| } | ||||
| /// Bitmask for storing a set of ColorMode values efficiently. | ||||
| /// Replaces std::set<ColorMode> to eliminate red-black tree overhead (~586 bytes). | ||||
| class ColorModeMask { | ||||
|  public: | ||||
|   constexpr ColorModeMask() = default; | ||||
|  | ||||
| /// Check if any mode in the bitmask has a specific capability | ||||
| /// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB) | ||||
| inline bool has_capability(const ColorModeMask &mask, ColorCapability capability) { | ||||
|   // Lookup the pre-computed bitmask for this capability and check intersection with our mask | ||||
|   return (mask.get_mask() & CAPABILITY_BITMASKS[capability_to_index(capability)]) != 0; | ||||
| } | ||||
|   /// Support initializer list syntax: {ColorMode::RGB, ColorMode::WHITE} | ||||
|   constexpr ColorModeMask(std::initializer_list<ColorMode> modes) { | ||||
|     for (auto mode : modes) { | ||||
|       this->add(mode); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   constexpr void add(ColorMode mode) { this->mask_ |= (1 << mode_to_bit(mode)); } | ||||
|  | ||||
|   /// Add multiple modes at once using initializer list | ||||
|   constexpr void add(std::initializer_list<ColorMode> modes) { | ||||
|     for (auto mode : modes) { | ||||
|       this->add(mode); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   constexpr bool contains(ColorMode mode) const { return (this->mask_ & (1 << mode_to_bit(mode))) != 0; } | ||||
|  | ||||
|   constexpr size_t size() const { | ||||
|     // Count set bits using Brian Kernighan's algorithm | ||||
|     // More efficient for sparse bitmasks (typical case: 2-4 modes out of 10) | ||||
|     uint16_t n = this->mask_; | ||||
|     size_t count = 0; | ||||
|     while (n) { | ||||
|       n &= n - 1;  // Clear the least significant set bit | ||||
|       count++; | ||||
|     } | ||||
|     return count; | ||||
|   } | ||||
|  | ||||
|   constexpr bool empty() const { return this->mask_ == 0; } | ||||
|  | ||||
|   /// Iterator support for API encoding | ||||
|   class Iterator { | ||||
|    public: | ||||
|     using iterator_category = std::forward_iterator_tag; | ||||
|     using value_type = ColorMode; | ||||
|     using difference_type = std::ptrdiff_t; | ||||
|     using pointer = const ColorMode *; | ||||
|     using reference = ColorMode; | ||||
|  | ||||
|     constexpr Iterator(color_mode_bitmask_t mask, int bit) : mask_(mask), bit_(bit) { advance_to_next_set_bit_(); } | ||||
|  | ||||
|     constexpr ColorMode operator*() const { return bit_to_mode(bit_); } | ||||
|  | ||||
|     constexpr Iterator &operator++() { | ||||
|       ++bit_; | ||||
|       advance_to_next_set_bit_(); | ||||
|       return *this; | ||||
|     } | ||||
|  | ||||
|     constexpr bool operator==(const Iterator &other) const { return bit_ == other.bit_; } | ||||
|  | ||||
|     constexpr bool operator!=(const Iterator &other) const { return !(*this == other); } | ||||
|  | ||||
|    private: | ||||
|     constexpr void advance_to_next_set_bit_() { bit_ = ColorModeMask::find_next_set_bit(mask_, bit_); } | ||||
|  | ||||
|     color_mode_bitmask_t mask_; | ||||
|     int bit_; | ||||
|   }; | ||||
|  | ||||
|   constexpr Iterator begin() const { return Iterator(mask_, 0); } | ||||
|   constexpr Iterator end() const { return Iterator(mask_, MAX_BIT_INDEX); } | ||||
|  | ||||
|   /// Get the raw bitmask value for API encoding | ||||
|   constexpr color_mode_bitmask_t get_mask() const { return this->mask_; } | ||||
|  | ||||
|   /// Find the next set bit in a bitmask starting from a given position | ||||
|   /// Returns the bit position, or MAX_BIT_INDEX if no more bits are set | ||||
|   static constexpr int find_next_set_bit(color_mode_bitmask_t mask, int start_bit) { | ||||
|     int bit = start_bit; | ||||
|     while (bit < MAX_BIT_INDEX && !(mask & (1 << bit))) { | ||||
|       ++bit; | ||||
|     } | ||||
|     return bit; | ||||
|   } | ||||
|  | ||||
|   /// Find the first set bit in a bitmask and return the corresponding ColorMode | ||||
|   /// Used for optimizing compute_color_mode_() intersection logic | ||||
|   static constexpr ColorMode first_mode_from_mask(color_mode_bitmask_t mask) { | ||||
|     return bit_to_mode(find_next_set_bit(mask, 0)); | ||||
|   } | ||||
|  | ||||
|   /// Check if a ColorMode is present in a raw bitmask value | ||||
|   /// Useful for checking intersection results without creating a temporary ColorModeMask | ||||
|   static constexpr bool mask_contains(color_mode_bitmask_t mask, ColorMode mode) { | ||||
|     return (mask & (1 << mode_to_bit(mode))) != 0; | ||||
|   } | ||||
|  | ||||
|   /// Check if any mode in the bitmask has a specific capability | ||||
|   /// Used for checking if a light supports a capability (e.g., BRIGHTNESS, RGB) | ||||
|   bool has_capability(ColorCapability capability) const { | ||||
|     // Lookup the pre-computed bitmask for this capability and check intersection with our mask | ||||
|     // ColorCapability values: 1, 2, 4, 8, 16, 32 -> array indices: 0, 1, 2, 3, 4, 5 | ||||
|     // We need to convert the power-of-2 value to an index | ||||
|     uint8_t cap_val = static_cast<uint8_t>(capability); | ||||
| #if defined(__GNUC__) || defined(__clang__) | ||||
|     // Use compiler intrinsic for efficient bit position lookup (O(1) vs O(log n)) | ||||
|     int index = __builtin_ctz(cap_val); | ||||
| #else | ||||
|     // Fallback for compilers without __builtin_ctz | ||||
|     int index = 0; | ||||
|     while (cap_val > 1) { | ||||
|       cap_val >>= 1; | ||||
|       ++index; | ||||
|     } | ||||
| #endif | ||||
|     return (this->mask_ & CAPABILITY_BITMASKS[index]) != 0; | ||||
|   } | ||||
|  | ||||
|  private: | ||||
|   // Using uint16_t instead of uint32_t for more efficient iteration (fewer bits to scan). | ||||
|   // Currently only 10 ColorMode values exist, so 16 bits is sufficient. | ||||
|   // Can be changed to uint32_t if more than 16 color modes are needed in the future. | ||||
|   // Note: Due to struct padding, uint16_t and uint32_t result in same LightTraits size (12 bytes). | ||||
|   color_mode_bitmask_t mask_{0}; | ||||
| }; | ||||
|  | ||||
| }  // namespace light | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -17,19 +17,19 @@ class ESPColorCorrection { | ||||
|                  this->color_correct_blue(color.blue), this->color_correct_white(color.white)); | ||||
|   } | ||||
|   inline uint8_t color_correct_red(uint8_t red) const ESPHOME_ALWAYS_INLINE { | ||||
|     uint8_t res = esp_scale8_twice(red, this->max_brightness_.red, this->local_brightness_); | ||||
|     uint8_t res = esp_scale8(esp_scale8(red, this->max_brightness_.red), this->local_brightness_); | ||||
|     return this->gamma_table_[res]; | ||||
|   } | ||||
|   inline uint8_t color_correct_green(uint8_t green) const ESPHOME_ALWAYS_INLINE { | ||||
|     uint8_t res = esp_scale8_twice(green, this->max_brightness_.green, this->local_brightness_); | ||||
|     uint8_t res = esp_scale8(esp_scale8(green, this->max_brightness_.green), this->local_brightness_); | ||||
|     return this->gamma_table_[res]; | ||||
|   } | ||||
|   inline uint8_t color_correct_blue(uint8_t blue) const ESPHOME_ALWAYS_INLINE { | ||||
|     uint8_t res = esp_scale8_twice(blue, this->max_brightness_.blue, this->local_brightness_); | ||||
|     uint8_t res = esp_scale8(esp_scale8(blue, this->max_brightness_.blue), this->local_brightness_); | ||||
|     return this->gamma_table_[res]; | ||||
|   } | ||||
|   inline uint8_t color_correct_white(uint8_t white) const ESPHOME_ALWAYS_INLINE { | ||||
|     uint8_t res = esp_scale8_twice(white, this->max_brightness_.white, this->local_brightness_); | ||||
|     uint8_t res = esp_scale8(esp_scale8(white, this->max_brightness_.white), this->local_brightness_); | ||||
|     return this->gamma_table_[res]; | ||||
|   } | ||||
|   inline Color color_uncorrect(Color color) const ESPHOME_ALWAYS_INLINE { | ||||
|   | ||||
| @@ -156,7 +156,7 @@ void LightCall::perform() { | ||||
|     if (this->effect_ == 0u) { | ||||
|       effect_s = "None"; | ||||
|     } else { | ||||
|       effect_s = this->parent_->effects_[this->effect_ - 1]->get_name(); | ||||
|       effect_s = this->parent_->effects_[this->effect_ - 1]->get_name().c_str(); | ||||
|     } | ||||
|  | ||||
|     if (publish) { | ||||
| @@ -437,7 +437,7 @@ ColorMode LightCall::compute_color_mode_() { | ||||
|  | ||||
|   // Use the preferred suitable mode. | ||||
|   if (intersection != 0) { | ||||
|     ColorMode mode = ColorModeMask::first_value_from_mask(intersection); | ||||
|     ColorMode mode = ColorModeMask::first_mode_from_mask(intersection); | ||||
|     ESP_LOGI(TAG, "'%s': color mode not specified; using %s", this->parent_->get_name().c_str(), | ||||
|              LOG_STR_ARG(color_mode_to_human(mode))); | ||||
|     return mode; | ||||
| @@ -511,7 +511,7 @@ LightCall &LightCall::set_effect(const std::string &effect) { | ||||
|   for (uint32_t i = 0; i < this->parent_->effects_.size(); i++) { | ||||
|     LightEffect *e = this->parent_->effects_[i]; | ||||
|  | ||||
|     if (strcasecmp(effect.c_str(), e->get_name()) == 0) { | ||||
|     if (strcasecmp(effect.c_str(), e->get_name().c_str()) == 0) { | ||||
|       this->set_effect(i + 1); | ||||
|       found = true; | ||||
|       break; | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <utility> | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
|  | ||||
| namespace esphome { | ||||
| @@ -9,7 +11,7 @@ class LightState; | ||||
|  | ||||
| class LightEffect { | ||||
|  public: | ||||
|   explicit LightEffect(const char *name) : name_(name) {} | ||||
|   explicit LightEffect(std::string name) : name_(std::move(name)) {} | ||||
|  | ||||
|   /// Initialize this LightEffect. Will be called once after creation. | ||||
|   virtual void start() {} | ||||
| @@ -22,11 +24,7 @@ class LightEffect { | ||||
|   /// Apply this effect. Use the provided state for starting transitions, ... | ||||
|   virtual void apply() = 0; | ||||
|  | ||||
|   /** | ||||
|    * Returns the name of this effect. | ||||
|    * The returned pointer is valid for the lifetime of the program and must not be freed. | ||||
|    */ | ||||
|   const char *get_name() const { return this->name_; } | ||||
|   const std::string &get_name() { return this->name_; } | ||||
|  | ||||
|   /// Internal method called by the LightState when this light effect is registered in it. | ||||
|   virtual void init() {} | ||||
| @@ -49,7 +47,7 @@ class LightEffect { | ||||
|  | ||||
|  protected: | ||||
|   LightState *state_{nullptr}; | ||||
|   const char *name_; | ||||
|   std::string name_; | ||||
|  | ||||
|   /// Internal method to find this effect's index in the parent light's effect list. | ||||
|   uint32_t get_index_in_parent_() const; | ||||
|   | ||||
| @@ -178,9 +178,12 @@ void LightState::set_restore_mode(LightRestoreMode restore_mode) { this->restore | ||||
| void LightState::set_initial_state(const LightStateRTCState &initial_state) { this->initial_state_ = initial_state; } | ||||
| bool LightState::supports_effects() { return !this->effects_.empty(); } | ||||
| const FixedVector<LightEffect *> &LightState::get_effects() const { return this->effects_; } | ||||
| void LightState::add_effects(const std::initializer_list<LightEffect *> &effects) { | ||||
| void LightState::add_effects(const std::vector<LightEffect *> &effects) { | ||||
|   // Called once from Python codegen during setup with all effects from YAML config | ||||
|   this->effects_ = effects; | ||||
|   this->effects_.init(effects.size()); | ||||
|   for (auto *effect : effects) { | ||||
|     this->effects_.push_back(effect); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void LightState::current_values_as_binary(bool *binary) { this->current_values.as_binary(binary); } | ||||
|   | ||||
| @@ -163,7 +163,7 @@ class LightState : public EntityBase, public Component { | ||||
|   const FixedVector<LightEffect *> &get_effects() const; | ||||
|  | ||||
|   /// Add effects for this light state. | ||||
|   void add_effects(const std::initializer_list<LightEffect *> &effects); | ||||
|   void add_effects(const std::vector<LightEffect *> &effects); | ||||
|  | ||||
|   /// Get the total number of effects available for this light. | ||||
|   size_t get_effect_count() const { return this->effects_.size(); } | ||||
| @@ -177,7 +177,7 @@ class LightState : public EntityBase, public Component { | ||||
|       return 0; | ||||
|     } | ||||
|     for (size_t i = 0; i < this->effects_.size(); i++) { | ||||
|       if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name()) == 0) { | ||||
|       if (strcasecmp(effect_name.c_str(), this->effects_[i]->get_name().c_str()) == 0) { | ||||
|         return i + 1;  // Effects are 1-indexed in active_effect_index_ | ||||
|       } | ||||
|     } | ||||
|   | ||||
| @@ -26,9 +26,9 @@ class LightTraits { | ||||
|     this->supported_color_modes_ = ColorModeMask(modes); | ||||
|   } | ||||
|  | ||||
|   bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.count(color_mode) > 0; } | ||||
|   bool supports_color_mode(ColorMode color_mode) const { return this->supported_color_modes_.contains(color_mode); } | ||||
|   bool supports_color_capability(ColorCapability color_capability) const { | ||||
|     return has_capability(this->supported_color_modes_, color_capability); | ||||
|     return this->supported_color_modes_.has_capability(color_capability); | ||||
|   } | ||||
|  | ||||
|   float get_min_mireds() const { return this->min_mireds_; } | ||||
|   | ||||
| @@ -300,11 +300,11 @@ void LvSelectable::set_selected_text(const std::string &text, lv_anim_enable_t a | ||||
|   } | ||||
| } | ||||
|  | ||||
| void LvSelectable::set_options(std::vector<std::string> options) { | ||||
| void LvSelectable::set_options(std::initializer_list<std::string> options) { | ||||
|   auto index = this->get_selected_index(); | ||||
|   if (index >= options.size()) | ||||
|     index = options.size() - 1; | ||||
|   this->options_ = std::move(options); | ||||
|   this->options_ = options; | ||||
|   this->set_option_string(join_string(this->options_).c_str()); | ||||
|   lv_event_send(this->obj, LV_EVENT_REFRESH, nullptr); | ||||
|   this->set_selected_index(index, LV_ANIM_OFF); | ||||
|   | ||||
| @@ -358,12 +358,12 @@ class LvSelectable : public LvCompound { | ||||
|   virtual void set_selected_index(size_t index, lv_anim_enable_t anim) = 0; | ||||
|   void set_selected_text(const std::string &text, lv_anim_enable_t anim); | ||||
|   std::string get_selected_text(); | ||||
|   std::vector<std::string> get_options() { return this->options_; } | ||||
|   void set_options(std::vector<std::string> options); | ||||
|   const FixedVector<std::string> &get_options() { return this->options_; } | ||||
|   void set_options(std::initializer_list<std::string> options); | ||||
|  | ||||
|  protected: | ||||
|   virtual void set_option_string(const char *options) = 0; | ||||
|   std::vector<std::string> options_{}; | ||||
|   FixedVector<std::string> options_{}; | ||||
| }; | ||||
|  | ||||
| #ifdef USE_LVGL_DROPDOWN | ||||
|   | ||||
| @@ -53,7 +53,10 @@ class LVGLSelect : public select::Select, public Component { | ||||
|     this->widget_->set_selected_text(value, this->anim_); | ||||
|     this->publish(); | ||||
|   } | ||||
|   void set_options_() { this->traits.set_options(this->widget_->get_options()); } | ||||
|   void set_options_() { | ||||
|     // Copy options from lvgl widget to select traits | ||||
|     this->traits.copy_options(this->widget_->get_options()); | ||||
|   } | ||||
|  | ||||
|   LvSelectable *widget_; | ||||
|   lv_anim_enable_t anim_; | ||||
|   | ||||
| @@ -99,11 +99,7 @@ const std::string &get_use_address() { | ||||
|   return wifi::global_wifi_component->get_use_address(); | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_OPENTHREAD | ||||
|   return openthread::global_openthread_component->get_use_address(); | ||||
| #endif | ||||
|  | ||||
| #if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) && !defined(USE_OPENTHREAD) | ||||
| #if !defined(USE_ETHERNET) && !defined(USE_MODEM) && !defined(USE_WIFI) | ||||
|   // Fallback when no network component is defined (e.g., host platform) | ||||
|   static const std::string empty; | ||||
|   return empty; | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| from __future__ import annotations | ||||
|  | ||||
| import asyncio | ||||
| import logging | ||||
| from pathlib import Path | ||||
|  | ||||
| @@ -278,19 +277,3 @@ def upload_program(config: ConfigType, args, host: str) -> bool: | ||||
|         raise EsphomeError(f"Upload failed with result: {result}") | ||||
|  | ||||
|     return handled | ||||
|  | ||||
|  | ||||
| def show_logs(config: ConfigType, args, devices: list[str]) -> bool: | ||||
|     address = devices[0] | ||||
|     from .ble_logger import is_mac_address, logger_connect, logger_scan | ||||
|  | ||||
|     if devices[0] == "BLE": | ||||
|         ble_device = asyncio.run(logger_scan(CORE.config["esphome"]["name"])) | ||||
|         if ble_device: | ||||
|             address = ble_device.address | ||||
|         else: | ||||
|             return True | ||||
|     if is_mac_address(address): | ||||
|         asyncio.run(logger_connect(address)) | ||||
|         return True | ||||
|     return False | ||||
|   | ||||
| @@ -1,60 +0,0 @@ | ||||
| import asyncio | ||||
| import logging | ||||
| import re | ||||
| from typing import Final | ||||
|  | ||||
| from bleak import BleakClient, BleakScanner, BLEDevice | ||||
| from bleak.exc import ( | ||||
|     BleakCharacteristicNotFoundError, | ||||
|     BleakDBusError, | ||||
|     BleakDeviceNotFoundError, | ||||
| ) | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
|  | ||||
| NUS_SERVICE_UUID = "6E400001-B5A3-F393-E0A9-E50E24DCCA9E" | ||||
| NUS_TX_CHAR_UUID = "6E400003-B5A3-F393-E0A9-E50E24DCCA9E" | ||||
|  | ||||
| MAC_ADDRESS_PATTERN: Final = re.compile( | ||||
|     r"([0-9A-F]{2}[:]){5}[0-9A-F]{2}$", flags=re.IGNORECASE | ||||
| ) | ||||
|  | ||||
|  | ||||
| def is_mac_address(value: str) -> bool: | ||||
|     return MAC_ADDRESS_PATTERN.match(value) | ||||
|  | ||||
|  | ||||
| async def logger_scan(name: str) -> BLEDevice | None: | ||||
|     _LOGGER.info("Scanning bluetooth for %s...", name) | ||||
|     device = await BleakScanner.find_device_by_name(name) | ||||
|     if not device: | ||||
|         _LOGGER.error("%s Bluetooth LE device was not found!", name) | ||||
|     return device | ||||
|  | ||||
|  | ||||
| async def logger_connect(host: str) -> int | None: | ||||
|     disconnected_event = asyncio.Event() | ||||
|  | ||||
|     def handle_disconnect(client): | ||||
|         disconnected_event.set() | ||||
|  | ||||
|     def handle_rx(_, data: bytearray): | ||||
|         print(data.decode("utf-8"), end="") | ||||
|  | ||||
|     _LOGGER.info("Connecting %s...", host) | ||||
|     try: | ||||
|         async with BleakClient(host, disconnected_callback=handle_disconnect) as client: | ||||
|             _LOGGER.info("Connected %s...", host) | ||||
|             try: | ||||
|                 await client.start_notify(NUS_TX_CHAR_UUID, handle_rx) | ||||
|             except BleakDBusError as e: | ||||
|                 _LOGGER.error("Bluetooth LE logger: %s", e) | ||||
|                 disconnected_event.set() | ||||
|             await disconnected_event.wait() | ||||
|     except BleakDeviceNotFoundError: | ||||
|         _LOGGER.error("Device %s not found", host) | ||||
|         return 1 | ||||
|     except BleakCharacteristicNotFoundError: | ||||
|         _LOGGER.error("Device %s has no NUS characteristic", host) | ||||
|         return 1 | ||||
| @@ -74,9 +74,6 @@ async def nrf52_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(num)) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
|   | ||||
| @@ -4,14 +4,11 @@ from esphome.components.esp32 import ( | ||||
|     VARIANT_ESP32H2, | ||||
|     add_idf_sdkconfig_option, | ||||
|     only_on_variant, | ||||
|     require_vfs_select, | ||||
| ) | ||||
| from esphome.components.mdns import MDNSComponent, enable_mdns_storage | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID, CONF_USE_ADDRESS | ||||
| from esphome.core import CORE | ||||
| from esphome.const import CONF_CHANNEL, CONF_ENABLE_IPV6, CONF_ID | ||||
| import esphome.final_validate as fv | ||||
| from esphome.types import ConfigType | ||||
|  | ||||
| from .const import ( | ||||
|     CONF_DEVICE_TYPE, | ||||
| @@ -109,20 +106,6 @@ _CONNECTION_SCHEMA = cv.Schema( | ||||
|     } | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _validate(config: ConfigType) -> ConfigType: | ||||
|     if CONF_USE_ADDRESS not in config: | ||||
|         config[CONF_USE_ADDRESS] = f"{CORE.name}.local" | ||||
|     return config | ||||
|  | ||||
|  | ||||
| def _require_vfs_select(config): | ||||
|     """Register VFS select requirement during config validation.""" | ||||
|     # OpenThread uses esp_vfs_eventfd which requires VFS select support | ||||
|     require_vfs_select() | ||||
|     return config | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.Schema( | ||||
|         { | ||||
| @@ -134,14 +117,11 @@ CONFIG_SCHEMA = cv.All( | ||||
|             ), | ||||
|             cv.Optional(CONF_FORCE_DATASET): cv.boolean, | ||||
|             cv.Optional(CONF_TLV): cv.string_strict, | ||||
|             cv.Optional(CONF_USE_ADDRESS): cv.string_strict, | ||||
|         } | ||||
|     ).extend(_CONNECTION_SCHEMA), | ||||
|     cv.has_exactly_one_key(CONF_NETWORK_KEY, CONF_TLV), | ||||
|     cv.only_with_esp_idf, | ||||
|     only_on_variant(supported=[VARIANT_ESP32C6, VARIANT_ESP32H2]), | ||||
|     _validate, | ||||
|     _require_vfs_select, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -165,7 +145,6 @@ async def to_code(config): | ||||
|     enable_mdns_storage() | ||||
|  | ||||
|     ot = cg.new_Pvariable(config[CONF_ID]) | ||||
|     cg.add(ot.set_use_address(config[CONF_USE_ADDRESS])) | ||||
|     await cg.register_component(ot, config) | ||||
|  | ||||
|     srp = cg.new_Pvariable(config[CONF_SRP_ID]) | ||||
|   | ||||
| @@ -252,12 +252,6 @@ void OpenThreadComponent::on_factory_reset(std::function<void()> callback) { | ||||
|   ESP_LOGD(TAG, "Waiting on Confirmation Removal SRP Host and Services"); | ||||
| } | ||||
|  | ||||
| // set_use_address() is guaranteed to be called during component setup by Python code generation, | ||||
| // so use_address_ will always be valid when get_use_address() is called - no fallback needed. | ||||
| const std::string &OpenThreadComponent::get_use_address() const { return this->use_address_; } | ||||
|  | ||||
| void OpenThreadComponent::set_use_address(const std::string &use_address) { this->use_address_ = use_address; } | ||||
|  | ||||
| }  // namespace openthread | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -33,15 +33,11 @@ class OpenThreadComponent : public Component { | ||||
|   void on_factory_reset(std::function<void()> callback); | ||||
|   void defer_factory_reset_external_callback(); | ||||
|  | ||||
|   const std::string &get_use_address() const; | ||||
|   void set_use_address(const std::string &use_address); | ||||
|  | ||||
|  protected: | ||||
|   std::optional<otIp6Address> get_omr_address_(InstanceLock &lock); | ||||
|   bool teardown_started_{false}; | ||||
|   bool teardown_complete_{false}; | ||||
|   std::function<void()> factory_reset_external_callback_; | ||||
|   std::string use_address_; | ||||
| }; | ||||
|  | ||||
| extern OpenThreadComponent *global_openthread_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   | ||||
| @@ -38,6 +38,7 @@ void Pipsolar::loop() { | ||||
|   } | ||||
|   if (this->state_ == STATE_COMMAND_COMPLETE) { | ||||
|     if (this->check_incoming_length_(4)) { | ||||
|       ESP_LOGD(TAG, "response length for command OK"); | ||||
|       if (this->check_incoming_crc_()) { | ||||
|         // crc ok | ||||
|         if (this->read_buffer_[1] == 'A' && this->read_buffer_[2] == 'C' && this->read_buffer_[3] == 'K') { | ||||
| @@ -48,15 +49,15 @@ void Pipsolar::loop() { | ||||
|         this->command_queue_[this->command_queue_position_] = std::string(""); | ||||
|         this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; | ||||
|         this->state_ = STATE_IDLE; | ||||
|  | ||||
|       } else { | ||||
|         // crc failed | ||||
|         // no log message necessary, check_incoming_crc_() logs | ||||
|         this->command_queue_[this->command_queue_position_] = std::string(""); | ||||
|         this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; | ||||
|         this->state_ = STATE_IDLE; | ||||
|       } | ||||
|     } else { | ||||
|       ESP_LOGD(TAG, "command %s response length not OK: with length %zu", | ||||
|       ESP_LOGD(TAG, "response length for command %s not OK: with length %zu", | ||||
|                this->command_queue_[this->command_queue_position_].c_str(), this->read_pos_); | ||||
|       this->command_queue_[this->command_queue_position_] = std::string(""); | ||||
|       this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; | ||||
| @@ -65,10 +66,46 @@ void Pipsolar::loop() { | ||||
|   } | ||||
|  | ||||
|   if (this->state_ == STATE_POLL_CHECKED) { | ||||
|     ESP_LOGD(TAG, "poll %s decode", this->enabled_polling_commands_[this->last_polling_command_].command); | ||||
|     this->handle_poll_response_(this->enabled_polling_commands_[this->last_polling_command_].identifier, | ||||
|                                 (const char *) this->read_buffer_); | ||||
|     this->state_ = STATE_IDLE; | ||||
|     switch (this->enabled_polling_commands_[this->last_polling_command_].identifier) { | ||||
|       case POLLING_QPIRI: | ||||
|         ESP_LOGD(TAG, "Decode QPIRI"); | ||||
|         handle_qpiri_((const char *) this->read_buffer_); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|       case POLLING_QPIGS: | ||||
|         ESP_LOGD(TAG, "Decode QPIGS"); | ||||
|         handle_qpigs_((const char *) this->read_buffer_); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|       case POLLING_QMOD: | ||||
|         ESP_LOGD(TAG, "Decode QMOD"); | ||||
|         handle_qmod_((const char *) this->read_buffer_); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|       case POLLING_QFLAG: | ||||
|         ESP_LOGD(TAG, "Decode QFLAG"); | ||||
|         handle_qflag_((const char *) this->read_buffer_); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|       case POLLING_QPIWS: | ||||
|         ESP_LOGD(TAG, "Decode QPIWS"); | ||||
|         handle_qpiws_((const char *) this->read_buffer_); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|       case POLLING_QT: | ||||
|         ESP_LOGD(TAG, "Decode QT"); | ||||
|         handle_qt_((const char *) this->read_buffer_); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|       case POLLING_QMN: | ||||
|         ESP_LOGD(TAG, "Decode QMN"); | ||||
|         handle_qmn_((const char *) this->read_buffer_); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|       default: | ||||
|         this->state_ = STATE_IDLE; | ||||
|         break; | ||||
|     } | ||||
|     return; | ||||
|   } | ||||
|  | ||||
| @@ -76,8 +113,6 @@ void Pipsolar::loop() { | ||||
|     if (this->check_incoming_crc_()) { | ||||
|       if (this->read_buffer_[0] == '(' && this->read_buffer_[1] == 'N' && this->read_buffer_[2] == 'A' && | ||||
|           this->read_buffer_[3] == 'K') { | ||||
|         ESP_LOGD(TAG, "poll %s NACK", this->enabled_polling_commands_[this->last_polling_command_].command); | ||||
|         this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier); | ||||
|         this->state_ = STATE_IDLE; | ||||
|         return; | ||||
|       } | ||||
| @@ -86,9 +121,6 @@ void Pipsolar::loop() { | ||||
|       this->state_ = STATE_POLL_CHECKED; | ||||
|       return; | ||||
|     } else { | ||||
|       // crc failed | ||||
|       // no log message necessary, check_incoming_crc_() logs | ||||
|       this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier); | ||||
|       this->state_ = STATE_IDLE; | ||||
|     } | ||||
|   } | ||||
| @@ -126,19 +158,21 @@ void Pipsolar::loop() { | ||||
|       // command timeout | ||||
|       const char *command = this->command_queue_[this->command_queue_position_].c_str(); | ||||
|       this->command_start_millis_ = millis(); | ||||
|       ESP_LOGD(TAG, "command %s timeout", command); | ||||
|       ESP_LOGD(TAG, "timeout command from queue: %s", command); | ||||
|       this->command_queue_[this->command_queue_position_] = std::string(""); | ||||
|       this->command_queue_position_ = (command_queue_position_ + 1) % COMMAND_QUEUE_LENGTH; | ||||
|       this->state_ = STATE_IDLE; | ||||
|       return; | ||||
|     } else { | ||||
|     } | ||||
|   } | ||||
|   if (this->state_ == STATE_POLL) { | ||||
|     if (millis() - this->command_start_millis_ > esphome::pipsolar::Pipsolar::COMMAND_TIMEOUT) { | ||||
|       // command timeout | ||||
|       ESP_LOGD(TAG, "poll %s timeout", this->enabled_polling_commands_[this->last_polling_command_].command); | ||||
|       this->handle_poll_error_(this->enabled_polling_commands_[this->last_polling_command_].identifier); | ||||
|       ESP_LOGD(TAG, "timeout command to poll: %s", | ||||
|                this->enabled_polling_commands_[this->last_polling_command_].command); | ||||
|       this->state_ = STATE_IDLE; | ||||
|     } else { | ||||
|     } | ||||
|   } | ||||
| } | ||||
| @@ -153,6 +187,7 @@ uint8_t Pipsolar::check_incoming_length_(uint8_t length) { | ||||
| uint8_t Pipsolar::check_incoming_crc_() { | ||||
|   uint16_t crc16; | ||||
|   crc16 = this->pipsolar_crc_(read_buffer_, read_pos_ - 3); | ||||
|   ESP_LOGD(TAG, "checking crc on incoming message"); | ||||
|   if (((uint8_t) ((crc16) >> 8)) == read_buffer_[read_pos_ - 3] && | ||||
|       ((uint8_t) ((crc16) &0xff)) == read_buffer_[read_pos_ - 2]) { | ||||
|     ESP_LOGD(TAG, "CRC OK"); | ||||
| @@ -218,7 +253,7 @@ bool Pipsolar::send_next_poll_() { | ||||
|     this->write(((uint8_t) ((crc16) &0xff)));  // lowbyte | ||||
|     // end Byte | ||||
|     this->write(0x0D); | ||||
|     ESP_LOGD(TAG, "Sending polling command: %s with length %d", | ||||
|     ESP_LOGD(TAG, "Sending polling command : %s with length %d", | ||||
|              this->enabled_polling_commands_[this->last_polling_command_].command, | ||||
|              this->enabled_polling_commands_[this->last_polling_command_].length); | ||||
|     return true; | ||||
| @@ -239,38 +274,6 @@ void Pipsolar::queue_command(const std::string &command) { | ||||
|   ESP_LOGD(TAG, "Command queue full dropping command: %s", command.c_str()); | ||||
| } | ||||
|  | ||||
| void Pipsolar::handle_poll_response_(ENUMPollingCommand polling_command, const char *message) { | ||||
|   switch (polling_command) { | ||||
|     case POLLING_QPIRI: | ||||
|       handle_qpiri_(message); | ||||
|       break; | ||||
|     case POLLING_QPIGS: | ||||
|       handle_qpigs_(message); | ||||
|       break; | ||||
|     case POLLING_QMOD: | ||||
|       handle_qmod_(message); | ||||
|       break; | ||||
|     case POLLING_QFLAG: | ||||
|       handle_qflag_(message); | ||||
|       break; | ||||
|     case POLLING_QPIWS: | ||||
|       handle_qpiws_(message); | ||||
|       break; | ||||
|     case POLLING_QT: | ||||
|       handle_qt_(message); | ||||
|       break; | ||||
|     case POLLING_QMN: | ||||
|       handle_qmn_(message); | ||||
|       break; | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
| } | ||||
| void Pipsolar::handle_poll_error_(ENUMPollingCommand polling_command) { | ||||
|   // handlers are designed in a way that an empty message sets all sensors to unknown | ||||
|   this->handle_poll_response_(polling_command, ""); | ||||
| } | ||||
|  | ||||
| void Pipsolar::handle_qpiri_(const char *message) { | ||||
|   if (this->last_qpiri_) { | ||||
|     this->last_qpiri_->publish_state(message); | ||||
|   | ||||
| @@ -204,9 +204,6 @@ class Pipsolar : public uart::UARTDevice, public PollingComponent { | ||||
|   bool send_next_command_(); | ||||
|   bool send_next_poll_(); | ||||
|  | ||||
|   void handle_poll_response_(ENUMPollingCommand polling_command, const char *message); | ||||
|   void handle_poll_error_(ENUMPollingCommand polling_command); | ||||
|   // these handlers are designed in a way that an empty message sets all sensors to unknown | ||||
|   void handle_qpiri_(const char *message); | ||||
|   void handle_qpigs_(const char *message); | ||||
|   void handle_qmod_(const char *message); | ||||
|   | ||||
| @@ -4,18 +4,11 @@ import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_BATTERY_VOLTAGE, | ||||
|     CONF_BUS_VOLTAGE, | ||||
|     DEVICE_CLASS_APPARENT_POWER, | ||||
|     DEVICE_CLASS_BATTERY, | ||||
|     DEVICE_CLASS_CURRENT, | ||||
|     DEVICE_CLASS_FREQUENCY, | ||||
|     DEVICE_CLASS_POWER, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     DEVICE_CLASS_VOLTAGE, | ||||
|     ICON_BATTERY, | ||||
|     ICON_CURRENT_AC, | ||||
|     ICON_FLASH, | ||||
|     ICON_GAUGE, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|     UNIT_AMPERE, | ||||
|     UNIT_CELSIUS, | ||||
|     UNIT_HERTZ, | ||||
| @@ -29,10 +22,6 @@ from .. import CONF_PIPSOLAR_ID, PIPSOLAR_COMPONENT_SCHEMA | ||||
|  | ||||
| DEPENDENCIES = ["uart"] | ||||
|  | ||||
| ICON_SOLAR_POWER = "mdi:solar-power" | ||||
| ICON_SOLAR_PANEL = "mdi:solar-panel" | ||||
| ICON_CURRENT_DC = "mdi:current-dc" | ||||
|  | ||||
| # QPIRI sensors | ||||
| CONF_GRID_RATING_VOLTAGE = "grid_rating_voltage" | ||||
| CONF_GRID_RATING_CURRENT = "grid_rating_current" | ||||
| @@ -86,19 +75,16 @@ TYPES = { | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_GRID_RATING_CURRENT: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_AMPERE, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_CURRENT, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_RATING_VOLTAGE: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_RATING_FREQUENCY: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_HERTZ, | ||||
| @@ -112,12 +98,11 @@ TYPES = { | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_RATING_APPARENT_POWER: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT_AMPS, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_APPARENT_POWER, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_RATING_ACTIVE_POWER: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_WATT, | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_POWER, | ||||
|     ), | ||||
|     CONF_BATTERY_RATING_VOLTAGE: sensor.sensor_schema( | ||||
| @@ -146,151 +131,124 @@ TYPES = { | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|     ), | ||||
|     CONF_BATTERY_TYPE: sensor.sensor_schema( | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_CURRENT_MAX_AC_CHARGING_CURRENT: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_AMPERE, | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_CURRENT, | ||||
|     ), | ||||
|     CONF_CURRENT_MAX_CHARGING_CURRENT: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_AMPERE, | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_CURRENT, | ||||
|     ), | ||||
|     CONF_INPUT_VOLTAGE_RANGE: sensor.sensor_schema( | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_OUTPUT_SOURCE_PRIORITY: sensor.sensor_schema( | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_CHARGER_SOURCE_PRIORITY: sensor.sensor_schema( | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_PARALLEL_MAX_NUM: sensor.sensor_schema( | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_MACHINE_TYPE: sensor.sensor_schema( | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_TOPOLOGY: sensor.sensor_schema( | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_OUTPUT_MODE: sensor.sensor_schema( | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_BATTERY_REDISCHARGE_VOLTAGE: sensor.sensor_schema( | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_PV_OK_CONDITION_FOR_PARALLEL: sensor.sensor_schema( | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_PV_POWER_BALANCE: sensor.sensor_schema( | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_GRID_VOLTAGE: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_GRID_FREQUENCY: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_HERTZ, | ||||
|         icon=ICON_CURRENT_AC, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_FREQUENCY, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_VOLTAGE: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_FREQUENCY: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_HERTZ, | ||||
|         icon=ICON_CURRENT_AC, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_FREQUENCY, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_APPARENT_POWER: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT_AMPS, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_APPARENT_POWER, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_AC_OUTPUT_ACTIVE_POWER: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_WATT, | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_POWER, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_OUTPUT_LOAD_PERCENT: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_PERCENT, | ||||
|         icon=ICON_GAUGE, | ||||
|         accuracy_decimals=0, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_BUS_VOLTAGE: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         icon=ICON_FLASH, | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_BATTERY_VOLTAGE: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         icon=ICON_BATTERY, | ||||
|         accuracy_decimals=2, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_BATTERY_CHARGING_CURRENT: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_AMPERE, | ||||
|         icon=ICON_CURRENT_DC, | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_CURRENT, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_BATTERY_CAPACITY_PERCENT: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_PERCENT, | ||||
|         accuracy_decimals=0, | ||||
|         device_class=DEVICE_CLASS_BATTERY, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_INVERTER_HEAT_SINK_TEMPERATURE: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_CELSIUS, | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_TEMPERATURE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_PV_INPUT_CURRENT_FOR_BATTERY: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_AMPERE, | ||||
|         icon=ICON_SOLAR_PANEL, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_CURRENT, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_PV_INPUT_VOLTAGE: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         icon=ICON_SOLAR_PANEL, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_BATTERY_VOLTAGE_SCC: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
|         accuracy_decimals=2, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_BATTERY_DISCHARGE_CURRENT: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_AMPERE, | ||||
|         icon=ICON_CURRENT_DC, | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_CURRENT, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
|     CONF_BATTERY_VOLTAGE_OFFSET_FOR_FANS_ON: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_VOLT, | ||||
| @@ -298,14 +256,12 @@ TYPES = { | ||||
|         device_class=DEVICE_CLASS_VOLTAGE, | ||||
|     ), | ||||
|     CONF_EEPROM_VERSION: sensor.sensor_schema( | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|     ), | ||||
|     CONF_PV_CHARGING_POWER: sensor.sensor_schema( | ||||
|         unit_of_measurement=UNIT_WATT, | ||||
|         icon=ICON_SOLAR_POWER, | ||||
|         accuracy_decimals=0, | ||||
|         accuracy_decimals=1, | ||||
|         device_class=DEVICE_CLASS_POWER, | ||||
|         state_class=STATE_CLASS_MEASUREMENT, | ||||
|     ), | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -12,25 +12,6 @@ | ||||
| namespace esphome { | ||||
| namespace remote_transmitter { | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
| // IDF version 5.5.1 and above is required because of a bug in | ||||
| // the RMT encoder: https://github.com/espressif/esp-idf/issues/17244 | ||||
| typedef union {  // NOLINT(modernize-use-using) | ||||
|   struct { | ||||
|     uint16_t duration : 15; | ||||
|     uint16_t level : 1; | ||||
|   }; | ||||
|   uint16_t val; | ||||
| } rmt_symbol_half_t; | ||||
|  | ||||
| struct RemoteTransmitterComponentStore { | ||||
|   uint32_t times{0}; | ||||
|   uint32_t index{0}; | ||||
| }; | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
|                                    public Component | ||||
| #ifdef USE_ESP32 | ||||
| @@ -75,14 +56,9 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
| #ifdef USE_ESP32 | ||||
|   void configure_rmt_(); | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
|   RemoteTransmitterComponentStore store_{}; | ||||
|   std::vector<rmt_symbol_half_t> rmt_temp_; | ||||
| #else | ||||
|   std::vector<rmt_symbol_word_t> rmt_temp_; | ||||
| #endif | ||||
|   uint32_t current_carrier_frequency_{38000}; | ||||
|   bool initialized_{false}; | ||||
|   std::vector<rmt_symbol_word_t> rmt_temp_; | ||||
|   bool with_dma_{false}; | ||||
|   bool eot_level_{false}; | ||||
|   rmt_channel_handle_t channel_{NULL}; | ||||
|   | ||||
| @@ -10,46 +10,6 @@ namespace remote_transmitter { | ||||
|  | ||||
| static const char *const TAG = "remote_transmitter"; | ||||
|  | ||||
| // Maximum RMT symbol duration (15-bit field) | ||||
| static constexpr uint32_t RMT_SYMBOL_DURATION_MAX = 0x7FFF; | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
| static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size_t written, size_t free, | ||||
|                                              rmt_symbol_word_t *symbols, bool *done, void *arg) { | ||||
|   auto *store = static_cast<RemoteTransmitterComponentStore *>(arg); | ||||
|   const auto *encoded = static_cast<const rmt_symbol_half_t *>(data); | ||||
|   size_t length = size / sizeof(rmt_symbol_half_t); | ||||
|   size_t count = 0; | ||||
|  | ||||
|   // copy symbols | ||||
|   for (size_t i = 0; i < free; i++) { | ||||
|     uint16_t sym_0 = encoded[store->index++].val; | ||||
|     if (store->index >= length) { | ||||
|       store->index = 0; | ||||
|       store->times--; | ||||
|       if (store->times == 0) { | ||||
|         *done = true; | ||||
|         symbols[count++].val = sym_0; | ||||
|         return count; | ||||
|       } | ||||
|     } | ||||
|     uint16_t sym_1 = encoded[store->index++].val; | ||||
|     if (store->index >= length) { | ||||
|       store->index = 0; | ||||
|       store->times--; | ||||
|       if (store->times == 0) { | ||||
|         *done = true; | ||||
|         symbols[count++].val = sym_0 | (sym_1 << 16); | ||||
|         return count; | ||||
|       } | ||||
|     } | ||||
|     symbols[count++].val = sym_0 | (sym_1 << 16); | ||||
|   } | ||||
|   *done = false; | ||||
|   return count; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| void RemoteTransmitterComponent::setup() { | ||||
|   this->inverted_ = this->pin_->is_inverted(); | ||||
|   this->configure_rmt_(); | ||||
| @@ -74,17 +34,6 @@ void RemoteTransmitterComponent::dump_config() { | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::digital_write(bool value) { | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
|   rmt_symbol_half_t symbol = { | ||||
|       .duration = 1, | ||||
|       .level = value, | ||||
|   }; | ||||
|   rmt_transmit_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.flags.eot_level = value; | ||||
|   this->store_.times = 1; | ||||
|   this->store_.index = 0; | ||||
| #else | ||||
|   rmt_symbol_word_t symbol = { | ||||
|       .duration0 = 1, | ||||
|       .level0 = value, | ||||
| @@ -93,8 +42,8 @@ void RemoteTransmitterComponent::digital_write(bool value) { | ||||
|   }; | ||||
|   rmt_transmit_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.loop_count = 0; | ||||
|   config.flags.eot_level = value; | ||||
| #endif | ||||
|   esp_err_t error = rmt_transmit(this->channel_, this->encoder_, &symbol, sizeof(symbol), &config); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); | ||||
| @@ -141,20 +90,6 @@ void RemoteTransmitterComponent::configure_rmt_() { | ||||
|       gpio_pullup_dis(gpio_num_t(this->pin_->get_pin())); | ||||
|     } | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
|     rmt_simple_encoder_config_t encoder; | ||||
|     memset(&encoder, 0, sizeof(encoder)); | ||||
|     encoder.callback = encoder_callback; | ||||
|     encoder.arg = &this->store_; | ||||
|     encoder.min_chunk_size = 1; | ||||
|     error = rmt_new_simple_encoder(&encoder, &this->encoder_); | ||||
|     if (error != ESP_OK) { | ||||
|       this->error_code_ = error; | ||||
|       this->error_string_ = "in rmt_new_simple_encoder"; | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
| #else | ||||
|     rmt_copy_encoder_config_t encoder; | ||||
|     memset(&encoder, 0, sizeof(encoder)); | ||||
|     error = rmt_new_copy_encoder(&encoder, &this->encoder_); | ||||
| @@ -164,7 +99,6 @@ void RemoteTransmitterComponent::configure_rmt_() { | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
| #endif | ||||
|  | ||||
|     error = rmt_enable(this->channel_); | ||||
|     if (error != ESP_OK) { | ||||
| @@ -196,79 +130,6 @@ void RemoteTransmitterComponent::configure_rmt_() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   if (this->is_failed()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) { | ||||
|     this->current_carrier_frequency_ = this->temp_.get_carrier_frequency(); | ||||
|     this->configure_rmt_(); | ||||
|   } | ||||
|  | ||||
|   this->rmt_temp_.clear(); | ||||
|   this->rmt_temp_.reserve(this->temp_.get_data().size() + 1); | ||||
|  | ||||
|   // encode any delay at the start of the buffer to simplify the encoder callback | ||||
|   // this will be skipped the first time around | ||||
|   send_wait = this->from_microseconds_(static_cast<uint32_t>(send_wait)); | ||||
|   while (send_wait > 0) { | ||||
|     int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
|     this->rmt_temp_.push_back({ | ||||
|         .duration = static_cast<uint16_t>(duration), | ||||
|         .level = static_cast<uint16_t>(this->eot_level_), | ||||
|     }); | ||||
|     send_wait -= duration; | ||||
|   } | ||||
|  | ||||
|   // encode data | ||||
|   size_t offset = this->rmt_temp_.size(); | ||||
|   for (int32_t value : this->temp_.get_data()) { | ||||
|     bool level = value >= 0; | ||||
|     if (!level) { | ||||
|       value = -value; | ||||
|     } | ||||
|     value = this->from_microseconds_(static_cast<uint32_t>(value)); | ||||
|     while (value > 0) { | ||||
|       int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
|       this->rmt_temp_.push_back({ | ||||
|           .duration = static_cast<uint16_t>(duration), | ||||
|           .level = static_cast<uint16_t>(level ^ this->inverted_), | ||||
|       }); | ||||
|       value -= duration; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if ((this->rmt_temp_.data() == nullptr) || this->rmt_temp_.size() <= offset) { | ||||
|     ESP_LOGE(TAG, "Empty data"); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->transmit_trigger_->trigger(); | ||||
|  | ||||
|   rmt_transmit_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.flags.eot_level = this->eot_level_; | ||||
|   this->store_.times = send_times; | ||||
|   this->store_.index = offset; | ||||
|   esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), | ||||
|                                  this->rmt_temp_.size() * sizeof(rmt_symbol_half_t), &config); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_transmit failed: %s", esp_err_to_name(error)); | ||||
|     this->status_set_warning(); | ||||
|   } else { | ||||
|     this->status_clear_warning(); | ||||
|   } | ||||
|   error = rmt_tx_wait_all_done(this->channel_, -1); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); | ||||
|     this->status_set_warning(); | ||||
|   } | ||||
|  | ||||
|   this->complete_trigger_->trigger(); | ||||
| } | ||||
| #else | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   if (this->is_failed()) | ||||
|     return; | ||||
| @@ -290,7 +151,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|     val = this->from_microseconds_(static_cast<uint32_t>(val)); | ||||
|  | ||||
|     do { | ||||
|       int32_t item = std::min(val, int32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
|       int32_t item = std::min(val, int32_t(32767)); | ||||
|       val -= item; | ||||
|  | ||||
|       if (rmt_i % 2 == 0) { | ||||
| @@ -319,6 +180,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|   for (uint32_t i = 0; i < send_times; i++) { | ||||
|     rmt_transmit_config_t config; | ||||
|     memset(&config, 0, sizeof(config)); | ||||
|     config.loop_count = 0; | ||||
|     config.flags.eot_level = this->eot_level_; | ||||
|     esp_err_t error = rmt_transmit(this->channel_, this->encoder_, this->rmt_temp_.data(), | ||||
|                                    this->rmt_temp_.size() * sizeof(rmt_symbol_word_t), &config); | ||||
| @@ -338,7 +200,6 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|   } | ||||
|   this->complete_trigger_->trigger(); | ||||
| } | ||||
| #endif | ||||
|  | ||||
| }  // namespace remote_transmitter | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -29,8 +29,8 @@ class RP2040GPIOPin : public InternalGPIOPin { | ||||
|   void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override; | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   bool inverted_{}; | ||||
|   gpio::Flags flags_{}; | ||||
|   bool inverted_; | ||||
|   gpio::Flags flags_; | ||||
| }; | ||||
|  | ||||
| }  // namespace rp2040 | ||||
|   | ||||
| @@ -94,9 +94,6 @@ async def rp2040_pin_to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     num = config[CONF_NUMBER] | ||||
|     cg.add(var.set_pin(num)) | ||||
|     # Only set if true to avoid bloating setup() function | ||||
|     # (inverted bit in pin_flags_ bitfield is zero-initialized to false) | ||||
|     if config[CONF_INVERTED]: | ||||
|         cg.add(var.set_inverted(True)) | ||||
|     cg.add(var.set_inverted(config[CONF_INVERTED])) | ||||
|     cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE]))) | ||||
|     return var | ||||
|   | ||||
| @@ -3,9 +3,9 @@ | ||||
| namespace esphome { | ||||
| namespace select { | ||||
|  | ||||
| void SelectTraits::set_options(std::vector<std::string> options) { this->options_ = std::move(options); } | ||||
| void SelectTraits::set_options(std::initializer_list<std::string> options) { this->options_ = options; } | ||||
|  | ||||
| const std::vector<std::string> &SelectTraits::get_options() const { return this->options_; } | ||||
| const FixedVector<std::string> &SelectTraits::get_options() const { return this->options_; } | ||||
|  | ||||
| }  // namespace select | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -1,18 +1,21 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <vector> | ||||
| #include <string> | ||||
| #include <initializer_list> | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace select { | ||||
|  | ||||
| class SelectTraits { | ||||
|  public: | ||||
|   void set_options(std::vector<std::string> options); | ||||
|   const std::vector<std::string> &get_options() const; | ||||
|   void set_options(std::initializer_list<std::string> options); | ||||
|   const FixedVector<std::string> &get_options() const; | ||||
|   /// Copy options from another SelectTraits (for copy_select, lvgl) | ||||
|   void copy_options(const FixedVector<std::string> &other) { this->options_.copy_from(other); } | ||||
|  | ||||
|  protected: | ||||
|   std::vector<std::string> options_; | ||||
|   FixedVector<std::string> options_; | ||||
| }; | ||||
|  | ||||
| }  // namespace select | ||||
|   | ||||
| @@ -878,9 +878,7 @@ async def setup_sensor_core_(var, config): | ||||
|         cg.add(var.set_unit_of_measurement(unit_of_measurement)) | ||||
|     if (accuracy_decimals := config.get(CONF_ACCURACY_DECIMALS)) is not None: | ||||
|         cg.add(var.set_accuracy_decimals(accuracy_decimals)) | ||||
|     # Only set force_update if True (default is False) | ||||
|     if config[CONF_FORCE_UPDATE]: | ||||
|         cg.add(var.set_force_update(True)) | ||||
|     cg.add(var.set_force_update(config[CONF_FORCE_UPDATE])) | ||||
|     if config.get(CONF_FILTERS):  # must exist and not be empty | ||||
|         filters = await build_filters(config[CONF_FILTERS]) | ||||
|         cg.add(var.set_filters(filters)) | ||||
|   | ||||
| @@ -27,7 +27,7 @@ void SNTPComponent::setup() { | ||||
|   esp_sntp_setoperatingmode(ESP_SNTP_OPMODE_POLL); | ||||
|   size_t i = 0; | ||||
|   for (auto &server : this->servers_) { | ||||
|     esp_sntp_setservername(i++, server); | ||||
|     esp_sntp_setservername(i++, server.c_str()); | ||||
|   } | ||||
|   esp_sntp_set_sync_interval(this->get_update_interval()); | ||||
|   esp_sntp_set_time_sync_notification_cb([](struct timeval *tv) { | ||||
| @@ -42,7 +42,7 @@ void SNTPComponent::setup() { | ||||
|  | ||||
|   size_t i = 0; | ||||
|   for (auto &server : this->servers_) { | ||||
|     sntp_setservername(i++, server); | ||||
|     sntp_setservername(i++, server.c_str()); | ||||
|   } | ||||
|  | ||||
| #if defined(USE_ESP8266) | ||||
| @@ -59,7 +59,7 @@ void SNTPComponent::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "SNTP Time:"); | ||||
|   size_t i = 0; | ||||
|   for (auto &server : this->servers_) { | ||||
|     ESP_LOGCONFIG(TAG, "  Server %zu: '%s'", i++, server); | ||||
|     ESP_LOGCONFIG(TAG, "  Server %zu: '%s'", i++, server.c_str()); | ||||
|   } | ||||
| } | ||||
| void SNTPComponent::update() { | ||||
|   | ||||
| @@ -2,14 +2,10 @@ | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/time/real_time_clock.h" | ||||
| #include <array> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace sntp { | ||||
|  | ||||
| // Server count is calculated at compile time by Python codegen | ||||
| // SNTP_SERVER_COUNT will always be defined | ||||
|  | ||||
| /// The SNTP component allows you to configure local timekeeping via Simple Network Time Protocol. | ||||
| /// | ||||
| /// \note | ||||
| @@ -18,7 +14,10 @@ namespace sntp { | ||||
| /// \see https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html | ||||
| class SNTPComponent : public time::RealTimeClock { | ||||
|  public: | ||||
|   SNTPComponent(const std::array<const char *, SNTP_SERVER_COUNT> &servers) : servers_(servers) {} | ||||
|   SNTPComponent(const std::vector<std::string> &servers) : servers_(servers) {} | ||||
|  | ||||
|   // Note: set_servers() has been removed and replaced by a constructor - calling set_servers after setup would | ||||
|   // have had no effect anyway, and making the strings immutable avoids the need to strdup their contents. | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
| @@ -30,10 +29,7 @@ class SNTPComponent : public time::RealTimeClock { | ||||
|   void time_synced(); | ||||
|  | ||||
|  protected: | ||||
|   // Store const char pointers to string literals | ||||
|   // ESP8266: strings in rodata (RAM), but avoids std::string overhead (~24 bytes each) | ||||
|   // Other platforms: strings in flash | ||||
|   std::array<const char *, SNTP_SERVER_COUNT> servers_; | ||||
|   std::vector<std::string> servers_; | ||||
|   bool has_time_{false}; | ||||
|  | ||||
| #if defined(USE_ESP32) | ||||
|   | ||||
| @@ -43,11 +43,6 @@ CONFIG_SCHEMA = cv.All( | ||||
|  | ||||
| async def to_code(config): | ||||
|     servers = config[CONF_SERVERS] | ||||
|  | ||||
|     # Define server count at compile time | ||||
|     cg.add_define("SNTP_SERVER_COUNT", len(servers)) | ||||
|  | ||||
|     # Pass string literals to constructor - stored in flash/rodata by compiler | ||||
|     var = cg.new_Pvariable(config[CONF_ID], servers) | ||||
|  | ||||
|     await cg.register_component(var, config) | ||||
|   | ||||
| @@ -10,9 +10,6 @@ from .. import template_ns | ||||
| TemplateBinarySensor = template_ns.class_( | ||||
|     "TemplateBinarySensor", binary_sensor.BinarySensor, cg.Component | ||||
| ) | ||||
| StatelessTemplateBinarySensor = template_ns.class_( | ||||
|     "StatelessTemplateBinarySensor", binary_sensor.BinarySensor, cg.Component | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = ( | ||||
|     binary_sensor.binary_sensor_schema(TemplateBinarySensor) | ||||
| @@ -29,22 +26,15 @@ CONFIG_SCHEMA = ( | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     # Check if we have a lambda first - determines which class to instantiate | ||||
|     var = await binary_sensor.new_binary_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     if lamb := config.get(CONF_LAMBDA): | ||||
|         # Use new_lambda_pvariable to create either TemplateBinarySensor or StatelessTemplateBinarySensor | ||||
|         template_ = await cg.process_lambda( | ||||
|             lamb, [], return_type=cg.optional.template(bool) | ||||
|         ) | ||||
|         var = automation.new_lambda_pvariable( | ||||
|             config[CONF_ID], template_, StatelessTemplateBinarySensor | ||||
|         ) | ||||
|         # Manually register as binary sensor since we didn't use new_binary_sensor | ||||
|         await binary_sensor.register_binary_sensor(var, config) | ||||
|         await cg.register_component(var, config) | ||||
|     elif condition := config.get(CONF_CONDITION): | ||||
|         # For conditions, create stateful version and set template | ||||
|         var = await binary_sensor.new_binary_sensor(config) | ||||
|         await cg.register_component(var, config) | ||||
|         cg.add(var.set_template(template_)) | ||||
|     if condition := config.get(CONF_CONDITION): | ||||
|         condition = await automation.build_condition( | ||||
|             condition, cg.TemplateArguments(), [] | ||||
|         ) | ||||
| @@ -52,10 +42,6 @@ async def to_code(config): | ||||
|             f"return {condition.check()};", [], return_type=cg.optional.template(bool) | ||||
|         ) | ||||
|         cg.add(var.set_template(template_)) | ||||
|     else: | ||||
|         # No lambda or condition - just create the base template sensor | ||||
|         var = await binary_sensor.new_binary_sensor(config) | ||||
|         await cg.register_component(var, config) | ||||
|  | ||||
|  | ||||
| @automation.register_action( | ||||
|   | ||||
| @@ -6,13 +6,18 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.binary_sensor"; | ||||
|  | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateBinarySensorBase<F>::dump_config() { | ||||
|   LOG_BINARY_SENSOR("", "Template Binary Sensor", this); | ||||
| } | ||||
| void TemplateBinarySensor::setup() { this->loop(); } | ||||
|  | ||||
| template class TemplateBinarySensorBase<std::function<optional<bool>()>>; | ||||
| template class TemplateBinarySensorBase<optional<bool> (*)()>; | ||||
| void TemplateBinarySensor::loop() { | ||||
|   if (this->f_ == nullptr) | ||||
|     return; | ||||
|  | ||||
|   auto s = this->f_(); | ||||
|   if (s.has_value()) { | ||||
|     this->publish_state(*s); | ||||
|   } | ||||
| } | ||||
| void TemplateBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Template Binary Sensor", this); } | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -6,41 +6,18 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| template<typename F> class TemplateBinarySensorBase : public Component, public binary_sensor::BinarySensor { | ||||
| class TemplateBinarySensor : public Component, public binary_sensor::BinarySensor { | ||||
|  public: | ||||
|   void setup() override { this->loop(); } | ||||
|  | ||||
|   void loop() override { | ||||
|     if (this->f_ == nullptr) | ||||
|       return; | ||||
|     auto s = this->f_(); | ||||
|     if (s.has_value()) { | ||||
|       this->publish_state(*s); | ||||
|     } | ||||
|   } | ||||
|   void set_template(std::function<optional<bool>()> &&f) { this->f_ = f; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|   void dump_config() override; | ||||
|  | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|  protected: | ||||
|   F f_; | ||||
| }; | ||||
|  | ||||
| class TemplateBinarySensor : public TemplateBinarySensorBase<std::function<optional<bool>()>> { | ||||
|  public: | ||||
|   TemplateBinarySensor() { this->f_ = nullptr; } | ||||
|   void set_template(std::function<optional<bool>()> &&f) { this->f_ = f; } | ||||
| }; | ||||
|  | ||||
| /** Optimized template binary sensor for stateless lambdas (no capture). | ||||
|  * | ||||
|  * Uses function pointer instead of std::function to reduce memory overhead. | ||||
|  * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function). | ||||
|  */ | ||||
| class StatelessTemplateBinarySensor : public TemplateBinarySensorBase<optional<bool> (*)()> { | ||||
|  public: | ||||
|   explicit StatelessTemplateBinarySensor(optional<bool> (*f)()) { this->f_ = f; } | ||||
|   std::function<optional<bool>()> f_{nullptr}; | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -23,9 +23,6 @@ from esphome.const import ( | ||||
| from .. import template_ns | ||||
|  | ||||
| TemplateCover = template_ns.class_("TemplateCover", cover.Cover, cg.Component) | ||||
| StatelessTemplateCover = template_ns.class_( | ||||
|     "StatelessTemplateCover", cover.Cover, cg.Component | ||||
| ) | ||||
|  | ||||
| TemplateCoverRestoreMode = template_ns.enum("TemplateCoverRestoreMode") | ||||
| RESTORE_MODES = { | ||||
| @@ -66,22 +63,13 @@ CONFIG_SCHEMA = ( | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await cover.new_cover(config) | ||||
|     await cg.register_component(var, config) | ||||
|     if CONF_LAMBDA in config: | ||||
|         # Use new_lambda_pvariable to create either TemplateCover or StatelessTemplateCover | ||||
|         template_ = await cg.process_lambda( | ||||
|             config[CONF_LAMBDA], [], return_type=cg.optional.template(float) | ||||
|         ) | ||||
|         var = automation.new_lambda_pvariable( | ||||
|             config[CONF_ID], template_, StatelessTemplateCover | ||||
|         ) | ||||
|         # Manually register as cover since we didn't use new_cover | ||||
|         await cover.register_cover(var, config) | ||||
|         await cg.register_component(var, config) | ||||
|     else: | ||||
|         # No state lambda - just create the base template cover | ||||
|         var = await cover.new_cover(config) | ||||
|         await cg.register_component(var, config) | ||||
|  | ||||
|         cg.add(var.set_state_lambda(template_)) | ||||
|     if CONF_OPEN_ACTION in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_open_trigger(), [], config[CONF_OPEN_ACTION] | ||||
|   | ||||
| @@ -8,8 +8,14 @@ using namespace esphome::cover; | ||||
|  | ||||
| static const char *const TAG = "template.cover"; | ||||
|  | ||||
| // Template instantiations | ||||
| template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::setup() { | ||||
| TemplateCover::TemplateCover() | ||||
|     : open_trigger_(new Trigger<>()), | ||||
|       close_trigger_(new Trigger<>), | ||||
|       stop_trigger_(new Trigger<>()), | ||||
|       toggle_trigger_(new Trigger<>()), | ||||
|       position_trigger_(new Trigger<float>()), | ||||
|       tilt_trigger_(new Trigger<float>()) {} | ||||
| void TemplateCover::setup() { | ||||
|   switch (this->restore_mode_) { | ||||
|     case COVER_NO_RESTORE: | ||||
|       break; | ||||
| @@ -28,12 +34,43 @@ template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>: | ||||
|     } | ||||
|   } | ||||
| } | ||||
| void TemplateCover::loop() { | ||||
|   bool changed = false; | ||||
|  | ||||
| template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::dump_config() { | ||||
|   LOG_COVER("", "Template Cover", this); | ||||
|   if (this->state_f_.has_value()) { | ||||
|     auto s = (*this->state_f_)(); | ||||
|     if (s.has_value()) { | ||||
|       auto pos = clamp(*s, 0.0f, 1.0f); | ||||
|       if (pos != this->position) { | ||||
|         this->position = pos; | ||||
|         changed = true; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   if (this->tilt_f_.has_value()) { | ||||
|     auto s = (*this->tilt_f_)(); | ||||
|     if (s.has_value()) { | ||||
|       auto tilt = clamp(*s, 0.0f, 1.0f); | ||||
|       if (tilt != this->tilt) { | ||||
|         this->tilt = tilt; | ||||
|         changed = true; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (changed) | ||||
|     this->publish_state(); | ||||
| } | ||||
|  | ||||
| template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::control(const CoverCall &call) { | ||||
| void TemplateCover::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
| void TemplateCover::set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } | ||||
| void TemplateCover::set_state_lambda(std::function<optional<float>()> &&f) { this->state_f_ = f; } | ||||
| float TemplateCover::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
| Trigger<> *TemplateCover::get_open_trigger() const { return this->open_trigger_; } | ||||
| Trigger<> *TemplateCover::get_close_trigger() const { return this->close_trigger_; } | ||||
| Trigger<> *TemplateCover::get_stop_trigger() const { return this->stop_trigger_; } | ||||
| Trigger<> *TemplateCover::get_toggle_trigger() const { return this->toggle_trigger_; } | ||||
| void TemplateCover::dump_config() { LOG_COVER("", "Template Cover", this); } | ||||
| void TemplateCover::control(const CoverCall &call) { | ||||
|   if (call.get_stop()) { | ||||
|     this->stop_prev_trigger_(); | ||||
|     this->stop_trigger_->trigger(); | ||||
| @@ -76,8 +113,7 @@ template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>: | ||||
|  | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| template<typename StateF, typename TiltF> CoverTraits TemplateCoverBase<StateF, TiltF>::get_traits() { | ||||
| CoverTraits TemplateCover::get_traits() { | ||||
|   auto traits = CoverTraits(); | ||||
|   traits.set_is_assumed_state(this->assumed_state_); | ||||
|   traits.set_supports_stop(this->has_stop_); | ||||
| @@ -86,16 +122,19 @@ template<typename StateF, typename TiltF> CoverTraits TemplateCoverBase<StateF, | ||||
|   traits.set_supports_tilt(this->has_tilt_); | ||||
|   return traits; | ||||
| } | ||||
|  | ||||
| template<typename StateF, typename TiltF> void TemplateCoverBase<StateF, TiltF>::stop_prev_trigger_() { | ||||
| Trigger<float> *TemplateCover::get_position_trigger() const { return this->position_trigger_; } | ||||
| Trigger<float> *TemplateCover::get_tilt_trigger() const { return this->tilt_trigger_; } | ||||
| void TemplateCover::set_tilt_lambda(std::function<optional<float>()> &&tilt_f) { this->tilt_f_ = tilt_f; } | ||||
| void TemplateCover::set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } | ||||
| void TemplateCover::set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } | ||||
| void TemplateCover::set_has_position(bool has_position) { this->has_position_ = has_position; } | ||||
| void TemplateCover::set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } | ||||
| void TemplateCover::stop_prev_trigger_() { | ||||
|   if (this->prev_command_trigger_ != nullptr) { | ||||
|     this->prev_command_trigger_->stop_action(); | ||||
|     this->prev_command_trigger_ = nullptr; | ||||
|   } | ||||
| } | ||||
|  | ||||
| template class TemplateCoverBase<std::function<optional<float>()>, std::function<optional<float>()>>; | ||||
| template class TemplateCoverBase<optional<float> (*)(), optional<float> (*)()>; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -13,59 +13,31 @@ enum TemplateCoverRestoreMode { | ||||
|   COVER_RESTORE_AND_CALL, | ||||
| }; | ||||
|  | ||||
| template<typename StateF, typename TiltF> class TemplateCoverBase : public cover::Cover, public Component { | ||||
| class TemplateCover : public cover::Cover, public Component { | ||||
|  public: | ||||
|   TemplateCoverBase() | ||||
|       : open_trigger_(new Trigger<>()), | ||||
|         close_trigger_(new Trigger<>()), | ||||
|         stop_trigger_(new Trigger<>()), | ||||
|         toggle_trigger_(new Trigger<>()), | ||||
|         position_trigger_(new Trigger<float>()), | ||||
|         tilt_trigger_(new Trigger<float>()) {} | ||||
|   TemplateCover(); | ||||
|  | ||||
|   void loop() override { | ||||
|     bool changed = false; | ||||
|     if (this->state_f_.has_value()) { | ||||
|       auto s = (*this->state_f_)(); | ||||
|       if (s.has_value()) { | ||||
|         auto pos = clamp(*s, 0.0f, 1.0f); | ||||
|         if (pos != this->position) { | ||||
|           this->position = pos; | ||||
|           changed = true; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (this->tilt_f_.has_value()) { | ||||
|       auto s = (*this->tilt_f_)(); | ||||
|       if (s.has_value()) { | ||||
|         auto tilt = clamp(*s, 0.0f, 1.0f); | ||||
|         if (tilt != this->tilt) { | ||||
|           this->tilt = tilt; | ||||
|           changed = true; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (changed) | ||||
|       this->publish_state(); | ||||
|   } | ||||
|   void set_state_lambda(std::function<optional<float>()> &&f); | ||||
|   Trigger<> *get_open_trigger() const; | ||||
|   Trigger<> *get_close_trigger() const; | ||||
|   Trigger<> *get_stop_trigger() const; | ||||
|   Trigger<> *get_toggle_trigger() const; | ||||
|   Trigger<float> *get_position_trigger() const; | ||||
|   Trigger<float> *get_tilt_trigger() const; | ||||
|   void set_optimistic(bool optimistic); | ||||
|   void set_assumed_state(bool assumed_state); | ||||
|   void set_tilt_lambda(std::function<optional<float>()> &&tilt_f); | ||||
|   void set_has_stop(bool has_stop); | ||||
|   void set_has_position(bool has_position); | ||||
|   void set_has_tilt(bool has_tilt); | ||||
|   void set_has_toggle(bool has_toggle); | ||||
|   void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
|   Trigger<> *get_open_trigger() const { return this->open_trigger_; } | ||||
|   Trigger<> *get_close_trigger() const { return this->close_trigger_; } | ||||
|   Trigger<> *get_stop_trigger() const { return this->stop_trigger_; } | ||||
|   Trigger<> *get_toggle_trigger() const { return this->toggle_trigger_; } | ||||
|   Trigger<float> *get_position_trigger() const { return this->position_trigger_; } | ||||
|   Trigger<float> *get_tilt_trigger() const { return this->tilt_trigger_; } | ||||
|   void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
|   void set_assumed_state(bool assumed_state) { this->assumed_state_ = assumed_state; } | ||||
|   void set_has_stop(bool has_stop) { this->has_stop_ = has_stop; } | ||||
|   void set_has_position(bool has_position) { this->has_position_ = has_position; } | ||||
|   void set_has_tilt(bool has_tilt) { this->has_tilt_ = has_tilt; } | ||||
|   void set_has_toggle(bool has_toggle) { this->has_toggle_ = has_toggle; } | ||||
|   void set_restore_mode(TemplateCoverRestoreMode restore_mode) { restore_mode_ = restore_mode; } | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|  protected: | ||||
|   void control(const cover::CoverCall &call) override; | ||||
| @@ -73,8 +45,8 @@ template<typename StateF, typename TiltF> class TemplateCoverBase : public cover | ||||
|   void stop_prev_trigger_(); | ||||
|  | ||||
|   TemplateCoverRestoreMode restore_mode_{COVER_RESTORE}; | ||||
|   optional<StateF> state_f_; | ||||
|   optional<TiltF> tilt_f_; | ||||
|   optional<std::function<optional<float>()>> state_f_; | ||||
|   optional<std::function<optional<float>()>> tilt_f_; | ||||
|   bool assumed_state_{false}; | ||||
|   bool optimistic_{false}; | ||||
|   Trigger<> *open_trigger_; | ||||
| @@ -90,22 +62,5 @@ template<typename StateF, typename TiltF> class TemplateCoverBase : public cover | ||||
|   bool has_tilt_{false}; | ||||
| }; | ||||
|  | ||||
| class TemplateCover : public TemplateCoverBase<std::function<optional<float>()>, std::function<optional<float>()>> { | ||||
|  public: | ||||
|   void set_state_lambda(std::function<optional<float>()> &&f) { this->state_f_ = f; } | ||||
|   void set_tilt_lambda(std::function<optional<float>()> &&tilt_f) { this->tilt_f_ = tilt_f; } | ||||
| }; | ||||
|  | ||||
| /** Optimized template cover for stateless lambdas (no capture). | ||||
|  * | ||||
|  * Uses function pointers instead of std::function to reduce memory overhead. | ||||
|  * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function) per lambda. | ||||
|  */ | ||||
| class StatelessTemplateCover : public TemplateCoverBase<optional<float> (*)(), optional<float> (*)()> { | ||||
|  public: | ||||
|   explicit StatelessTemplateCover(optional<float> (*state_f)()) { this->state_f_ = state_f; } | ||||
|   void set_tilt_lambda(optional<float> (*tilt_f)()) { this->tilt_f_ = tilt_f; } | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_DAY, | ||||
|     CONF_HOUR, | ||||
|     CONF_ID, | ||||
|     CONF_INITIAL_VALUE, | ||||
|     CONF_LAMBDA, | ||||
|     CONF_MINUTE, | ||||
| @@ -26,23 +25,14 @@ CODEOWNERS = ["@rfdarter"] | ||||
| TemplateDate = template_ns.class_( | ||||
|     "TemplateDate", datetime.DateEntity, cg.PollingComponent | ||||
| ) | ||||
| StatelessTemplateDate = template_ns.class_( | ||||
|     "StatelessTemplateDate", datetime.DateEntity, cg.PollingComponent | ||||
| ) | ||||
|  | ||||
| TemplateTime = template_ns.class_( | ||||
|     "TemplateTime", datetime.TimeEntity, cg.PollingComponent | ||||
| ) | ||||
| StatelessTemplateTime = template_ns.class_( | ||||
|     "StatelessTemplateTime", datetime.TimeEntity, cg.PollingComponent | ||||
| ) | ||||
|  | ||||
| TemplateDateTime = template_ns.class_( | ||||
|     "TemplateDateTime", datetime.DateTimeEntity, cg.PollingComponent | ||||
| ) | ||||
| StatelessTemplateDateTime = template_ns.class_( | ||||
|     "StatelessTemplateDateTime", datetime.DateTimeEntity, cg.PollingComponent | ||||
| ) | ||||
|  | ||||
|  | ||||
| def validate(config): | ||||
| @@ -109,30 +99,15 @@ CONFIG_SCHEMA = cv.All( | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await datetime.new_datetime(config) | ||||
|  | ||||
|     if CONF_LAMBDA in config: | ||||
|         # Use new_lambda_pvariable to create either Template* or StatelessTemplate* | ||||
|         template_ = await cg.process_lambda( | ||||
|             config[CONF_LAMBDA], [], return_type=cg.optional.template(cg.ESPTime) | ||||
|         ) | ||||
|         # Determine the appropriate stateless class based on type | ||||
|         if config[CONF_TYPE] == "DATE": | ||||
|             stateless_class = StatelessTemplateDate | ||||
|         elif config[CONF_TYPE] == "TIME": | ||||
|             stateless_class = StatelessTemplateTime | ||||
|         else:  # DATETIME | ||||
|             stateless_class = StatelessTemplateDateTime | ||||
|         cg.add(var.set_template(template_)) | ||||
|  | ||||
|         var = automation.new_lambda_pvariable( | ||||
|             config[CONF_ID], template_, stateless_class | ||||
|         ) | ||||
|         # Manually register as datetime since we didn't use new_datetime | ||||
|         await datetime.register_datetime(var, config) | ||||
|         await cg.register_component(var, config) | ||||
|     else: | ||||
|         # No lambda - just create the base template datetime | ||||
|         var = await datetime.new_datetime(config) | ||||
|         await cg.register_component(var, config) | ||||
|  | ||||
|         cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) | ||||
|         cg.add(var.set_restore_value(config[CONF_RESTORE_VALUE])) | ||||
|  | ||||
| @@ -171,3 +146,5 @@ async def to_code(config): | ||||
|             [(cg.ESPTime, "x")], | ||||
|             config[CONF_SET_ACTION], | ||||
|         ) | ||||
|  | ||||
|     await cg.register_component(var, config) | ||||
|   | ||||
| @@ -9,8 +9,7 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.date"; | ||||
|  | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateDateBase<F>::setup() { | ||||
| void TemplateDate::setup() { | ||||
|   if (this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
| @@ -37,7 +36,21 @@ template<typename F> void TemplateDateBase<F>::setup() { | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| template<typename F> void TemplateDateBase<F>::control(const datetime::DateCall &call) { | ||||
| void TemplateDate::update() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto val = (*this->f_)(); | ||||
|   if (!val.has_value()) | ||||
|     return; | ||||
|  | ||||
|   this->year_ = val->year; | ||||
|   this->month_ = val->month; | ||||
|   this->day_ = val->day_of_month; | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| void TemplateDate::control(const datetime::DateCall &call) { | ||||
|   bool has_year = call.get_year().has_value(); | ||||
|   bool has_month = call.get_month().has_value(); | ||||
|   bool has_day = call.get_day().has_value(); | ||||
| @@ -86,15 +99,12 @@ template<typename F> void TemplateDateBase<F>::control(const datetime::DateCall | ||||
|   } | ||||
| } | ||||
|  | ||||
| template<typename F> void TemplateDateBase<F>::dump_config() { | ||||
| void TemplateDate::dump_config() { | ||||
|   LOG_DATETIME_DATE("", "Template Date", this); | ||||
|   ESP_LOGCONFIG(TAG, "  Optimistic: %s", YESNO(this->optimistic_)); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| template class TemplateDateBase<std::function<optional<ESPTime>()>>; | ||||
| template class TemplateDateBase<optional<ESPTime> (*)()>; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -13,23 +13,12 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| template<typename F> class TemplateDateBase : public datetime::DateEntity, public PollingComponent { | ||||
| class TemplateDate : public datetime::DateEntity, public PollingComponent { | ||||
|  public: | ||||
|   void update() override { | ||||
|     if (!this->f_.has_value()) | ||||
|       return; | ||||
|  | ||||
|     auto val = (*this->f_)(); | ||||
|     if (!val.has_value()) | ||||
|       return; | ||||
|  | ||||
|     this->year_ = val->year; | ||||
|     this->month_ = val->month; | ||||
|     this->day_ = val->day_of_month; | ||||
|     this->publish_state(); | ||||
|   } | ||||
|   void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
| @@ -46,26 +35,11 @@ template<typename F> class TemplateDateBase : public datetime::DateEntity, publi | ||||
|   ESPTime initial_value_{}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); | ||||
|   optional<F> f_; | ||||
|   optional<std::function<optional<ESPTime>()>> f_; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
| }; | ||||
|  | ||||
| class TemplateDate : public TemplateDateBase<std::function<optional<ESPTime>()>> { | ||||
|  public: | ||||
|   void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; } | ||||
| }; | ||||
|  | ||||
| /** Optimized template date for stateless lambdas (no capture). | ||||
|  * | ||||
|  * Uses function pointers instead of std::function to reduce memory overhead. | ||||
|  * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function) per lambda. | ||||
|  */ | ||||
| class StatelessTemplateDate : public TemplateDateBase<optional<ESPTime> (*)()> { | ||||
|  public: | ||||
|   explicit StatelessTemplateDate(optional<ESPTime> (*f)()) { this->f_ = f; } | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -9,8 +9,7 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.datetime"; | ||||
|  | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateDateTimeBase<F>::setup() { | ||||
| void TemplateDateTime::setup() { | ||||
|   if (this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
| @@ -40,7 +39,24 @@ template<typename F> void TemplateDateTimeBase<F>::setup() { | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| template<typename F> void TemplateDateTimeBase<F>::control(const datetime::DateTimeCall &call) { | ||||
| void TemplateDateTime::update() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto val = (*this->f_)(); | ||||
|   if (!val.has_value()) | ||||
|     return; | ||||
|  | ||||
|   this->year_ = val->year; | ||||
|   this->month_ = val->month; | ||||
|   this->day_ = val->day_of_month; | ||||
|   this->hour_ = val->hour; | ||||
|   this->minute_ = val->minute; | ||||
|   this->second_ = val->second; | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| void TemplateDateTime::control(const datetime::DateTimeCall &call) { | ||||
|   bool has_year = call.get_year().has_value(); | ||||
|   bool has_month = call.get_month().has_value(); | ||||
|   bool has_day = call.get_day().has_value(); | ||||
| @@ -122,15 +138,12 @@ template<typename F> void TemplateDateTimeBase<F>::control(const datetime::DateT | ||||
|   } | ||||
| } | ||||
|  | ||||
| template<typename F> void TemplateDateTimeBase<F>::dump_config() { | ||||
| void TemplateDateTime::dump_config() { | ||||
|   LOG_DATETIME_DATETIME("", "Template DateTime", this); | ||||
|   ESP_LOGCONFIG(TAG, "  Optimistic: %s", YESNO(this->optimistic_)); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| template class TemplateDateTimeBase<std::function<optional<ESPTime>()>>; | ||||
| template class TemplateDateTimeBase<optional<ESPTime> (*)()>; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -13,26 +13,12 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| template<typename F> class TemplateDateTimeBase : public datetime::DateTimeEntity, public PollingComponent { | ||||
| class TemplateDateTime : public datetime::DateTimeEntity, public PollingComponent { | ||||
|  public: | ||||
|   void update() override { | ||||
|     if (!this->f_.has_value()) | ||||
|       return; | ||||
|  | ||||
|     auto val = (*this->f_)(); | ||||
|     if (!val.has_value()) | ||||
|       return; | ||||
|  | ||||
|     this->year_ = val->year; | ||||
|     this->month_ = val->month; | ||||
|     this->day_ = val->day_of_month; | ||||
|     this->hour_ = val->hour; | ||||
|     this->minute_ = val->minute; | ||||
|     this->second_ = val->second; | ||||
|     this->publish_state(); | ||||
|   } | ||||
|   void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
| @@ -49,26 +35,11 @@ template<typename F> class TemplateDateTimeBase : public datetime::DateTimeEntit | ||||
|   ESPTime initial_value_{}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); | ||||
|   optional<F> f_; | ||||
|   optional<std::function<optional<ESPTime>()>> f_; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
| }; | ||||
|  | ||||
| class TemplateDateTime : public TemplateDateTimeBase<std::function<optional<ESPTime>()>> { | ||||
|  public: | ||||
|   void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; } | ||||
| }; | ||||
|  | ||||
| /** Optimized template datetime for stateless lambdas (no capture). | ||||
|  * | ||||
|  * Uses function pointers instead of std::function to reduce memory overhead. | ||||
|  * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function) per lambda. | ||||
|  */ | ||||
| class StatelessTemplateDateTime : public TemplateDateTimeBase<optional<ESPTime> (*)()> { | ||||
|  public: | ||||
|   explicit StatelessTemplateDateTime(optional<ESPTime> (*f)()) { this->f_ = f; } | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -9,8 +9,7 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.time"; | ||||
|  | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateTimeBase<F>::setup() { | ||||
| void TemplateTime::setup() { | ||||
|   if (this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
| @@ -37,7 +36,21 @@ template<typename F> void TemplateTimeBase<F>::setup() { | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| template<typename F> void TemplateTimeBase<F>::control(const datetime::TimeCall &call) { | ||||
| void TemplateTime::update() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto val = (*this->f_)(); | ||||
|   if (!val.has_value()) | ||||
|     return; | ||||
|  | ||||
|   this->hour_ = val->hour; | ||||
|   this->minute_ = val->minute; | ||||
|   this->second_ = val->second; | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| void TemplateTime::control(const datetime::TimeCall &call) { | ||||
|   bool has_hour = call.get_hour().has_value(); | ||||
|   bool has_minute = call.get_minute().has_value(); | ||||
|   bool has_second = call.get_second().has_value(); | ||||
| @@ -86,15 +99,12 @@ template<typename F> void TemplateTimeBase<F>::control(const datetime::TimeCall | ||||
|   } | ||||
| } | ||||
|  | ||||
| template<typename F> void TemplateTimeBase<F>::dump_config() { | ||||
| void TemplateTime::dump_config() { | ||||
|   LOG_DATETIME_TIME("", "Template Time", this); | ||||
|   ESP_LOGCONFIG(TAG, "  Optimistic: %s", YESNO(this->optimistic_)); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| template class TemplateTimeBase<std::function<optional<ESPTime>()>>; | ||||
| template class TemplateTimeBase<optional<ESPTime> (*)()>; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -13,23 +13,12 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| template<typename F> class TemplateTimeBase : public datetime::TimeEntity, public PollingComponent { | ||||
| class TemplateTime : public datetime::TimeEntity, public PollingComponent { | ||||
|  public: | ||||
|   void update() override { | ||||
|     if (!this->f_.has_value()) | ||||
|       return; | ||||
|  | ||||
|     auto val = (*this->f_)(); | ||||
|     if (!val.has_value()) | ||||
|       return; | ||||
|  | ||||
|     this->hour_ = val->hour; | ||||
|     this->minute_ = val->minute; | ||||
|     this->second_ = val->second; | ||||
|     this->publish_state(); | ||||
|   } | ||||
|   void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void update() override; | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|  | ||||
| @@ -46,26 +35,11 @@ template<typename F> class TemplateTimeBase : public datetime::TimeEntity, publi | ||||
|   ESPTime initial_value_{}; | ||||
|   bool restore_value_{false}; | ||||
|   Trigger<ESPTime> *set_trigger_ = new Trigger<ESPTime>(); | ||||
|   optional<F> f_; | ||||
|   optional<std::function<optional<ESPTime>()>> f_; | ||||
|  | ||||
|   ESPPreferenceObject pref_; | ||||
| }; | ||||
|  | ||||
| class TemplateTime : public TemplateTimeBase<std::function<optional<ESPTime>()>> { | ||||
|  public: | ||||
|   void set_template(std::function<optional<ESPTime>()> &&f) { this->f_ = f; } | ||||
| }; | ||||
|  | ||||
| /** Optimized template time for stateless lambdas (no capture). | ||||
|  * | ||||
|  * Uses function pointers instead of std::function to reduce memory overhead. | ||||
|  * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function) per lambda. | ||||
|  */ | ||||
| class StatelessTemplateTime : public TemplateTimeBase<optional<ESPTime> (*)()> { | ||||
|  public: | ||||
|   explicit StatelessTemplateTime(optional<ESPTime> (*f)()) { this->f_ = f; } | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <vector> | ||||
| #include <set> | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/fan/fan.h" | ||||
| @@ -16,7 +16,7 @@ class TemplateFan : public Component, public fan::Fan { | ||||
|   void set_has_direction(bool has_direction) { this->has_direction_ = has_direction; } | ||||
|   void set_has_oscillating(bool has_oscillating) { this->has_oscillating_ = has_oscillating; } | ||||
|   void set_speed_count(int count) { this->speed_count_ = count; } | ||||
|   void set_preset_modes(const std::initializer_list<std::string> &presets) { this->preset_modes_ = presets; } | ||||
|   void set_preset_modes(const std::set<std::string> &presets) { this->preset_modes_ = presets; } | ||||
|   fan::FanTraits get_traits() override { return this->traits_; } | ||||
|  | ||||
|  protected: | ||||
| @@ -26,7 +26,7 @@ class TemplateFan : public Component, public fan::Fan { | ||||
|   bool has_direction_{false}; | ||||
|   int speed_count_{0}; | ||||
|   fan::FanTraits traits_; | ||||
|   std::vector<std::string> preset_modes_{}; | ||||
|   std::set<std::string> preset_modes_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
|   | ||||
| @@ -16,9 +16,6 @@ from esphome.const import ( | ||||
| from .. import template_ns | ||||
|  | ||||
| TemplateLock = template_ns.class_("TemplateLock", lock.Lock, cg.Component) | ||||
| StatelessTemplateLock = template_ns.class_( | ||||
|     "StatelessTemplateLock", lock.Lock, cg.Component | ||||
| ) | ||||
|  | ||||
| TemplateLockPublishAction = template_ns.class_( | ||||
|     "TemplateLockPublishAction", | ||||
| @@ -58,22 +55,14 @@ CONFIG_SCHEMA = cv.All( | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await lock.new_lock(config) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|     if CONF_LAMBDA in config: | ||||
|         # Use new_lambda_pvariable to create either TemplateLock or StatelessTemplateLock | ||||
|         template_ = await cg.process_lambda( | ||||
|             config[CONF_LAMBDA], [], return_type=cg.optional.template(lock.LockState) | ||||
|         ) | ||||
|         var = automation.new_lambda_pvariable( | ||||
|             config[CONF_ID], template_, StatelessTemplateLock | ||||
|         ) | ||||
|         # Manually register as lock since we didn't use new_lock | ||||
|         await lock.register_lock(var, config) | ||||
|         await cg.register_component(var, config) | ||||
|     else: | ||||
|         # No lambda - just create the base template lock | ||||
|         var = await lock.new_lock(config) | ||||
|         await cg.register_component(var, config) | ||||
|  | ||||
|         cg.add(var.set_state_lambda(template_)) | ||||
|     if CONF_UNLOCK_ACTION in config: | ||||
|         await automation.build_automation( | ||||
|             var.get_unlock_trigger(), [], config[CONF_UNLOCK_ACTION] | ||||
|   | ||||
| @@ -8,8 +8,19 @@ using namespace esphome::lock; | ||||
|  | ||||
| static const char *const TAG = "template.lock"; | ||||
|  | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateLockBase<F>::control(const lock::LockCall &call) { | ||||
| TemplateLock::TemplateLock() | ||||
|     : lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {} | ||||
|  | ||||
| void TemplateLock::loop() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|   auto val = (*this->f_)(); | ||||
|   if (!val.has_value()) | ||||
|     return; | ||||
|  | ||||
|   this->publish_state(*val); | ||||
| } | ||||
| void TemplateLock::control(const lock::LockCall &call) { | ||||
|   if (this->prev_trigger_ != nullptr) { | ||||
|     this->prev_trigger_->stop_action(); | ||||
|   } | ||||
| @@ -26,22 +37,23 @@ template<typename F> void TemplateLockBase<F>::control(const lock::LockCall &cal | ||||
|   if (this->optimistic_) | ||||
|     this->publish_state(state); | ||||
| } | ||||
|  | ||||
| template<typename F> void TemplateLockBase<F>::open_latch() { | ||||
| void TemplateLock::open_latch() { | ||||
|   if (this->prev_trigger_ != nullptr) { | ||||
|     this->prev_trigger_->stop_action(); | ||||
|   } | ||||
|   this->prev_trigger_ = this->open_trigger_; | ||||
|   this->open_trigger_->trigger(); | ||||
| } | ||||
|  | ||||
| template<typename F> void TemplateLockBase<F>::dump_config() { | ||||
| void TemplateLock::set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
| void TemplateLock::set_state_lambda(std::function<optional<lock::LockState>()> &&f) { this->f_ = f; } | ||||
| float TemplateLock::get_setup_priority() const { return setup_priority::HARDWARE; } | ||||
| Trigger<> *TemplateLock::get_lock_trigger() const { return this->lock_trigger_; } | ||||
| Trigger<> *TemplateLock::get_unlock_trigger() const { return this->unlock_trigger_; } | ||||
| Trigger<> *TemplateLock::get_open_trigger() const { return this->open_trigger_; } | ||||
| void TemplateLock::dump_config() { | ||||
|   LOG_LOCK("", "Template Lock", this); | ||||
|   ESP_LOGCONFIG(TAG, "  Optimistic: %s", YESNO(this->optimistic_)); | ||||
| } | ||||
|  | ||||
| template class TemplateLockBase<std::function<optional<lock::LockState>()>>; | ||||
| template class TemplateLockBase<optional<lock::LockState> (*)()>; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -7,35 +7,26 @@ | ||||
| namespace esphome { | ||||
| namespace template_ { | ||||
|  | ||||
| template<typename F> class TemplateLockBase : public lock::Lock, public Component { | ||||
| class TemplateLock : public lock::Lock, public Component { | ||||
|  public: | ||||
|   TemplateLockBase() | ||||
|       : lock_trigger_(new Trigger<>()), unlock_trigger_(new Trigger<>()), open_trigger_(new Trigger<>()) {} | ||||
|  | ||||
|   void loop() override { | ||||
|     if (!this->f_.has_value()) | ||||
|       return; | ||||
|     auto val = (*this->f_)(); | ||||
|     if (!val.has_value()) | ||||
|       return; | ||||
|  | ||||
|     this->publish_state(*val); | ||||
|   } | ||||
|   TemplateLock(); | ||||
|  | ||||
|   void dump_config() override; | ||||
|  | ||||
|   Trigger<> *get_lock_trigger() const { return this->lock_trigger_; } | ||||
|   Trigger<> *get_unlock_trigger() const { return this->unlock_trigger_; } | ||||
|   Trigger<> *get_open_trigger() const { return this->open_trigger_; } | ||||
|   void set_optimistic(bool optimistic) { this->optimistic_ = optimistic; } | ||||
|   void set_state_lambda(std::function<optional<lock::LockState>()> &&f); | ||||
|   Trigger<> *get_lock_trigger() const; | ||||
|   Trigger<> *get_unlock_trigger() const; | ||||
|   Trigger<> *get_open_trigger() const; | ||||
|   void set_optimistic(bool optimistic); | ||||
|   void loop() override; | ||||
|  | ||||
|   float get_setup_priority() const override { return setup_priority::HARDWARE; } | ||||
|   float get_setup_priority() const override; | ||||
|  | ||||
|  protected: | ||||
|   void control(const lock::LockCall &call) override; | ||||
|   void open_latch() override; | ||||
|  | ||||
|   optional<F> f_; | ||||
|   optional<std::function<optional<lock::LockState>()>> f_; | ||||
|   bool optimistic_{false}; | ||||
|   Trigger<> *lock_trigger_; | ||||
|   Trigger<> *unlock_trigger_; | ||||
| @@ -43,20 +34,5 @@ template<typename F> class TemplateLockBase : public lock::Lock, public Componen | ||||
|   Trigger<> *prev_trigger_{nullptr}; | ||||
| }; | ||||
|  | ||||
| class TemplateLock : public TemplateLockBase<std::function<optional<lock::LockState>()>> { | ||||
|  public: | ||||
|   void set_state_lambda(std::function<optional<lock::LockState>()> &&f) { this->f_ = f; } | ||||
| }; | ||||
|  | ||||
| /** Optimized template lock for stateless lambdas (no capture). | ||||
|  * | ||||
|  * Uses function pointers instead of std::function to reduce memory overhead. | ||||
|  * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function) per lambda. | ||||
|  */ | ||||
| class StatelessTemplateLock : public TemplateLockBase<optional<lock::LockState> (*)()> { | ||||
|  public: | ||||
|   explicit StatelessTemplateLock(optional<lock::LockState> (*f)()) { this->f_ = f; } | ||||
| }; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -19,9 +19,6 @@ from .. import template_ns | ||||
| TemplateNumber = template_ns.class_( | ||||
|     "TemplateNumber", number.Number, cg.PollingComponent | ||||
| ) | ||||
| StatelessTemplateNumber = template_ns.class_( | ||||
|     "StatelessTemplateNumber", number.Number, cg.PollingComponent | ||||
| ) | ||||
|  | ||||
|  | ||||
| def validate_min_max(config): | ||||
| @@ -69,33 +66,23 @@ CONFIG_SCHEMA = cv.All( | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|     await number.register_number( | ||||
|         var, | ||||
|         config, | ||||
|         min_value=config[CONF_MIN_VALUE], | ||||
|         max_value=config[CONF_MAX_VALUE], | ||||
|         step=config[CONF_STEP], | ||||
|     ) | ||||
|  | ||||
|     if CONF_LAMBDA in config: | ||||
|         # Use new_lambda_pvariable to create either TemplateNumber or StatelessTemplateNumber | ||||
|         template_ = await cg.process_lambda( | ||||
|             config[CONF_LAMBDA], [], return_type=cg.optional.template(float) | ||||
|         ) | ||||
|         var = automation.new_lambda_pvariable( | ||||
|             config[CONF_ID], template_, StatelessTemplateNumber | ||||
|         ) | ||||
|         await cg.register_component(var, config) | ||||
|         await number.register_number( | ||||
|             var, | ||||
|             config, | ||||
|             min_value=config[CONF_MIN_VALUE], | ||||
|             max_value=config[CONF_MAX_VALUE], | ||||
|             step=config[CONF_STEP], | ||||
|         ) | ||||
|         cg.add(var.set_template(template_)) | ||||
|  | ||||
|     else: | ||||
|         # No lambda - just create the base template number | ||||
|         var = cg.new_Pvariable(config[CONF_ID]) | ||||
|         await cg.register_component(var, config) | ||||
|         await number.register_number( | ||||
|             var, | ||||
|             config, | ||||
|             min_value=config[CONF_MIN_VALUE], | ||||
|             max_value=config[CONF_MAX_VALUE], | ||||
|             step=config[CONF_STEP], | ||||
|         ) | ||||
|         cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) | ||||
|         cg.add(var.set_initial_value(config[CONF_INITIAL_VALUE])) | ||||
|         if CONF_RESTORE_VALUE in config: | ||||
|   | ||||
| @@ -6,8 +6,7 @@ namespace template_ { | ||||
|  | ||||
| static const char *const TAG = "template.number"; | ||||
|  | ||||
| // Template instantiations | ||||
| template<typename F> void TemplateNumberBase<F>::setup() { | ||||
| void TemplateNumber::setup() { | ||||
|   if (this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
| @@ -27,7 +26,18 @@ template<typename F> void TemplateNumberBase<F>::setup() { | ||||
|   this->publish_state(value); | ||||
| } | ||||
|  | ||||
| template<typename F> void TemplateNumberBase<F>::control(float value) { | ||||
| void TemplateNumber::update() { | ||||
|   if (!this->f_.has_value()) | ||||
|     return; | ||||
|  | ||||
|   auto val = (*this->f_)(); | ||||
|   if (!val.has_value()) | ||||
|     return; | ||||
|  | ||||
|   this->publish_state(*val); | ||||
| } | ||||
|  | ||||
| void TemplateNumber::control(float value) { | ||||
|   this->set_trigger_->trigger(value); | ||||
|  | ||||
|   if (this->optimistic_) | ||||
| @@ -36,15 +46,11 @@ template<typename F> void TemplateNumberBase<F>::control(float value) { | ||||
|   if (this->restore_value_) | ||||
|     this->pref_.save(&value); | ||||
| } | ||||
|  | ||||
| template<typename F> void TemplateNumberBase<F>::dump_config() { | ||||
| void TemplateNumber::dump_config() { | ||||
|   LOG_NUMBER("", "Template Number", this); | ||||
|   ESP_LOGCONFIG(TAG, "  Optimistic: %s", YESNO(this->optimistic_)); | ||||
|   LOG_UPDATE_INTERVAL(this); | ||||
| } | ||||
|  | ||||
| template class TemplateNumberBase<std::function<optional<float>()>>; | ||||
| template class TemplateNumberBase<optional<float> (*)()>; | ||||
|  | ||||
| }  // namespace template_ | ||||
| }  // namespace esphome | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user