mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	
							
								
								
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							| @@ -48,7 +48,7 @@ PROJECT_NAME           = ESPHome | |||||||
| # could be handy for archiving the generated documentation or if some version | # could be handy for archiving the generated documentation or if some version | ||||||
| # control system is used. | # control system is used. | ||||||
|  |  | ||||||
| PROJECT_NUMBER         = 2025.7.0b3 | PROJECT_NUMBER         = 2025.7.0b4 | ||||||
|  |  | ||||||
| # Using the PROJECT_BRIEF tag one can provide an optional one line description | # Using the PROJECT_BRIEF tag one can provide an optional one line description | ||||||
| # for a project that appears at the top of each page and should give viewer a | # for a project that appears at the top of each page and should give viewer a | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ CONFIG_SCHEMA = cv.All( | |||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     if CORE.is_esp32 or CORE.is_libretiny: |     if CORE.is_esp32 or CORE.is_libretiny: | ||||||
|         # https://github.com/ESP32Async/AsyncTCP |         # https://github.com/ESP32Async/AsyncTCP | ||||||
|         cg.add_library("ESP32Async/AsyncTCP", "3.4.4") |         cg.add_library("ESP32Async/AsyncTCP", "3.4.5") | ||||||
|     elif CORE.is_esp8266: |     elif CORE.is_esp8266: | ||||||
|         # https://github.com/ESP32Async/ESPAsyncTCP |         # https://github.com/ESP32Async/ESPAsyncTCP | ||||||
|         cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") |         cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") | ||||||
|   | |||||||
| @@ -177,6 +177,10 @@ optional<FanRestoreState> Fan::restore_state_() { | |||||||
|   return {}; |   return {}; | ||||||
| } | } | ||||||
| void Fan::save_state_() { | void Fan::save_state_() { | ||||||
|  |   if (this->restore_mode_ == FanRestoreMode::NO_RESTORE) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   FanRestoreState state{}; |   FanRestoreState state{}; | ||||||
|   state.state = this->state; |   state.state = this->state; | ||||||
|   state.oscillating = this->oscillating; |   state.oscillating = this->oscillating; | ||||||
|   | |||||||
| @@ -83,7 +83,7 @@ void HttpRequestUpdate::update_task(void *params) { | |||||||
|     container.reset();  // Release ownership of the container's shared_ptr |     container.reset();  // Release ownership of the container's shared_ptr | ||||||
|  |  | ||||||
|     valid = json::parse_json(response, [this_update](JsonObject root) -> bool { |     valid = json::parse_json(response, [this_update](JsonObject root) -> bool { | ||||||
|       if (!root.containsKey("name") || !root.containsKey("version") || !root.containsKey("builds")) { |       if (!root["name"].is<const char *>() || !root["version"].is<const char *>() || !root["builds"].is<JsonArray>()) { | ||||||
|         ESP_LOGE(TAG, "Manifest does not contain required fields"); |         ESP_LOGE(TAG, "Manifest does not contain required fields"); | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
| @@ -91,26 +91,26 @@ void HttpRequestUpdate::update_task(void *params) { | |||||||
|       this_update->update_info_.latest_version = root["version"].as<std::string>(); |       this_update->update_info_.latest_version = root["version"].as<std::string>(); | ||||||
|  |  | ||||||
|       for (auto build : root["builds"].as<JsonArray>()) { |       for (auto build : root["builds"].as<JsonArray>()) { | ||||||
|         if (!build.containsKey("chipFamily")) { |         if (!build["chipFamily"].is<const char *>()) { | ||||||
|           ESP_LOGE(TAG, "Manifest does not contain required fields"); |           ESP_LOGE(TAG, "Manifest does not contain required fields"); | ||||||
|           return false; |           return false; | ||||||
|         } |         } | ||||||
|         if (build["chipFamily"] == ESPHOME_VARIANT) { |         if (build["chipFamily"] == ESPHOME_VARIANT) { | ||||||
|           if (!build.containsKey("ota")) { |           if (!build["ota"].is<JsonObject>()) { | ||||||
|             ESP_LOGE(TAG, "Manifest does not contain required fields"); |             ESP_LOGE(TAG, "Manifest does not contain required fields"); | ||||||
|             return false; |             return false; | ||||||
|           } |           } | ||||||
|           auto ota = build["ota"]; |           JsonObject ota = build["ota"].as<JsonObject>(); | ||||||
|           if (!ota.containsKey("path") || !ota.containsKey("md5")) { |           if (!ota["path"].is<const char *>() || !ota["md5"].is<const char *>()) { | ||||||
|             ESP_LOGE(TAG, "Manifest does not contain required fields"); |             ESP_LOGE(TAG, "Manifest does not contain required fields"); | ||||||
|             return false; |             return false; | ||||||
|           } |           } | ||||||
|           this_update->update_info_.firmware_url = ota["path"].as<std::string>(); |           this_update->update_info_.firmware_url = ota["path"].as<std::string>(); | ||||||
|           this_update->update_info_.md5 = ota["md5"].as<std::string>(); |           this_update->update_info_.md5 = ota["md5"].as<std::string>(); | ||||||
|  |  | ||||||
|           if (ota.containsKey("summary")) |           if (ota["summary"].is<const char *>()) | ||||||
|             this_update->update_info_.summary = ota["summary"].as<std::string>(); |             this_update->update_info_.summary = ota["summary"].as<std::string>(); | ||||||
|           if (ota.containsKey("release_url")) |           if (ota["release_url"].is<const char *>()) | ||||||
|             this_update->update_info_.release_url = ota["release_url"].as<std::string>(); |             this_update->update_info_.release_url = ota["release_url"].as<std::string>(); | ||||||
|  |  | ||||||
|           return true; |           return true; | ||||||
|   | |||||||
| @@ -12,6 +12,6 @@ CONFIG_SCHEMA = cv.All( | |||||||
|  |  | ||||||
| @coroutine_with_priority(1.0) | @coroutine_with_priority(1.0) | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     cg.add_library("bblanchon/ArduinoJson", "6.18.5") |     cg.add_library("bblanchon/ArduinoJson", "7.4.2") | ||||||
|     cg.add_define("USE_JSON") |     cg.add_define("USE_JSON") | ||||||
|     cg.add_global(json_ns.using) |     cg.add_global(json_ns.using) | ||||||
|   | |||||||
| @@ -1,83 +1,76 @@ | |||||||
| #include "json_util.h" | #include "json_util.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | // ArduinoJson::Allocator is included via ArduinoJson.h in json_util.h | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace json { | namespace json { | ||||||
|  |  | ||||||
| static const char *const TAG = "json"; | static const char *const TAG = "json"; | ||||||
|  |  | ||||||
| static std::vector<char> global_json_build_buffer;  // NOLINT | // Build an allocator for the JSON Library using the RAMAllocator class | ||||||
| static const auto ALLOCATOR = RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::ALLOC_INTERNAL); | struct SpiRamAllocator : ArduinoJson::Allocator { | ||||||
|  |   void *allocate(size_t size) override { return this->allocator_.allocate(size); } | ||||||
|  |  | ||||||
|  |   void deallocate(void *pointer) override { | ||||||
|  |     // ArduinoJson's Allocator interface doesn't provide the size parameter in deallocate. | ||||||
|  |     // RAMAllocator::deallocate() requires the size, which we don't have access to here. | ||||||
|  |     // RAMAllocator::deallocate implementation just calls free() regardless of whether | ||||||
|  |     // the memory was allocated with heap_caps_malloc or malloc. | ||||||
|  |     // This is safe because ESP-IDF's heap implementation internally tracks the memory region | ||||||
|  |     // and routes free() to the appropriate heap. | ||||||
|  |     free(pointer);  // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc) | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   void *reallocate(void *ptr, size_t new_size) override { | ||||||
|  |     return this->allocator_.reallocate(static_cast<uint8_t *>(ptr), new_size); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   RAMAllocator<uint8_t> allocator_{RAMAllocator<uint8_t>(RAMAllocator<uint8_t>::NONE)}; | ||||||
|  | }; | ||||||
|  |  | ||||||
| std::string build_json(const json_build_t &f) { | std::string build_json(const json_build_t &f) { | ||||||
|   // Here we are allocating up to 5kb of memory, |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   // with the heap size minus 2kb to be safe if less than 5kb |   auto doc_allocator = SpiRamAllocator(); | ||||||
|   // as we can not have a true dynamic sized document. |   JsonDocument json_document(&doc_allocator); | ||||||
|   // The excess memory is freed below with `shrinkToFit()` |   if (json_document.overflowed()) { | ||||||
|   auto free_heap = ALLOCATOR.get_max_free_block_size(); |     ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); | ||||||
|   size_t request_size = std::min(free_heap, (size_t) 512); |  | ||||||
|   while (true) { |  | ||||||
|     ESP_LOGV(TAG, "Attempting to allocate %zu bytes for JSON serialization", request_size); |  | ||||||
|     DynamicJsonDocument json_document(request_size); |  | ||||||
|     if (json_document.capacity() == 0) { |  | ||||||
|       ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, largest free heap block: %zu bytes", |  | ||||||
|                request_size, free_heap); |  | ||||||
|     return "{}"; |     return "{}"; | ||||||
|   } |   } | ||||||
|   JsonObject root = json_document.to<JsonObject>(); |   JsonObject root = json_document.to<JsonObject>(); | ||||||
|   f(root); |   f(root); | ||||||
|   if (json_document.overflowed()) { |   if (json_document.overflowed()) { | ||||||
|       if (request_size == free_heap) { |     ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); | ||||||
|         ESP_LOGE(TAG, "Could not allocate memory for document! Overflowed largest free heap block: %zu bytes", |  | ||||||
|                  free_heap); |  | ||||||
|     return "{}"; |     return "{}"; | ||||||
|   } |   } | ||||||
|       request_size = std::min(request_size * 2, free_heap); |  | ||||||
|       continue; |  | ||||||
|     } |  | ||||||
|     json_document.shrinkToFit(); |  | ||||||
|     ESP_LOGV(TAG, "Size after shrink %zu bytes", json_document.capacity()); |  | ||||||
|   std::string output; |   std::string output; | ||||||
|   serializeJson(json_document, output); |   serializeJson(json_document, output); | ||||||
|   return output; |   return output; | ||||||
|   } |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
| } | } | ||||||
|  |  | ||||||
| bool parse_json(const std::string &data, const json_parse_t &f) { | bool parse_json(const std::string &data, const json_parse_t &f) { | ||||||
|   // Here we are allocating 1.5 times the data size, |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   // with the heap size minus 2kb to be safe if less than that |   auto doc_allocator = SpiRamAllocator(); | ||||||
|   // as we can not have a true dynamic sized document. |   JsonDocument json_document(&doc_allocator); | ||||||
|   // The excess memory is freed below with `shrinkToFit()` |   if (json_document.overflowed()) { | ||||||
|   auto free_heap = ALLOCATOR.get_max_free_block_size(); |     ESP_LOGE(TAG, "Could not allocate memory for JSON document!"); | ||||||
|   size_t request_size = std::min(free_heap, (size_t) (data.size() * 1.5)); |  | ||||||
|   while (true) { |  | ||||||
|     DynamicJsonDocument json_document(request_size); |  | ||||||
|     if (json_document.capacity() == 0) { |  | ||||||
|       ESP_LOGE(TAG, "Could not allocate memory for document! Requested %zu bytes, free heap: %zu", request_size, |  | ||||||
|                free_heap); |  | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|   DeserializationError err = deserializeJson(json_document, data); |   DeserializationError err = deserializeJson(json_document, data); | ||||||
|     json_document.shrinkToFit(); |  | ||||||
|  |  | ||||||
|   JsonObject root = json_document.as<JsonObject>(); |   JsonObject root = json_document.as<JsonObject>(); | ||||||
|  |  | ||||||
|   if (err == DeserializationError::Ok) { |   if (err == DeserializationError::Ok) { | ||||||
|     return f(root); |     return f(root); | ||||||
|   } else if (err == DeserializationError::NoMemory) { |   } else if (err == DeserializationError::NoMemory) { | ||||||
|       if (request_size * 2 >= free_heap) { |  | ||||||
|     ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); |     ESP_LOGE(TAG, "Can not allocate more memory for deserialization. Consider making source string smaller"); | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|       ESP_LOGV(TAG, "Increasing memory allocation."); |  | ||||||
|       request_size *= 2; |  | ||||||
|       continue; |  | ||||||
|     } else { |  | ||||||
|   ESP_LOGE(TAG, "Parse error: %s", err.c_str()); |   ESP_LOGE(TAG, "Parse error: %s", err.c_str()); | ||||||
|   return false; |   return false; | ||||||
|     } |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
|   }; |  | ||||||
|   return false; |  | ||||||
| } | } | ||||||
|  |  | ||||||
| }  // namespace json | }  // namespace json | ||||||
|   | |||||||
| @@ -9,6 +9,7 @@ namespace light { | |||||||
| // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema | // See https://www.home-assistant.io/integrations/light.mqtt/#json-schema for documentation on the schema | ||||||
|  |  | ||||||
| void LightJSONSchema::dump_json(LightState &state, JsonObject root) { | void LightJSONSchema::dump_json(LightState &state, JsonObject root) { | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   if (state.supports_effects()) |   if (state.supports_effects()) | ||||||
|     root["effect"] = state.get_effect_name(); |     root["effect"] = state.get_effect_name(); | ||||||
|  |  | ||||||
| @@ -52,7 +53,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) { | |||||||
|   if (values.get_color_mode() & ColorCapability::BRIGHTNESS) |   if (values.get_color_mode() & ColorCapability::BRIGHTNESS) | ||||||
|     root["brightness"] = uint8_t(values.get_brightness() * 255); |     root["brightness"] = uint8_t(values.get_brightness() * 255); | ||||||
|  |  | ||||||
|   JsonObject color = root.createNestedObject("color"); |   JsonObject color = root["color"].to<JsonObject>(); | ||||||
|   if (values.get_color_mode() & ColorCapability::RGB) { |   if (values.get_color_mode() & ColorCapability::RGB) { | ||||||
|     color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255); |     color["r"] = uint8_t(values.get_color_brightness() * values.get_red() * 255); | ||||||
|     color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255); |     color["g"] = uint8_t(values.get_color_brightness() * values.get_green() * 255); | ||||||
| @@ -73,7 +74,7 @@ void LightJSONSchema::dump_json(LightState &state, JsonObject root) { | |||||||
| } | } | ||||||
|  |  | ||||||
| void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) { | void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonObject root) { | ||||||
|   if (root.containsKey("state")) { |   if (root["state"].is<const char *>()) { | ||||||
|     auto val = parse_on_off(root["state"]); |     auto val = parse_on_off(root["state"]); | ||||||
|     switch (val) { |     switch (val) { | ||||||
|       case PARSE_ON: |       case PARSE_ON: | ||||||
| @@ -90,40 +91,40 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (root.containsKey("brightness")) { |   if (root["brightness"].is<uint8_t>()) { | ||||||
|     call.set_brightness(float(root["brightness"]) / 255.0f); |     call.set_brightness(float(root["brightness"]) / 255.0f); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (root.containsKey("color")) { |   if (root["color"].is<JsonObject>()) { | ||||||
|     JsonObject color = root["color"]; |     JsonObject color = root["color"]; | ||||||
|     // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. |     // HA also encodes brightness information in the r, g, b values, so extract that and set it as color brightness. | ||||||
|     float max_rgb = 0.0f; |     float max_rgb = 0.0f; | ||||||
|     if (color.containsKey("r")) { |     if (color["r"].is<uint8_t>()) { | ||||||
|       float r = float(color["r"]) / 255.0f; |       float r = float(color["r"]) / 255.0f; | ||||||
|       max_rgb = fmaxf(max_rgb, r); |       max_rgb = fmaxf(max_rgb, r); | ||||||
|       call.set_red(r); |       call.set_red(r); | ||||||
|     } |     } | ||||||
|     if (color.containsKey("g")) { |     if (color["g"].is<uint8_t>()) { | ||||||
|       float g = float(color["g"]) / 255.0f; |       float g = float(color["g"]) / 255.0f; | ||||||
|       max_rgb = fmaxf(max_rgb, g); |       max_rgb = fmaxf(max_rgb, g); | ||||||
|       call.set_green(g); |       call.set_green(g); | ||||||
|     } |     } | ||||||
|     if (color.containsKey("b")) { |     if (color["b"].is<uint8_t>()) { | ||||||
|       float b = float(color["b"]) / 255.0f; |       float b = float(color["b"]) / 255.0f; | ||||||
|       max_rgb = fmaxf(max_rgb, b); |       max_rgb = fmaxf(max_rgb, b); | ||||||
|       call.set_blue(b); |       call.set_blue(b); | ||||||
|     } |     } | ||||||
|     if (color.containsKey("r") || color.containsKey("g") || color.containsKey("b")) { |     if (color["r"].is<uint8_t>() || color["g"].is<uint8_t>() || color["b"].is<uint8_t>()) { | ||||||
|       call.set_color_brightness(max_rgb); |       call.set_color_brightness(max_rgb); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (color.containsKey("c")) { |     if (color["c"].is<uint8_t>()) { | ||||||
|       call.set_cold_white(float(color["c"]) / 255.0f); |       call.set_cold_white(float(color["c"]) / 255.0f); | ||||||
|     } |     } | ||||||
|     if (color.containsKey("w")) { |     if (color["w"].is<uint8_t>()) { | ||||||
|       // the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm |       // the HA scheme is ambiguous here, the same key is used for white channel in RGBW and warm | ||||||
|       // white channel in RGBWW. |       // white channel in RGBWW. | ||||||
|       if (color.containsKey("c")) { |       if (color["c"].is<uint8_t>()) { | ||||||
|         call.set_warm_white(float(color["w"]) / 255.0f); |         call.set_warm_white(float(color["w"]) / 255.0f); | ||||||
|       } else { |       } else { | ||||||
|         call.set_white(float(color["w"]) / 255.0f); |         call.set_white(float(color["w"]) / 255.0f); | ||||||
| @@ -131,11 +132,11 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (root.containsKey("white_value")) {  // legacy API |   if (root["white_value"].is<uint8_t>()) {  // legacy API | ||||||
|     call.set_white(float(root["white_value"]) / 255.0f); |     call.set_white(float(root["white_value"]) / 255.0f); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (root.containsKey("color_temp")) { |   if (root["color_temp"].is<uint16_t>()) { | ||||||
|     call.set_color_temperature(float(root["color_temp"])); |     call.set_color_temperature(float(root["color_temp"])); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -143,17 +144,17 @@ void LightJSONSchema::parse_color_json(LightState &state, LightCall &call, JsonO | |||||||
| void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) { | void LightJSONSchema::parse_json(LightState &state, LightCall &call, JsonObject root) { | ||||||
|   LightJSONSchema::parse_color_json(state, call, root); |   LightJSONSchema::parse_color_json(state, call, root); | ||||||
|  |  | ||||||
|   if (root.containsKey("flash")) { |   if (root["flash"].is<uint32_t>()) { | ||||||
|     auto length = uint32_t(float(root["flash"]) * 1000); |     auto length = uint32_t(float(root["flash"]) * 1000); | ||||||
|     call.set_flash_length(length); |     call.set_flash_length(length); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (root.containsKey("transition")) { |   if (root["transition"].is<uint16_t>()) { | ||||||
|     auto length = uint32_t(float(root["transition"]) * 1000); |     auto length = uint32_t(float(root["transition"]) * 1000); | ||||||
|     call.set_transition_length(length); |     call.set_transition_length(length); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (root.containsKey("effect")) { |   if (root["effect"].is<const char *>()) { | ||||||
|     const char *effect = root["effect"]; |     const char *effect = root["effect"]; | ||||||
|     call.set_effect(effect); |     call.set_effect(effect); | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -55,7 +55,8 @@ void MQTTAlarmControlPanelComponent::dump_config() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTAlarmControlPanelComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   JsonArray supported_features = root.createNestedArray(MQTT_SUPPORTED_FEATURES); |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   JsonArray supported_features = root[MQTT_SUPPORTED_FEATURES].to<JsonArray>(); | ||||||
|   const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features(); |   const uint32_t acp_supported_features = this->alarm_control_panel_->get_supported_features(); | ||||||
|   if (acp_supported_features & ACP_FEAT_ARM_AWAY) { |   if (acp_supported_features & ACP_FEAT_ARM_AWAY) { | ||||||
|     supported_features.add("arm_away"); |     supported_features.add("arm_away"); | ||||||
|   | |||||||
| @@ -30,6 +30,7 @@ MQTTBinarySensorComponent::MQTTBinarySensorComponent(binary_sensor::BinarySensor | |||||||
| } | } | ||||||
|  |  | ||||||
| void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTBinarySensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   if (!this->binary_sensor_->get_device_class().empty()) |   if (!this->binary_sensor_->get_device_class().empty()) | ||||||
|     root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); |     root[MQTT_DEVICE_CLASS] = this->binary_sensor_->get_device_class(); | ||||||
|   if (this->binary_sensor_->is_status_binary_sensor()) |   if (this->binary_sensor_->is_status_binary_sensor()) | ||||||
|   | |||||||
| @@ -31,10 +31,13 @@ void MQTTButtonComponent::dump_config() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTButtonComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   config.state_topic = false; |   config.state_topic = false; | ||||||
|   if (!this->button_->get_device_class().empty()) |   if (!this->button_->get_device_class().empty()) { | ||||||
|     root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); |     root[MQTT_DEVICE_CLASS] = this->button_->get_device_class(); | ||||||
|   } |   } | ||||||
|  |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
|  | } | ||||||
|  |  | ||||||
| std::string MQTTButtonComponent::component_type() const { return "button"; } | std::string MQTTButtonComponent::component_type() const { return "button"; } | ||||||
| const EntityBase *MQTTButtonComponent::get_entity() const { return this->button_; } | const EntityBase *MQTTButtonComponent::get_entity() const { return this->button_; } | ||||||
|   | |||||||
| @@ -92,6 +92,7 @@ void MQTTClientComponent::send_device_info_() { | |||||||
|   std::string topic = "esphome/discover/"; |   std::string topic = "esphome/discover/"; | ||||||
|   topic.append(App.get_name()); |   topic.append(App.get_name()); | ||||||
|  |  | ||||||
|  |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   this->publish_json( |   this->publish_json( | ||||||
|       topic, |       topic, | ||||||
|       [](JsonObject root) { |       [](JsonObject root) { | ||||||
| @@ -147,6 +148,7 @@ void MQTTClientComponent::send_device_info_() { | |||||||
| #endif | #endif | ||||||
|       }, |       }, | ||||||
|       2, this->discovery_info_.retain); |       2, this->discovery_info_.retain); | ||||||
|  |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
| } | } | ||||||
|  |  | ||||||
| void MQTTClientComponent::dump_config() { | void MQTTClientComponent::dump_config() { | ||||||
|   | |||||||
| @@ -14,6 +14,7 @@ static const char *const TAG = "mqtt.climate"; | |||||||
| using namespace esphome::climate; | using namespace esphome::climate; | ||||||
|  |  | ||||||
| void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   auto traits = this->device_->get_traits(); |   auto traits = this->device_->get_traits(); | ||||||
|   // current_temperature_topic |   // current_temperature_topic | ||||||
|   if (traits.get_supports_current_temperature()) { |   if (traits.get_supports_current_temperature()) { | ||||||
| @@ -28,7 +29,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo | |||||||
|   // mode_state_topic |   // mode_state_topic | ||||||
|   root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic(); |   root[MQTT_MODE_STATE_TOPIC] = this->get_mode_state_topic(); | ||||||
|   // modes |   // modes | ||||||
|   JsonArray modes = root.createNestedArray(MQTT_MODES); |   JsonArray modes = root[MQTT_MODES].to<JsonArray>(); | ||||||
|   // sort array for nice UI in HA |   // sort array for nice UI in HA | ||||||
|   if (traits.supports_mode(CLIMATE_MODE_AUTO)) |   if (traits.supports_mode(CLIMATE_MODE_AUTO)) | ||||||
|     modes.add("auto"); |     modes.add("auto"); | ||||||
| @@ -89,7 +90,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo | |||||||
|     // preset_mode_state_topic |     // preset_mode_state_topic | ||||||
|     root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic(); |     root[MQTT_PRESET_MODE_STATE_TOPIC] = this->get_preset_state_topic(); | ||||||
|     // presets |     // presets | ||||||
|     JsonArray presets = root.createNestedArray("preset_modes"); |     JsonArray presets = root["preset_modes"].to<JsonArray>(); | ||||||
|     if (traits.supports_preset(CLIMATE_PRESET_HOME)) |     if (traits.supports_preset(CLIMATE_PRESET_HOME)) | ||||||
|       presets.add("home"); |       presets.add("home"); | ||||||
|     if (traits.supports_preset(CLIMATE_PRESET_AWAY)) |     if (traits.supports_preset(CLIMATE_PRESET_AWAY)) | ||||||
| @@ -119,7 +120,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo | |||||||
|     // fan_mode_state_topic |     // fan_mode_state_topic | ||||||
|     root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic(); |     root[MQTT_FAN_MODE_STATE_TOPIC] = this->get_fan_mode_state_topic(); | ||||||
|     // fan_modes |     // fan_modes | ||||||
|     JsonArray fan_modes = root.createNestedArray("fan_modes"); |     JsonArray fan_modes = root["fan_modes"].to<JsonArray>(); | ||||||
|     if (traits.supports_fan_mode(CLIMATE_FAN_ON)) |     if (traits.supports_fan_mode(CLIMATE_FAN_ON)) | ||||||
|       fan_modes.add("on"); |       fan_modes.add("on"); | ||||||
|     if (traits.supports_fan_mode(CLIMATE_FAN_OFF)) |     if (traits.supports_fan_mode(CLIMATE_FAN_OFF)) | ||||||
| @@ -150,7 +151,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo | |||||||
|     // swing_mode_state_topic |     // swing_mode_state_topic | ||||||
|     root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic(); |     root[MQTT_SWING_MODE_STATE_TOPIC] = this->get_swing_mode_state_topic(); | ||||||
|     // swing_modes |     // swing_modes | ||||||
|     JsonArray swing_modes = root.createNestedArray("swing_modes"); |     JsonArray swing_modes = root["swing_modes"].to<JsonArray>(); | ||||||
|     if (traits.supports_swing_mode(CLIMATE_SWING_OFF)) |     if (traits.supports_swing_mode(CLIMATE_SWING_OFF)) | ||||||
|       swing_modes.add("off"); |       swing_modes.add("off"); | ||||||
|     if (traits.supports_swing_mode(CLIMATE_SWING_BOTH)) |     if (traits.supports_swing_mode(CLIMATE_SWING_BOTH)) | ||||||
| @@ -163,6 +164,7 @@ void MQTTClimateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCo | |||||||
|  |  | ||||||
|   config.state_topic = false; |   config.state_topic = false; | ||||||
|   config.command_topic = false; |   config.command_topic = false; | ||||||
|  |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
| } | } | ||||||
| void MQTTClimateComponent::setup() { | void MQTTClimateComponent::setup() { | ||||||
|   auto traits = this->device_->get_traits(); |   auto traits = this->device_->get_traits(); | ||||||
|   | |||||||
| @@ -70,6 +70,7 @@ bool MQTTComponent::send_discovery_() { | |||||||
|  |  | ||||||
|   ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str()); |   ESP_LOGV(TAG, "'%s': Sending discovery", this->friendly_name().c_str()); | ||||||
|  |  | ||||||
|  |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   return global_mqtt_client->publish_json( |   return global_mqtt_client->publish_json( | ||||||
|       this->get_discovery_topic_(discovery_info), |       this->get_discovery_topic_(discovery_info), | ||||||
|       [this](JsonObject root) { |       [this](JsonObject root) { | ||||||
| @@ -155,7 +156,7 @@ bool MQTTComponent::send_discovery_() { | |||||||
|         } |         } | ||||||
|         std::string node_area = App.get_area(); |         std::string node_area = App.get_area(); | ||||||
|  |  | ||||||
|         JsonObject device_info = root.createNestedObject(MQTT_DEVICE); |         JsonObject device_info = root[MQTT_DEVICE].to<JsonObject>(); | ||||||
|         const auto mac = get_mac_address(); |         const auto mac = get_mac_address(); | ||||||
|         device_info[MQTT_DEVICE_IDENTIFIERS] = mac; |         device_info[MQTT_DEVICE_IDENTIFIERS] = mac; | ||||||
|         device_info[MQTT_DEVICE_NAME] = node_friendly_name; |         device_info[MQTT_DEVICE_NAME] = node_friendly_name; | ||||||
| @@ -192,6 +193,7 @@ bool MQTTComponent::send_discovery_() { | |||||||
|         device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac; |         device_info[MQTT_DEVICE_CONNECTIONS][0][1] = mac; | ||||||
|       }, |       }, | ||||||
|       this->qos_, discovery_info.retain); |       this->qos_, discovery_info.retain); | ||||||
|  |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
| } | } | ||||||
|  |  | ||||||
| uint8_t MQTTComponent::get_qos() const { return this->qos_; } | uint8_t MQTTComponent::get_qos() const { return this->qos_; } | ||||||
|   | |||||||
| @@ -67,6 +67,7 @@ void MQTTCoverComponent::dump_config() { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTCoverComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   if (!this->cover_->get_device_class().empty()) |   if (!this->cover_->get_device_class().empty()) | ||||||
|     root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); |     root[MQTT_DEVICE_CLASS] = this->cover_->get_device_class(); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,13 +20,13 @@ MQTTDateComponent::MQTTDateComponent(DateEntity *date) : date_(date) {} | |||||||
| void MQTTDateComponent::setup() { | void MQTTDateComponent::setup() { | ||||||
|   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { |   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { | ||||||
|     auto call = this->date_->make_call(); |     auto call = this->date_->make_call(); | ||||||
|     if (root.containsKey("year")) { |     if (root["year"].is<uint16_t>()) { | ||||||
|       call.set_year(root["year"]); |       call.set_year(root["year"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("month")) { |     if (root["month"].is<uint8_t>()) { | ||||||
|       call.set_month(root["month"]); |       call.set_month(root["month"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("day")) { |     if (root["day"].is<uint8_t>()) { | ||||||
|       call.set_day(root["day"]); |       call.set_day(root["day"]); | ||||||
|     } |     } | ||||||
|     call.perform(); |     call.perform(); | ||||||
| @@ -55,6 +55,7 @@ bool MQTTDateComponent::send_initial_state() { | |||||||
| } | } | ||||||
| bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) { | bool MQTTDateComponent::publish_state(uint16_t year, uint8_t month, uint8_t day) { | ||||||
|   return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) { |   return this->publish_json(this->get_state_topic_(), [year, month, day](JsonObject root) { | ||||||
|  |     // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|     root["year"] = year; |     root["year"] = year; | ||||||
|     root["month"] = month; |     root["month"] = month; | ||||||
|     root["day"] = day; |     root["day"] = day; | ||||||
|   | |||||||
| @@ -20,22 +20,22 @@ MQTTDateTimeComponent::MQTTDateTimeComponent(DateTimeEntity *datetime) : datetim | |||||||
| void MQTTDateTimeComponent::setup() { | void MQTTDateTimeComponent::setup() { | ||||||
|   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { |   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { | ||||||
|     auto call = this->datetime_->make_call(); |     auto call = this->datetime_->make_call(); | ||||||
|     if (root.containsKey("year")) { |     if (root["year"].is<uint16_t>()) { | ||||||
|       call.set_year(root["year"]); |       call.set_year(root["year"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("month")) { |     if (root["month"].is<uint8_t>()) { | ||||||
|       call.set_month(root["month"]); |       call.set_month(root["month"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("day")) { |     if (root["day"].is<uint8_t>()) { | ||||||
|       call.set_day(root["day"]); |       call.set_day(root["day"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("hour")) { |     if (root["hour"].is<uint8_t>()) { | ||||||
|       call.set_hour(root["hour"]); |       call.set_hour(root["hour"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("minute")) { |     if (root["minute"].is<uint8_t>()) { | ||||||
|       call.set_minute(root["minute"]); |       call.set_minute(root["minute"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("second")) { |     if (root["second"].is<uint8_t>()) { | ||||||
|       call.set_second(root["second"]); |       call.set_second(root["second"]); | ||||||
|     } |     } | ||||||
|     call.perform(); |     call.perform(); | ||||||
| @@ -68,6 +68,7 @@ bool MQTTDateTimeComponent::send_initial_state() { | |||||||
| bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, | bool MQTTDateTimeComponent::publish_state(uint16_t year, uint8_t month, uint8_t day, uint8_t hour, uint8_t minute, | ||||||
|                                           uint8_t second) { |                                           uint8_t second) { | ||||||
|   return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) { |   return this->publish_json(this->get_state_topic_(), [year, month, day, hour, minute, second](JsonObject root) { | ||||||
|  |     // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|     root["year"] = year; |     root["year"] = year; | ||||||
|     root["month"] = month; |     root["month"] = month; | ||||||
|     root["day"] = day; |     root["day"] = day; | ||||||
|   | |||||||
| @@ -16,7 +16,8 @@ using namespace esphome::event; | |||||||
| MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {} | MQTTEventComponent::MQTTEventComponent(event::Event *event) : event_(event) {} | ||||||
|  |  | ||||||
| void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTEventComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   JsonArray event_types = root.createNestedArray(MQTT_EVENT_TYPES); |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   JsonArray event_types = root[MQTT_EVENT_TYPES].to<JsonArray>(); | ||||||
|   for (const auto &event_type : this->event_->get_event_types()) |   for (const auto &event_type : this->event_->get_event_types()) | ||||||
|     event_types.add(event_type); |     event_types.add(event_type); | ||||||
|  |  | ||||||
| @@ -40,8 +41,10 @@ void MQTTEventComponent::dump_config() { | |||||||
| } | } | ||||||
|  |  | ||||||
| bool MQTTEventComponent::publish_event_(const std::string &event_type) { | bool MQTTEventComponent::publish_event_(const std::string &event_type) { | ||||||
|   return this->publish_json(this->get_state_topic_(), |   return this->publish_json(this->get_state_topic_(), [event_type](JsonObject root) { | ||||||
|                             [event_type](JsonObject root) { root[MQTT_EVENT_TYPE] = event_type; }); |     // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |     root[MQTT_EVENT_TYPE] = event_type; | ||||||
|  |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| std::string MQTTEventComponent::component_type() const { return "event"; } | std::string MQTTEventComponent::component_type() const { return "event"; } | ||||||
|   | |||||||
| @@ -143,6 +143,7 @@ void MQTTFanComponent::dump_config() { | |||||||
| bool MQTTFanComponent::send_initial_state() { return this->publish_state(); } | bool MQTTFanComponent::send_initial_state() { return this->publish_state(); } | ||||||
|  |  | ||||||
| void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTFanComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   if (this->state_->get_traits().supports_direction()) { |   if (this->state_->get_traits().supports_direction()) { | ||||||
|     root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic(); |     root[MQTT_DIRECTION_COMMAND_TOPIC] = this->get_direction_command_topic(); | ||||||
|     root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic(); |     root[MQTT_DIRECTION_STATE_TOPIC] = this->get_direction_state_topic(); | ||||||
|   | |||||||
| @@ -32,17 +32,21 @@ void MQTTJSONLightComponent::setup() { | |||||||
| MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {} | MQTTJSONLightComponent::MQTTJSONLightComponent(LightState *state) : state_(state) {} | ||||||
|  |  | ||||||
| bool MQTTJSONLightComponent::publish_state_() { | bool MQTTJSONLightComponent::publish_state_() { | ||||||
|   return this->publish_json(this->get_state_topic_(), |   return this->publish_json(this->get_state_topic_(), [this](JsonObject root) { | ||||||
|                             [this](JsonObject root) { LightJSONSchema::dump_json(*this->state_, root); }); |     // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |     LightJSONSchema::dump_json(*this->state_, root); | ||||||
|  |   }); | ||||||
| } | } | ||||||
| LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } | LightState *MQTTJSONLightComponent::get_state() const { return this->state_; } | ||||||
|  |  | ||||||
| void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   root["schema"] = "json"; |   root["schema"] = "json"; | ||||||
|   auto traits = this->state_->get_traits(); |   auto traits = this->state_->get_traits(); | ||||||
|  |  | ||||||
|   root[MQTT_COLOR_MODE] = true; |   root[MQTT_COLOR_MODE] = true; | ||||||
|   JsonArray color_modes = root.createNestedArray("supported_color_modes"); |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   JsonArray color_modes = root["supported_color_modes"].to<JsonArray>(); | ||||||
|   if (traits.supports_color_mode(ColorMode::ON_OFF)) |   if (traits.supports_color_mode(ColorMode::ON_OFF)) | ||||||
|     color_modes.add("onoff"); |     color_modes.add("onoff"); | ||||||
|   if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) |   if (traits.supports_color_mode(ColorMode::BRIGHTNESS)) | ||||||
| @@ -67,7 +71,7 @@ void MQTTJSONLightComponent::send_discovery(JsonObject root, mqtt::SendDiscovery | |||||||
|  |  | ||||||
|   if (this->state_->supports_effects()) { |   if (this->state_->supports_effects()) { | ||||||
|     root["effect"] = true; |     root["effect"] = true; | ||||||
|     JsonArray effect_list = root.createNestedArray(MQTT_EFFECT_LIST); |     JsonArray effect_list = root[MQTT_EFFECT_LIST].to<JsonArray>(); | ||||||
|     for (auto *effect : this->state_->get_effects()) |     for (auto *effect : this->state_->get_effects()) | ||||||
|       effect_list.add(effect->get_name()); |       effect_list.add(effect->get_name()); | ||||||
|     effect_list.add("None"); |     effect_list.add("None"); | ||||||
|   | |||||||
| @@ -38,8 +38,10 @@ void MQTTLockComponent::dump_config() { | |||||||
| std::string MQTTLockComponent::component_type() const { return "lock"; } | std::string MQTTLockComponent::component_type() const { return "lock"; } | ||||||
| const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } | const EntityBase *MQTTLockComponent::get_entity() const { return this->lock_; } | ||||||
| void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTLockComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   if (this->lock_->traits.get_assumed_state()) |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   if (this->lock_->traits.get_assumed_state()) { | ||||||
|     root[MQTT_OPTIMISTIC] = true; |     root[MQTT_OPTIMISTIC] = true; | ||||||
|  |   } | ||||||
|   if (this->lock_->traits.get_supports_open()) |   if (this->lock_->traits.get_supports_open()) | ||||||
|     root[MQTT_PAYLOAD_OPEN] = "OPEN"; |     root[MQTT_PAYLOAD_OPEN] = "OPEN"; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -40,6 +40,7 @@ const EntityBase *MQTTNumberComponent::get_entity() const { return this->number_ | |||||||
| void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTNumberComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   const auto &traits = number_->traits; |   const auto &traits = number_->traits; | ||||||
|   // https://www.home-assistant.io/integrations/number.mqtt/ |   // https://www.home-assistant.io/integrations/number.mqtt/ | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   root[MQTT_MIN] = traits.get_min_value(); |   root[MQTT_MIN] = traits.get_min_value(); | ||||||
|   root[MQTT_MAX] = traits.get_max_value(); |   root[MQTT_MAX] = traits.get_max_value(); | ||||||
|   root[MQTT_STEP] = traits.get_step(); |   root[MQTT_STEP] = traits.get_step(); | ||||||
|   | |||||||
| @@ -35,7 +35,8 @@ const EntityBase *MQTTSelectComponent::get_entity() const { return this->select_ | |||||||
| void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   const auto &traits = select_->traits; |   const auto &traits = select_->traits; | ||||||
|   // https://www.home-assistant.io/integrations/select.mqtt/ |   // https://www.home-assistant.io/integrations/select.mqtt/ | ||||||
|   JsonArray options = root.createNestedArray(MQTT_OPTIONS); |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   JsonArray options = root[MQTT_OPTIONS].to<JsonArray>(); | ||||||
|   for (const auto &option : traits.get_options()) |   for (const auto &option : traits.get_options()) | ||||||
|     options.add(option); |     options.add(option); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -44,8 +44,10 @@ void MQTTSensorComponent::set_expire_after(uint32_t expire_after) { this->expire | |||||||
| void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } | void MQTTSensorComponent::disable_expire_after() { this->expire_after_ = 0; } | ||||||
|  |  | ||||||
| void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTSensorComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   if (!this->sensor_->get_device_class().empty()) |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   if (!this->sensor_->get_device_class().empty()) { | ||||||
|     root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); |     root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (!this->sensor_->get_unit_of_measurement().empty()) |   if (!this->sensor_->get_unit_of_measurement().empty()) | ||||||
|     root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); |     root[MQTT_UNIT_OF_MEASUREMENT] = this->sensor_->get_unit_of_measurement(); | ||||||
|   | |||||||
| @@ -45,9 +45,11 @@ void MQTTSwitchComponent::dump_config() { | |||||||
| std::string MQTTSwitchComponent::component_type() const { return "switch"; } | std::string MQTTSwitchComponent::component_type() const { return "switch"; } | ||||||
| const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; } | const EntityBase *MQTTSwitchComponent::get_entity() const { return this->switch_; } | ||||||
| void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTSwitchComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   if (this->switch_->assumed_state()) |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   if (this->switch_->assumed_state()) { | ||||||
|     root[MQTT_OPTIMISTIC] = true; |     root[MQTT_OPTIMISTIC] = true; | ||||||
|   } |   } | ||||||
|  | } | ||||||
| bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); } | bool MQTTSwitchComponent::send_initial_state() { return this->publish_state(this->switch_->state); } | ||||||
|  |  | ||||||
| bool MQTTSwitchComponent::publish_state(bool state) { | bool MQTTSwitchComponent::publish_state(bool state) { | ||||||
|   | |||||||
| @@ -34,6 +34,7 @@ std::string MQTTTextComponent::component_type() const { return "text"; } | |||||||
| const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; } | const EntityBase *MQTTTextComponent::get_entity() const { return this->text_; } | ||||||
|  |  | ||||||
| void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTTextComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   switch (this->text_->traits.get_mode()) { |   switch (this->text_->traits.get_mode()) { | ||||||
|     case TEXT_MODE_TEXT: |     case TEXT_MODE_TEXT: | ||||||
|       root[MQTT_MODE] = "text"; |       root[MQTT_MODE] = "text"; | ||||||
|   | |||||||
| @@ -15,8 +15,10 @@ using namespace esphome::text_sensor; | |||||||
|  |  | ||||||
| MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} | MQTTTextSensor::MQTTTextSensor(TextSensor *sensor) : sensor_(sensor) {} | ||||||
| void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTTextSensor::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   if (!this->sensor_->get_device_class().empty()) |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   if (!this->sensor_->get_device_class().empty()) { | ||||||
|     root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); |     root[MQTT_DEVICE_CLASS] = this->sensor_->get_device_class(); | ||||||
|  |   } | ||||||
|   config.command_topic = false; |   config.command_topic = false; | ||||||
| } | } | ||||||
| void MQTTTextSensor::setup() { | void MQTTTextSensor::setup() { | ||||||
|   | |||||||
| @@ -20,13 +20,13 @@ MQTTTimeComponent::MQTTTimeComponent(TimeEntity *time) : time_(time) {} | |||||||
| void MQTTTimeComponent::setup() { | void MQTTTimeComponent::setup() { | ||||||
|   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { |   this->subscribe_json(this->get_command_topic_(), [this](const std::string &topic, JsonObject root) { | ||||||
|     auto call = this->time_->make_call(); |     auto call = this->time_->make_call(); | ||||||
|     if (root.containsKey("hour")) { |     if (root["hour"].is<uint8_t>()) { | ||||||
|       call.set_hour(root["hour"]); |       call.set_hour(root["hour"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("minute")) { |     if (root["minute"].is<uint8_t>()) { | ||||||
|       call.set_minute(root["minute"]); |       call.set_minute(root["minute"]); | ||||||
|     } |     } | ||||||
|     if (root.containsKey("second")) { |     if (root["second"].is<uint8_t>()) { | ||||||
|       call.set_second(root["second"]); |       call.set_second(root["second"]); | ||||||
|     } |     } | ||||||
|     call.perform(); |     call.perform(); | ||||||
| @@ -55,6 +55,7 @@ bool MQTTTimeComponent::send_initial_state() { | |||||||
| } | } | ||||||
| bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) { | bool MQTTTimeComponent::publish_state(uint8_t hour, uint8_t minute, uint8_t second) { | ||||||
|   return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) { |   return this->publish_json(this->get_state_topic_(), [hour, minute, second](JsonObject root) { | ||||||
|  |     // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|     root["hour"] = hour; |     root["hour"] = hour; | ||||||
|     root["minute"] = minute; |     root["minute"] = minute; | ||||||
|     root["second"] = second; |     root["second"] = second; | ||||||
|   | |||||||
| @@ -41,6 +41,7 @@ bool MQTTUpdateComponent::publish_state() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTUpdateComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|  |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   root["schema"] = "json"; |   root["schema"] = "json"; | ||||||
|   root[MQTT_PAYLOAD_INSTALL] = "INSTALL"; |   root[MQTT_PAYLOAD_INSTALL] = "INSTALL"; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -49,8 +49,10 @@ void MQTTValveComponent::dump_config() { | |||||||
|   } |   } | ||||||
| } | } | ||||||
| void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | void MQTTValveComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryConfig &config) { | ||||||
|   if (!this->valve_->get_device_class().empty()) |   // NOLINTNEXTLINE(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|  |   if (!this->valve_->get_device_class().empty()) { | ||||||
|     root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class(); |     root[MQTT_DEVICE_CLASS] = this->valve_->get_device_class(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   auto traits = this->valve_->get_traits(); |   auto traits = this->valve_->get_traits(); | ||||||
|   if (traits.get_is_assumed_state()) { |   if (traits.get_is_assumed_state()) { | ||||||
|   | |||||||
| @@ -356,7 +356,7 @@ void MS8607Component::read_humidity_(float temperature_float) { | |||||||
|  |  | ||||||
|   // map 16 bit humidity value into range [-6%, 118%] |   // map 16 bit humidity value into range [-6%, 118%] | ||||||
|   float const humidity_partial = double(humidity) / (1 << 16); |   float const humidity_partial = double(humidity) / (1 << 16); | ||||||
|   float const humidity_percentage = lerp(humidity_partial, -6.0, 118.0); |   float const humidity_percentage = std::lerp(-6.0, 118.0, humidity_partial); | ||||||
|   float const compensated_humidity_percentage = |   float const compensated_humidity_percentage = | ||||||
|       humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT; |       humidity_percentage + (20 - temperature_float) * MS8607_H_TEMP_COEFFICIENT; | ||||||
|   ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage); |   ESP_LOGD(TAG, "Compensated for temperature, humidity=%.2f%%", compensated_humidity_percentage); | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import logging | |||||||
|  |  | ||||||
| from esphome import automation | from esphome import automation | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| from esphome.components.const import CONF_REQUEST_HEADERS | from esphome.components.const import CONF_BYTE_ORDER, CONF_REQUEST_HEADERS | ||||||
| from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent | from esphome.components.http_request import CONF_HTTP_REQUEST_ID, HttpRequestComponent | ||||||
| from esphome.components.image import ( | from esphome.components.image import ( | ||||||
|     CONF_INVERT_ALPHA, |     CONF_INVERT_ALPHA, | ||||||
| @@ -11,6 +11,7 @@ from esphome.components.image import ( | |||||||
|     Image_, |     Image_, | ||||||
|     get_image_type_enum, |     get_image_type_enum, | ||||||
|     get_transparency_enum, |     get_transparency_enum, | ||||||
|  |     validate_settings, | ||||||
| ) | ) | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
| @@ -161,6 +162,7 @@ CONFIG_SCHEMA = cv.Schema( | |||||||
|             rp2040_arduino=cv.Version(0, 0, 0), |             rp2040_arduino=cv.Version(0, 0, 0), | ||||||
|             host=cv.Version(0, 0, 0), |             host=cv.Version(0, 0, 0), | ||||||
|         ), |         ), | ||||||
|  |         validate_settings, | ||||||
|     ) |     ) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -213,6 +215,7 @@ async def to_code(config): | |||||||
|         get_image_type_enum(config[CONF_TYPE]), |         get_image_type_enum(config[CONF_TYPE]), | ||||||
|         transparent, |         transparent, | ||||||
|         config[CONF_BUFFER_SIZE], |         config[CONF_BUFFER_SIZE], | ||||||
|  |         config.get(CONF_BYTE_ORDER) != "LITTLE_ENDIAN", | ||||||
|     ) |     ) | ||||||
|     await cg.register_component(var, config) |     await cg.register_component(var, config) | ||||||
|     await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) |     await cg.register_parented(var, config[CONF_HTTP_REQUEST_ID]) | ||||||
|   | |||||||
| @@ -35,14 +35,15 @@ inline bool is_color_on(const Color &color) { | |||||||
| } | } | ||||||
|  |  | ||||||
| OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, | OnlineImage::OnlineImage(const std::string &url, int width, int height, ImageFormat format, ImageType type, | ||||||
|                          image::Transparency transparency, uint32_t download_buffer_size) |                          image::Transparency transparency, uint32_t download_buffer_size, bool is_big_endian) | ||||||
|     : Image(nullptr, 0, 0, type, transparency), |     : Image(nullptr, 0, 0, type, transparency), | ||||||
|       buffer_(nullptr), |       buffer_(nullptr), | ||||||
|       download_buffer_(download_buffer_size), |       download_buffer_(download_buffer_size), | ||||||
|       download_buffer_initial_size_(download_buffer_size), |       download_buffer_initial_size_(download_buffer_size), | ||||||
|       format_(format), |       format_(format), | ||||||
|       fixed_width_(width), |       fixed_width_(width), | ||||||
|       fixed_height_(height) { |       fixed_height_(height), | ||||||
|  |       is_big_endian_(is_big_endian) { | ||||||
|   this->set_url(url); |   this->set_url(url); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -296,7 +297,7 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) { | |||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case ImageType::IMAGE_TYPE_GRAYSCALE: { |     case ImageType::IMAGE_TYPE_GRAYSCALE: { | ||||||
|       uint8_t gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); |       auto gray = static_cast<uint8_t>(0.2125 * color.r + 0.7154 * color.g + 0.0721 * color.b); | ||||||
|       if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { |       if (this->transparency_ == image::TRANSPARENCY_CHROMA_KEY) { | ||||||
|         if (gray == 1) { |         if (gray == 1) { | ||||||
|           gray = 0; |           gray = 0; | ||||||
| @@ -314,8 +315,13 @@ void OnlineImage::draw_pixel_(int x, int y, Color color) { | |||||||
|     case ImageType::IMAGE_TYPE_RGB565: { |     case ImageType::IMAGE_TYPE_RGB565: { | ||||||
|       this->map_chroma_key(color); |       this->map_chroma_key(color); | ||||||
|       uint16_t col565 = display::ColorUtil::color_to_565(color); |       uint16_t col565 = display::ColorUtil::color_to_565(color); | ||||||
|  |       if (this->is_big_endian_) { | ||||||
|         this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF); |         this->buffer_[pos + 0] = static_cast<uint8_t>((col565 >> 8) & 0xFF); | ||||||
|         this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF); |         this->buffer_[pos + 1] = static_cast<uint8_t>(col565 & 0xFF); | ||||||
|  |       } else { | ||||||
|  |         this->buffer_[pos + 0] = static_cast<uint8_t>(col565 & 0xFF); | ||||||
|  |         this->buffer_[pos + 1] = static_cast<uint8_t>((col565 >> 8) & 0xFF); | ||||||
|  |       } | ||||||
|       if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { |       if (this->transparency_ == image::TRANSPARENCY_ALPHA_CHANNEL) { | ||||||
|         this->buffer_[pos + 2] = color.w; |         this->buffer_[pos + 2] = color.w; | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -50,7 +50,7 @@ class OnlineImage : public PollingComponent, | |||||||
|    * @param buffer_size Size of the buffer used to download the image. |    * @param buffer_size Size of the buffer used to download the image. | ||||||
|    */ |    */ | ||||||
|   OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, |   OnlineImage(const std::string &url, int width, int height, ImageFormat format, image::ImageType type, | ||||||
|               image::Transparency transparency, uint32_t buffer_size); |               image::Transparency transparency, uint32_t buffer_size, bool is_big_endian); | ||||||
|  |  | ||||||
|   void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; |   void draw(int x, int y, display::Display *display, Color color_on, Color color_off) override; | ||||||
|  |  | ||||||
| @@ -164,6 +164,11 @@ class OnlineImage : public PollingComponent, | |||||||
|   const int fixed_width_; |   const int fixed_width_; | ||||||
|   /** height requested on configuration, or 0 if non specified. */ |   /** height requested on configuration, or 0 if non specified. */ | ||||||
|   const int fixed_height_; |   const int fixed_height_; | ||||||
|  |   /** | ||||||
|  |    * Whether the image is stored in big-endian format. | ||||||
|  |    * This is used to determine how to store 16 bit colors in the buffer. | ||||||
|  |    */ | ||||||
|  |   bool is_big_endian_; | ||||||
|   /** |   /** | ||||||
|    * Actual width of the current image. If fixed_width_ is specified, |    * Actual width of the current image. If fixed_width_ is specified, | ||||||
|    * this will be equal to it; otherwise it will be set once the decoding |    * this will be equal to it; otherwise it will be set once the decoding | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ void opentherm::OpenthermOutput::write_state(float state) { | |||||||
|   ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_); |   ESP_LOGD(TAG, "Received state: %.2f. Min value: %.2f, max value: %.2f", state, min_value_, max_value_); | ||||||
|   this->state = state < 0.003 && this->zero_means_zero_ |   this->state = state < 0.003 && this->zero_means_zero_ | ||||||
|                     ? 0.0 |                     ? 0.0 | ||||||
|                     : clamp(lerp(state, min_value_, max_value_), min_value_, max_value_); |                     : clamp(std::lerp(min_value_, max_value_, state), min_value_, max_value_); | ||||||
|   this->has_state_ = true; |   this->has_state_ = true; | ||||||
|   ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state); |   ESP_LOGD(TAG, "Output %s set to %.2f", this->id_, this->state); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -88,9 +88,9 @@ void Servo::internal_write(float value) { | |||||||
|   value = clamp(value, -1.0f, 1.0f); |   value = clamp(value, -1.0f, 1.0f); | ||||||
|   float level; |   float level; | ||||||
|   if (value < 0.0) { |   if (value < 0.0) { | ||||||
|     level = lerp(-value, this->idle_level_, this->min_level_); |     level = std::lerp(this->idle_level_, this->min_level_, -value); | ||||||
|   } else { |   } else { | ||||||
|     level = lerp(value, this->idle_level_, this->max_level_); |     level = std::lerp(this->idle_level_, this->max_level_, value); | ||||||
|   } |   } | ||||||
|   this->output_->set_level(level); |   this->output_->set_level(level); | ||||||
|   this->current_value_ = value; |   this->current_value_ = value; | ||||||
|   | |||||||
| @@ -792,7 +792,7 @@ std::string WebServer::light_json(light::LightState *obj, JsonDetail start_confi | |||||||
|  |  | ||||||
|     light::LightJSONSchema::dump_json(*obj, root); |     light::LightJSONSchema::dump_json(*obj, root); | ||||||
|     if (start_config == DETAIL_ALL) { |     if (start_config == DETAIL_ALL) { | ||||||
|       JsonArray opt = root.createNestedArray("effects"); |       JsonArray opt = root["effects"].to<JsonArray>(); | ||||||
|       opt.add("None"); |       opt.add("None"); | ||||||
|       for (auto const &option : obj->get_effects()) { |       for (auto const &option : obj->get_effects()) { | ||||||
|         opt.add(option->get_name()); |         opt.add(option->get_name()); | ||||||
| @@ -1238,7 +1238,7 @@ std::string WebServer::select_json(select::Select *obj, const std::string &value | |||||||
|   return json::build_json([this, obj, value, start_config](JsonObject root) { |   return json::build_json([this, obj, value, start_config](JsonObject root) { | ||||||
|     set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); |     set_json_icon_state_value(root, obj, "select-" + obj->get_object_id(), value, value, start_config); | ||||||
|     if (start_config == DETAIL_ALL) { |     if (start_config == DETAIL_ALL) { | ||||||
|       JsonArray opt = root.createNestedArray("option"); |       JsonArray opt = root["option"].to<JsonArray>(); | ||||||
|       for (auto &option : obj->traits.get_options()) { |       for (auto &option : obj->traits.get_options()) { | ||||||
|         opt.add(option); |         opt.add(option); | ||||||
|       } |       } | ||||||
| @@ -1322,6 +1322,7 @@ std::string WebServer::climate_all_json_generator(WebServer *web_server, void *s | |||||||
|   return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL); |   return web_server->climate_json((climate::Climate *) (source), DETAIL_ALL); | ||||||
| } | } | ||||||
| std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { | std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_config) { | ||||||
|  |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   return json::build_json([this, obj, start_config](JsonObject root) { |   return json::build_json([this, obj, start_config](JsonObject root) { | ||||||
|     set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); |     set_json_id(root, obj, "climate-" + obj->get_object_id(), start_config); | ||||||
|     const auto traits = obj->get_traits(); |     const auto traits = obj->get_traits(); | ||||||
| @@ -1330,32 +1331,32 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf | |||||||
|     char buf[16]; |     char buf[16]; | ||||||
|  |  | ||||||
|     if (start_config == DETAIL_ALL) { |     if (start_config == DETAIL_ALL) { | ||||||
|       JsonArray opt = root.createNestedArray("modes"); |       JsonArray opt = root["modes"].to<JsonArray>(); | ||||||
|       for (climate::ClimateMode m : traits.get_supported_modes()) |       for (climate::ClimateMode m : traits.get_supported_modes()) | ||||||
|         opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); |         opt.add(PSTR_LOCAL(climate::climate_mode_to_string(m))); | ||||||
|       if (!traits.get_supported_custom_fan_modes().empty()) { |       if (!traits.get_supported_custom_fan_modes().empty()) { | ||||||
|         JsonArray opt = root.createNestedArray("fan_modes"); |         JsonArray opt = root["fan_modes"].to<JsonArray>(); | ||||||
|         for (climate::ClimateFanMode m : traits.get_supported_fan_modes()) |         for (climate::ClimateFanMode m : traits.get_supported_fan_modes()) | ||||||
|           opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); |           opt.add(PSTR_LOCAL(climate::climate_fan_mode_to_string(m))); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (!traits.get_supported_custom_fan_modes().empty()) { |       if (!traits.get_supported_custom_fan_modes().empty()) { | ||||||
|         JsonArray opt = root.createNestedArray("custom_fan_modes"); |         JsonArray opt = root["custom_fan_modes"].to<JsonArray>(); | ||||||
|         for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) |         for (auto const &custom_fan_mode : traits.get_supported_custom_fan_modes()) | ||||||
|           opt.add(custom_fan_mode); |           opt.add(custom_fan_mode); | ||||||
|       } |       } | ||||||
|       if (traits.get_supports_swing_modes()) { |       if (traits.get_supports_swing_modes()) { | ||||||
|         JsonArray opt = root.createNestedArray("swing_modes"); |         JsonArray opt = root["swing_modes"].to<JsonArray>(); | ||||||
|         for (auto swing_mode : traits.get_supported_swing_modes()) |         for (auto swing_mode : traits.get_supported_swing_modes()) | ||||||
|           opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); |           opt.add(PSTR_LOCAL(climate::climate_swing_mode_to_string(swing_mode))); | ||||||
|       } |       } | ||||||
|       if (traits.get_supports_presets() && obj->preset.has_value()) { |       if (traits.get_supports_presets() && obj->preset.has_value()) { | ||||||
|         JsonArray opt = root.createNestedArray("presets"); |         JsonArray opt = root["presets"].to<JsonArray>(); | ||||||
|         for (climate::ClimatePreset m : traits.get_supported_presets()) |         for (climate::ClimatePreset m : traits.get_supported_presets()) | ||||||
|           opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); |           opt.add(PSTR_LOCAL(climate::climate_preset_to_string(m))); | ||||||
|       } |       } | ||||||
|       if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { |       if (!traits.get_supported_custom_presets().empty() && obj->custom_preset.has_value()) { | ||||||
|         JsonArray opt = root.createNestedArray("custom_presets"); |         JsonArray opt = root["custom_presets"].to<JsonArray>(); | ||||||
|         for (auto const &custom_preset : traits.get_supported_custom_presets()) |         for (auto const &custom_preset : traits.get_supported_custom_presets()) | ||||||
|           opt.add(custom_preset); |           opt.add(custom_preset); | ||||||
|       } |       } | ||||||
| @@ -1407,6 +1408,7 @@ std::string WebServer::climate_json(climate::Climate *obj, JsonDetail start_conf | |||||||
|         root["state"] = root["target_temperature"]; |         root["state"] = root["target_temperature"]; | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|  |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| @@ -1635,7 +1637,7 @@ std::string WebServer::event_json(event::Event *obj, const std::string &event_ty | |||||||
|       root["event_type"] = event_type; |       root["event_type"] = event_type; | ||||||
|     } |     } | ||||||
|     if (start_config == DETAIL_ALL) { |     if (start_config == DETAIL_ALL) { | ||||||
|       JsonArray event_types = root.createNestedArray("event_types"); |       JsonArray event_types = root["event_types"].to<JsonArray>(); | ||||||
|       for (auto const &event_type : obj->get_event_types()) { |       for (auto const &event_type : obj->get_event_types()) { | ||||||
|         event_types.add(event_type); |         event_types.add(event_type); | ||||||
|       } |       } | ||||||
| @@ -1682,6 +1684,7 @@ std::string WebServer::update_all_json_generator(WebServer *web_server, void *so | |||||||
|   return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); |   return web_server->update_json((update::UpdateEntity *) (source), DETAIL_STATE); | ||||||
| } | } | ||||||
| std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { | std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_config) { | ||||||
|  |   // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|   return json::build_json([this, obj, start_config](JsonObject root) { |   return json::build_json([this, obj, start_config](JsonObject root) { | ||||||
|     set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); |     set_json_id(root, obj, "update-" + obj->get_object_id(), start_config); | ||||||
|     root["value"] = obj->update_info.latest_version; |     root["value"] = obj->update_info.latest_version; | ||||||
| @@ -1707,6 +1710,7 @@ std::string WebServer::update_json(update::UpdateEntity *obj, JsonDetail start_c | |||||||
|       this->add_sorting_info_(root, obj); |       this->add_sorting_info_(root, obj); | ||||||
|     } |     } | ||||||
|   }); |   }); | ||||||
|  |   // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   | |||||||
| @@ -40,4 +40,4 @@ async def to_code(config): | |||||||
|         if CORE.is_esp8266: |         if CORE.is_esp8266: | ||||||
|             cg.add_library("ESP8266WiFi", None) |             cg.add_library("ESP8266WiFi", None) | ||||||
|         # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json |         # https://github.com/ESP32Async/ESPAsyncWebServer/blob/main/library.json | ||||||
|         cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.8") |         cg.add_library("ESP32Async/ESPAsyncWebServer", "3.7.10") | ||||||
|   | |||||||
| @@ -389,10 +389,12 @@ AsyncEventSourceResponse::AsyncEventSourceResponse(const AsyncWebServerRequest * | |||||||
|  |  | ||||||
| #ifdef USE_WEBSERVER_SORTING | #ifdef USE_WEBSERVER_SORTING | ||||||
|   for (auto &group : ws->sorting_groups_) { |   for (auto &group : ws->sorting_groups_) { | ||||||
|  |     // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson | ||||||
|     message = json::build_json([group](JsonObject root) { |     message = json::build_json([group](JsonObject root) { | ||||||
|       root["name"] = group.second.name; |       root["name"] = group.second.name; | ||||||
|       root["sorting_weight"] = group.second.weight; |       root["sorting_weight"] = group.second.weight; | ||||||
|     }); |     }); | ||||||
|  |     // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks) | ||||||
|  |  | ||||||
|     // a (very) large number of these should be able to be queued initially without defer |     // a (very) large number of these should be able to be queued initially without defer | ||||||
|     // since the only thing in the send buffer at this point is the initial ping/config |     // since the only thing in the send buffer at this point is the initial ping/config | ||||||
|   | |||||||
| @@ -4,7 +4,7 @@ from enum import Enum | |||||||
|  |  | ||||||
| from esphome.enum import StrEnum | from esphome.enum import StrEnum | ||||||
|  |  | ||||||
| __version__ = "2025.7.0b3" | __version__ = "2025.7.0b4" | ||||||
|  |  | ||||||
| ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" | ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" | ||||||
| VALID_SUBSTITUTIONS_CHARACTERS = ( | VALID_SUBSTITUTIONS_CHARACTERS = ( | ||||||
|   | |||||||
| @@ -264,6 +264,7 @@ void Component::set_retry(uint32_t initial_wait_time, uint8_t max_attempts, std: | |||||||
| bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } | bool Component::is_failed() const { return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED; } | ||||||
| bool Component::is_ready() const { | bool Component::is_ready() const { | ||||||
|   return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || |   return (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP || | ||||||
|  |          (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_LOOP_DONE || | ||||||
|          (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; |          (this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_SETUP; | ||||||
| } | } | ||||||
| bool Component::can_proceed() { return true; } | bool Component::can_proceed() { return true; } | ||||||
|   | |||||||
| @@ -78,6 +78,8 @@ def run_platformio_cli(*args, **kwargs) -> str | int: | |||||||
|     os.environ.setdefault( |     os.environ.setdefault( | ||||||
|         "PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path()) |         "PLATFORMIO_LIBDEPS_DIR", os.path.abspath(CORE.relative_piolibdeps_path()) | ||||||
|     ) |     ) | ||||||
|  |     # Suppress Python syntax warnings from third-party scripts during compilation | ||||||
|  |     os.environ.setdefault("PYTHONWARNINGS", "ignore::SyntaxWarning") | ||||||
|     cmd = ["platformio"] + list(args) |     cmd = ["platformio"] + list(args) | ||||||
|  |  | ||||||
|     if not CORE.verbose: |     if not CORE.verbose: | ||||||
|   | |||||||
| @@ -162,6 +162,9 @@ def get_ini_content(): | |||||||
|     # Sort to avoid changing build unflags order |     # Sort to avoid changing build unflags order | ||||||
|     CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags)) |     CORE.add_platformio_option("build_unflags", sorted(CORE.build_unflags)) | ||||||
|  |  | ||||||
|  |     # Add extra script for C++ flags | ||||||
|  |     CORE.add_platformio_option("extra_scripts", [f"pre:{CXX_FLAGS_FILE_NAME}"]) | ||||||
|  |  | ||||||
|     content = "[platformio]\n" |     content = "[platformio]\n" | ||||||
|     content += f"description = ESPHome {__version__}\n" |     content += f"description = ESPHome {__version__}\n" | ||||||
|  |  | ||||||
| @@ -222,6 +225,9 @@ def write_platformio_project(): | |||||||
|         write_gitignore() |         write_gitignore() | ||||||
|     write_platformio_ini(content) |     write_platformio_ini(content) | ||||||
|  |  | ||||||
|  |     # Write extra script for C++ specific flags | ||||||
|  |     write_cxx_flags_script() | ||||||
|  |  | ||||||
|  |  | ||||||
| DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ | DEFINES_H_FORMAT = ESPHOME_H_FORMAT = """\ | ||||||
| #pragma once | #pragma once | ||||||
| @@ -394,3 +400,20 @@ def write_gitignore(): | |||||||
|     if not os.path.isfile(path): |     if not os.path.isfile(path): | ||||||
|         with open(file=path, mode="w", encoding="utf-8") as f: |         with open(file=path, mode="w", encoding="utf-8") as f: | ||||||
|             f.write(GITIGNORE_CONTENT) |             f.write(GITIGNORE_CONTENT) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | CXX_FLAGS_FILE_NAME = "cxx_flags.py" | ||||||
|  | CXX_FLAGS_FILE_CONTENTS = """# Auto-generated ESPHome script for C++ specific compiler flags | ||||||
|  | Import("env") | ||||||
|  |  | ||||||
|  | # Add C++ specific flags | ||||||
|  | """ | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def write_cxx_flags_script() -> None: | ||||||
|  |     path = CORE.relative_build_path(CXX_FLAGS_FILE_NAME) | ||||||
|  |     contents = CXX_FLAGS_FILE_CONTENTS | ||||||
|  |     if not CORE.is_host: | ||||||
|  |         contents += 'env.Append(CXXFLAGS=["-Wno-volatile"])' | ||||||
|  |         contents += "\n" | ||||||
|  |     write_file_if_changed(path, contents) | ||||||
|   | |||||||
| @@ -35,7 +35,7 @@ build_flags = | |||||||
| lib_deps = | lib_deps = | ||||||
|     esphome/noise-c@0.1.10                  ; api |     esphome/noise-c@0.1.10                  ; api | ||||||
|     improv/Improv@1.2.4                    ; improv_serial / esp32_improv |     improv/Improv@1.2.4                    ; improv_serial / esp32_improv | ||||||
|     bblanchon/ArduinoJson@6.18.5           ; json |     bblanchon/ArduinoJson@7.4.2            ; json | ||||||
|     wjtje/qr-code-generator-library@1.7.0  ; qr_code |     wjtje/qr-code-generator-library@1.7.0  ; qr_code | ||||||
|     functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 |     functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 | ||||||
|     pavlodn/HaierProtocol@0.9.31           ; haier |     pavlodn/HaierProtocol@0.9.31           ; haier | ||||||
| @@ -235,7 +235,7 @@ build_flags = | |||||||
|     -DUSE_ZEPHYR |     -DUSE_ZEPHYR | ||||||
|     -DUSE_NRF52 |     -DUSE_NRF52 | ||||||
| lib_deps = | lib_deps = | ||||||
|     bblanchon/ArduinoJson@7.0.0           ; json |     bblanchon/ArduinoJson@7.4.2           ; json | ||||||
|     wjtje/qr-code-generator-library@1.7.0  ; qr_code |     wjtje/qr-code-generator-library@1.7.0  ; qr_code | ||||||
|     pavlodn/HaierProtocol@0.9.31           ; haier |     pavlodn/HaierProtocol@0.9.31           ; haier | ||||||
|     functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 |     functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								tests/components/captive_portal/test.bk72xx-ard.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								tests/components/captive_portal/test.bk72xx-ard.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | |||||||
|  | <<: !include common.yaml | ||||||
| @@ -1,7 +1,7 @@ | |||||||
| from esphome import automation | from esphome import automation | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME | from esphome.const import CONF_COMPONENTS, CONF_ID, CONF_NAME, CONF_UPDATE_INTERVAL | ||||||
|  |  | ||||||
| CODEOWNERS = ["@esphome/tests"] | CODEOWNERS = ["@esphome/tests"] | ||||||
|  |  | ||||||
| @@ -10,10 +10,15 @@ LoopTestComponent = loop_test_component_ns.class_("LoopTestComponent", cg.Compon | |||||||
| LoopTestISRComponent = loop_test_component_ns.class_( | LoopTestISRComponent = loop_test_component_ns.class_( | ||||||
|     "LoopTestISRComponent", cg.Component |     "LoopTestISRComponent", cg.Component | ||||||
| ) | ) | ||||||
|  | LoopTestUpdateComponent = loop_test_component_ns.class_( | ||||||
|  |     "LoopTestUpdateComponent", cg.PollingComponent | ||||||
|  | ) | ||||||
|  |  | ||||||
| CONF_DISABLE_AFTER = "disable_after" | CONF_DISABLE_AFTER = "disable_after" | ||||||
| CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations" | CONF_TEST_REDUNDANT_OPERATIONS = "test_redundant_operations" | ||||||
| CONF_ISR_COMPONENTS = "isr_components" | CONF_ISR_COMPONENTS = "isr_components" | ||||||
|  | CONF_UPDATE_COMPONENTS = "update_components" | ||||||
|  | CONF_DISABLE_LOOP_AFTER = "disable_loop_after" | ||||||
|  |  | ||||||
| COMPONENT_CONFIG_SCHEMA = cv.Schema( | COMPONENT_CONFIG_SCHEMA = cv.Schema( | ||||||
|     { |     { | ||||||
| @@ -31,11 +36,23 @@ ISR_COMPONENT_CONFIG_SCHEMA = cv.Schema( | |||||||
|     } |     } | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  | UPDATE_COMPONENT_CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(LoopTestUpdateComponent), | ||||||
|  |         cv.Required(CONF_NAME): cv.string, | ||||||
|  |         cv.Optional(CONF_DISABLE_LOOP_AFTER, default=0): cv.int_, | ||||||
|  |         cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval, | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.Schema( | CONFIG_SCHEMA = cv.Schema( | ||||||
|     { |     { | ||||||
|         cv.GenerateID(): cv.declare_id(LoopTestComponent), |         cv.GenerateID(): cv.declare_id(LoopTestComponent), | ||||||
|         cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA), |         cv.Required(CONF_COMPONENTS): cv.ensure_list(COMPONENT_CONFIG_SCHEMA), | ||||||
|         cv.Optional(CONF_ISR_COMPONENTS): cv.ensure_list(ISR_COMPONENT_CONFIG_SCHEMA), |         cv.Optional(CONF_ISR_COMPONENTS): cv.ensure_list(ISR_COMPONENT_CONFIG_SCHEMA), | ||||||
|  |         cv.Optional(CONF_UPDATE_COMPONENTS): cv.ensure_list( | ||||||
|  |             UPDATE_COMPONENT_CONFIG_SCHEMA | ||||||
|  |         ), | ||||||
|     } |     } | ||||||
| ).extend(cv.COMPONENT_SCHEMA) | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
| @@ -94,3 +111,12 @@ async def to_code(config): | |||||||
|         var = cg.new_Pvariable(isr_config[CONF_ID]) |         var = cg.new_Pvariable(isr_config[CONF_ID]) | ||||||
|         await cg.register_component(var, isr_config) |         await cg.register_component(var, isr_config) | ||||||
|         cg.add(var.set_name(isr_config[CONF_NAME])) |         cg.add(var.set_name(isr_config[CONF_NAME])) | ||||||
|  |  | ||||||
|  |     # Create update test components | ||||||
|  |     for update_config in config.get(CONF_UPDATE_COMPONENTS, []): | ||||||
|  |         var = cg.new_Pvariable(update_config[CONF_ID]) | ||||||
|  |         await cg.register_component(var, update_config) | ||||||
|  |  | ||||||
|  |         cg.add(var.set_name(update_config[CONF_NAME])) | ||||||
|  |         cg.add(var.set_disable_loop_after(update_config[CONF_DISABLE_LOOP_AFTER])) | ||||||
|  |         cg.add(var.set_update_interval(update_config[CONF_UPDATE_INTERVAL])) | ||||||
|   | |||||||
| @@ -39,5 +39,29 @@ void LoopTestComponent::service_disable() { | |||||||
|   this->disable_loop(); |   this->disable_loop(); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // LoopTestUpdateComponent implementation | ||||||
|  | void LoopTestUpdateComponent::setup() { | ||||||
|  |   ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent setup called", this->name_.c_str()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void LoopTestUpdateComponent::loop() { | ||||||
|  |   this->loop_count_++; | ||||||
|  |   ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent loop count: %d", this->name_.c_str(), this->loop_count_); | ||||||
|  |  | ||||||
|  |   // Disable loop after specified count to test component.update when loop is disabled | ||||||
|  |   if (this->disable_loop_after_ > 0 && this->loop_count_ == this->disable_loop_after_) { | ||||||
|  |     ESP_LOGI(TAG, "[%s] Disabling loop after %d iterations", this->name_.c_str(), this->disable_loop_after_); | ||||||
|  |     this->disable_loop(); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void LoopTestUpdateComponent::update() { | ||||||
|  |   this->update_count_++; | ||||||
|  |   // Check if loop is disabled by testing component state | ||||||
|  |   bool loop_disabled = this->component_state_ == COMPONENT_STATE_LOOP_DONE; | ||||||
|  |   ESP_LOGI(TAG, "[%s] LoopTestUpdateComponent update() called, count: %d, loop_disabled: %s", this->name_.c_str(), | ||||||
|  |            this->update_count_, loop_disabled ? "YES" : "NO"); | ||||||
|  | } | ||||||
|  |  | ||||||
| }  // namespace loop_test_component | }  // namespace loop_test_component | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ | |||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include "esphome/core/application.h" | #include "esphome/core/application.h" | ||||||
| #include "esphome/core/automation.h" | #include "esphome/core/automation.h" | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace loop_test_component { | namespace loop_test_component { | ||||||
| @@ -54,5 +55,29 @@ template<typename... Ts> class DisableAction : public Action<Ts...> { | |||||||
|   LoopTestComponent *parent_; |   LoopTestComponent *parent_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | // Component with update() method to test component.update action | ||||||
|  | class LoopTestUpdateComponent : public PollingComponent { | ||||||
|  |  public: | ||||||
|  |   LoopTestUpdateComponent() : PollingComponent(1000) {}  // Default 1s update interval | ||||||
|  |  | ||||||
|  |   void set_name(const std::string &name) { this->name_ = name; } | ||||||
|  |   void set_disable_loop_after(int count) { this->disable_loop_after_ = count; } | ||||||
|  |  | ||||||
|  |   void setup() override; | ||||||
|  |   void loop() override; | ||||||
|  |   void update() override; | ||||||
|  |  | ||||||
|  |   int get_update_count() const { return this->update_count_; } | ||||||
|  |   int get_loop_count() const { return this->loop_count_; } | ||||||
|  |  | ||||||
|  |   float get_setup_priority() const override { return setup_priority::DATA; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   std::string name_; | ||||||
|  |   int loop_count_{0}; | ||||||
|  |   int update_count_{0}; | ||||||
|  |   int disable_loop_after_{0}; | ||||||
|  | }; | ||||||
|  |  | ||||||
| }  // namespace loop_test_component | }  // namespace loop_test_component | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -40,6 +40,13 @@ loop_test_component: | |||||||
|     - id: isr_test |     - id: isr_test | ||||||
|       name: "isr_test" |       name: "isr_test" | ||||||
|  |  | ||||||
|  |   # Update test component to test component.update when loop is disabled | ||||||
|  |   update_components: | ||||||
|  |     - id: update_test_component | ||||||
|  |       name: "update_test" | ||||||
|  |       disable_loop_after: 3  # Disable loop after 3 iterations | ||||||
|  |       update_interval: 0.1s  # Fast update interval for testing | ||||||
|  |  | ||||||
| # Interval to re-enable the self_disable_10 component after some time | # Interval to re-enable the self_disable_10 component after some time | ||||||
| interval: | interval: | ||||||
|   - interval: 0.5s |   - interval: 0.5s | ||||||
| @@ -51,3 +58,28 @@ interval: | |||||||
|             - logger.log: "Re-enabling self_disable_10 via service" |             - logger.log: "Re-enabling self_disable_10 via service" | ||||||
|             - loop_test_component.enable: |             - loop_test_component.enable: | ||||||
|                 id: self_disable_10 |                 id: self_disable_10 | ||||||
|  |  | ||||||
|  |   # Test component.update on a component with disabled loop | ||||||
|  |   - interval: 0.1s | ||||||
|  |     then: | ||||||
|  |       - lambda: |- | ||||||
|  |           static bool manual_update_done = false; | ||||||
|  |           if (!manual_update_done && | ||||||
|  |               id(update_test_component).get_loop_count() == 3 && | ||||||
|  |               id(update_test_component).get_update_count() >= 3) { | ||||||
|  |             ESP_LOGI("main", "Manually calling component.update on update_test_component with disabled loop"); | ||||||
|  |             manual_update_done = true; | ||||||
|  |           } | ||||||
|  |       - if: | ||||||
|  |           condition: | ||||||
|  |             lambda: |- | ||||||
|  |               static bool manual_update_triggered = false; | ||||||
|  |               if (!manual_update_triggered && | ||||||
|  |                   id(update_test_component).get_loop_count() == 3 && | ||||||
|  |                   id(update_test_component).get_update_count() >= 3) { | ||||||
|  |                 manual_update_triggered = true; | ||||||
|  |                 return true; | ||||||
|  |               } | ||||||
|  |               return false; | ||||||
|  |           then: | ||||||
|  |             - component.update: update_test_component | ||||||
|   | |||||||
| @@ -45,11 +45,18 @@ async def test_loop_disable_enable( | |||||||
|     isr_component_disabled = asyncio.Event() |     isr_component_disabled = asyncio.Event() | ||||||
|     isr_component_re_enabled = asyncio.Event() |     isr_component_re_enabled = asyncio.Event() | ||||||
|     isr_component_pure_re_enabled = asyncio.Event() |     isr_component_pure_re_enabled = asyncio.Event() | ||||||
|  |     # Events for update component testing | ||||||
|  |     update_component_loop_disabled = asyncio.Event() | ||||||
|  |     update_component_manual_update_called = asyncio.Event() | ||||||
|  |  | ||||||
|     # Track loop counts for components |     # Track loop counts for components | ||||||
|     self_disable_10_counts: list[int] = [] |     self_disable_10_counts: list[int] = [] | ||||||
|     normal_component_counts: list[int] = [] |     normal_component_counts: list[int] = [] | ||||||
|     isr_component_counts: list[int] = [] |     isr_component_counts: list[int] = [] | ||||||
|  |     # Track update component behavior | ||||||
|  |     update_component_loop_count = 0 | ||||||
|  |     update_component_update_count = 0 | ||||||
|  |     update_component_manual_update_count = 0 | ||||||
|  |  | ||||||
|     def on_log_line(line: str) -> None: |     def on_log_line(line: str) -> None: | ||||||
|         """Process each log line from the process output.""" |         """Process each log line from the process output.""" | ||||||
| @@ -59,6 +66,7 @@ async def test_loop_disable_enable( | |||||||
|         if ( |         if ( | ||||||
|             "loop_test_component" not in clean_line |             "loop_test_component" not in clean_line | ||||||
|             and "loop_test_isr_component" not in clean_line |             and "loop_test_isr_component" not in clean_line | ||||||
|  |             and "Manually calling component.update" not in clean_line | ||||||
|         ): |         ): | ||||||
|             return |             return | ||||||
|  |  | ||||||
| @@ -112,6 +120,23 @@ async def test_loop_disable_enable( | |||||||
|             elif "Running after pure ISR re-enable!" in clean_line: |             elif "Running after pure ISR re-enable!" in clean_line: | ||||||
|                 isr_component_pure_re_enabled.set() |                 isr_component_pure_re_enabled.set() | ||||||
|  |  | ||||||
|  |         # Update component events | ||||||
|  |         elif "[update_test]" in clean_line: | ||||||
|  |             if "LoopTestUpdateComponent loop count:" in clean_line: | ||||||
|  |                 nonlocal update_component_loop_count | ||||||
|  |                 update_component_loop_count = int( | ||||||
|  |                     clean_line.split("LoopTestUpdateComponent loop count: ")[1] | ||||||
|  |                 ) | ||||||
|  |             elif "LoopTestUpdateComponent update() called" in clean_line: | ||||||
|  |                 nonlocal update_component_update_count | ||||||
|  |                 update_component_update_count += 1 | ||||||
|  |                 if "Manually calling component.update" in " ".join(log_messages[-5:]): | ||||||
|  |                     nonlocal update_component_manual_update_count | ||||||
|  |                     update_component_manual_update_count += 1 | ||||||
|  |                     update_component_manual_update_called.set() | ||||||
|  |             elif "Disabling loop after" in clean_line: | ||||||
|  |                 update_component_loop_disabled.set() | ||||||
|  |  | ||||||
|     # Write, compile and run the ESPHome device with log callback |     # Write, compile and run the ESPHome device with log callback | ||||||
|     async with ( |     async with ( | ||||||
|         run_compiled(yaml_config, line_callback=on_log_line), |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
| @@ -205,3 +230,28 @@ async def test_loop_disable_enable( | |||||||
|         assert final_count > 10, ( |         assert final_count > 10, ( | ||||||
|             f"Component didn't run after pure ISR enable: got {final_count} counts total" |             f"Component didn't run after pure ISR enable: got {final_count} counts total" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |         # Test component.update functionality when loop is disabled | ||||||
|  |         # Wait for update component to disable its loop | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(update_component_loop_disabled.wait(), timeout=3.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("Update component did not disable its loop within 3 seconds") | ||||||
|  |  | ||||||
|  |         # Verify it ran exactly 3 loops before disabling | ||||||
|  |         assert update_component_loop_count == 3, ( | ||||||
|  |             f"Expected 3 loop iterations before disable, got {update_component_loop_count}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Wait for manual component.update to be called | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for( | ||||||
|  |                 update_component_manual_update_called.wait(), timeout=5.0 | ||||||
|  |             ) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail("Manual component.update was not called within 5 seconds") | ||||||
|  |  | ||||||
|  |         # The key test: verify that manual component.update worked after loop was disabled | ||||||
|  |         assert update_component_manual_update_count >= 1, ( | ||||||
|  |             "component.update did not fire after loop was disabled" | ||||||
|  |         ) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user