mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 07:03:55 +00:00 
			
		
		
		
	Merge branch 'heap_scheduler_stress_component' into integration
This commit is contained in:
		| @@ -14,8 +14,8 @@ from esphome.const import ( | |||||||
|  |  | ||||||
| from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns | from .. import CONF_LD2410_ID, LD2410Component, ld2410_ns | ||||||
|  |  | ||||||
|  | FactoryResetButton = ld2410_ns.class_("FactoryResetButton", button.Button) | ||||||
| QueryButton = ld2410_ns.class_("QueryButton", button.Button) | QueryButton = ld2410_ns.class_("QueryButton", button.Button) | ||||||
| ResetButton = ld2410_ns.class_("ResetButton", button.Button) |  | ||||||
| RestartButton = ld2410_ns.class_("RestartButton", button.Button) | RestartButton = ld2410_ns.class_("RestartButton", button.Button) | ||||||
|  |  | ||||||
| CONF_QUERY_PARAMS = "query_params" | CONF_QUERY_PARAMS = "query_params" | ||||||
| @@ -23,7 +23,7 @@ CONF_QUERY_PARAMS = "query_params" | |||||||
| CONFIG_SCHEMA = { | CONFIG_SCHEMA = { | ||||||
|     cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), |     cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), | ||||||
|     cv.Optional(CONF_FACTORY_RESET): button.button_schema( |     cv.Optional(CONF_FACTORY_RESET): button.button_schema( | ||||||
|         ResetButton, |         FactoryResetButton, | ||||||
|         device_class=DEVICE_CLASS_RESTART, |         device_class=DEVICE_CLASS_RESTART, | ||||||
|         entity_category=ENTITY_CATEGORY_CONFIG, |         entity_category=ENTITY_CATEGORY_CONFIG, | ||||||
|         icon=ICON_RESTART_ALERT, |         icon=ICON_RESTART_ALERT, | ||||||
| @@ -47,7 +47,7 @@ async def to_code(config): | |||||||
|     if factory_reset_config := config.get(CONF_FACTORY_RESET): |     if factory_reset_config := config.get(CONF_FACTORY_RESET): | ||||||
|         b = await button.new_button(factory_reset_config) |         b = await button.new_button(factory_reset_config) | ||||||
|         await cg.register_parented(b, config[CONF_LD2410_ID]) |         await cg.register_parented(b, config[CONF_LD2410_ID]) | ||||||
|         cg.add(ld2410_component.set_reset_button(b)) |         cg.add(ld2410_component.set_factory_reset_button(b)) | ||||||
|     if restart_config := config.get(CONF_RESTART): |     if restart_config := config.get(CONF_RESTART): | ||||||
|         b = await button.new_button(restart_config) |         b = await button.new_button(restart_config) | ||||||
|         await cg.register_parented(b, config[CONF_LD2410_ID]) |         await cg.register_parented(b, config[CONF_LD2410_ID]) | ||||||
|   | |||||||
| @@ -0,0 +1,9 @@ | |||||||
|  | #include "factory_reset_button.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace ld2410 { | ||||||
|  |  | ||||||
|  | void FactoryResetButton::press_action() { this->parent_->factory_reset(); } | ||||||
|  |  | ||||||
|  | }  // namespace ld2410 | ||||||
|  | }  // namespace esphome | ||||||
| @@ -6,9 +6,9 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace ld2410 { | namespace ld2410 { | ||||||
| 
 | 
 | ||||||
| class ResetButton : public button::Button, public Parented<LD2410Component> { | class FactoryResetButton : public button::Button, public Parented<LD2410Component> { | ||||||
|  public: |  public: | ||||||
|   ResetButton() = default; |   FactoryResetButton() = default; | ||||||
| 
 | 
 | ||||||
|  protected: |  protected: | ||||||
|   void press_action() override; |   void press_action() override; | ||||||
| @@ -1,9 +0,0 @@ | |||||||
| #include "reset_button.h" |  | ||||||
|  |  | ||||||
| namespace esphome { |  | ||||||
| namespace ld2410 { |  | ||||||
|  |  | ||||||
| void ResetButton::press_action() { this->parent_->factory_reset(); } |  | ||||||
|  |  | ||||||
| }  // namespace ld2410 |  | ||||||
| }  // namespace esphome |  | ||||||
| @@ -18,11 +18,10 @@ namespace esphome { | |||||||
| namespace ld2410 { | namespace ld2410 { | ||||||
|  |  | ||||||
| static const char *const TAG = "ld2410"; | static const char *const TAG = "ld2410"; | ||||||
| static const char *const NO_MAC = "08:05:04:03:02:01"; |  | ||||||
| static const char *const UNKNOWN_MAC = "unknown"; | static const char *const UNKNOWN_MAC = "unknown"; | ||||||
| static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; | static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X"; | ||||||
|  |  | ||||||
| enum BaudRateStructure : uint8_t { | enum BaudRate : uint8_t { | ||||||
|   BAUD_RATE_9600 = 1, |   BAUD_RATE_9600 = 1, | ||||||
|   BAUD_RATE_19200 = 2, |   BAUD_RATE_19200 = 2, | ||||||
|   BAUD_RATE_38400 = 3, |   BAUD_RATE_38400 = 3, | ||||||
| @@ -33,23 +32,23 @@ enum BaudRateStructure : uint8_t { | |||||||
|   BAUD_RATE_460800 = 8, |   BAUD_RATE_460800 = 8, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum DistanceResolutionStructure : uint8_t { | enum DistanceResolution : uint8_t { | ||||||
|   DISTANCE_RESOLUTION_0_2 = 0x01, |   DISTANCE_RESOLUTION_0_2 = 0x01, | ||||||
|   DISTANCE_RESOLUTION_0_75 = 0x00, |   DISTANCE_RESOLUTION_0_75 = 0x00, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum LightFunctionStructure : uint8_t { | enum LightFunction : uint8_t { | ||||||
|   LIGHT_FUNCTION_OFF = 0x00, |   LIGHT_FUNCTION_OFF = 0x00, | ||||||
|   LIGHT_FUNCTION_BELOW = 0x01, |   LIGHT_FUNCTION_BELOW = 0x01, | ||||||
|   LIGHT_FUNCTION_ABOVE = 0x02, |   LIGHT_FUNCTION_ABOVE = 0x02, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum OutPinLevelStructure : uint8_t { | enum OutPinLevel : uint8_t { | ||||||
|   OUT_PIN_LEVEL_LOW = 0x00, |   OUT_PIN_LEVEL_LOW = 0x00, | ||||||
|   OUT_PIN_LEVEL_HIGH = 0x01, |   OUT_PIN_LEVEL_HIGH = 0x01, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum PeriodicDataStructure : uint8_t { | enum PeriodicData : uint8_t { | ||||||
|   DATA_TYPES = 6, |   DATA_TYPES = 6, | ||||||
|   TARGET_STATES = 8, |   TARGET_STATES = 8, | ||||||
|   MOVING_TARGET_LOW = 9, |   MOVING_TARGET_LOW = 9, | ||||||
| @@ -67,12 +66,12 @@ enum PeriodicDataStructure : uint8_t { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| enum PeriodicDataValue : uint8_t { | enum PeriodicDataValue : uint8_t { | ||||||
|   HEAD = 0xAA, |   HEADER = 0xAA, | ||||||
|   END = 0x55, |   FOOTER = 0x55, | ||||||
|   CHECK = 0x00, |   CHECK = 0x00, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum AckDataStructure : uint8_t { | enum AckData : uint8_t { | ||||||
|   COMMAND = 6, |   COMMAND = 6, | ||||||
|   COMMAND_STATUS = 7, |   COMMAND_STATUS = 7, | ||||||
| }; | }; | ||||||
| @@ -80,11 +79,11 @@ enum AckDataStructure : uint8_t { | |||||||
| // Memory-efficient lookup tables | // Memory-efficient lookup tables | ||||||
| struct StringToUint8 { | struct StringToUint8 { | ||||||
|   const char *str; |   const char *str; | ||||||
|   uint8_t value; |   const uint8_t value; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| struct Uint8ToString { | struct Uint8ToString { | ||||||
|   uint8_t value; |   const uint8_t value; | ||||||
|   const char *str; |   const char *str; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -144,96 +143,119 @@ template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t v | |||||||
| } | } | ||||||
|  |  | ||||||
| // Commands | // Commands | ||||||
| static const uint8_t CMD_ENABLE_CONF = 0xFF; | static constexpr uint8_t CMD_ENABLE_CONF = 0xFF; | ||||||
| static const uint8_t CMD_DISABLE_CONF = 0xFE; | static constexpr uint8_t CMD_DISABLE_CONF = 0xFE; | ||||||
| static const uint8_t CMD_ENABLE_ENG = 0x62; | static constexpr uint8_t CMD_ENABLE_ENG = 0x62; | ||||||
| static const uint8_t CMD_DISABLE_ENG = 0x63; | static constexpr uint8_t CMD_DISABLE_ENG = 0x63; | ||||||
| static const uint8_t CMD_MAXDIST_DURATION = 0x60; | static constexpr uint8_t CMD_MAXDIST_DURATION = 0x60; | ||||||
| static const uint8_t CMD_QUERY = 0x61; | static constexpr uint8_t CMD_QUERY = 0x61; | ||||||
| static const uint8_t CMD_GATE_SENS = 0x64; | static constexpr uint8_t CMD_GATE_SENS = 0x64; | ||||||
| static const uint8_t CMD_VERSION = 0xA0; | static constexpr uint8_t CMD_QUERY_VERSION = 0xA0; | ||||||
| static const uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0xAB; | static constexpr uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0xAB; | ||||||
| static const uint8_t CMD_SET_DISTANCE_RESOLUTION = 0xAA; | static constexpr uint8_t CMD_SET_DISTANCE_RESOLUTION = 0xAA; | ||||||
| static const uint8_t CMD_QUERY_LIGHT_CONTROL = 0xAE; | static constexpr uint8_t CMD_QUERY_LIGHT_CONTROL = 0xAE; | ||||||
| static const uint8_t CMD_SET_LIGHT_CONTROL = 0xAD; | static constexpr uint8_t CMD_SET_LIGHT_CONTROL = 0xAD; | ||||||
| static const uint8_t CMD_SET_BAUD_RATE = 0xA1; | static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1; | ||||||
| static const uint8_t CMD_BT_PASSWORD = 0xA9; | static constexpr uint8_t CMD_BT_PASSWORD = 0xA9; | ||||||
| static const uint8_t CMD_MAC = 0xA5; | static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5; | ||||||
| static const uint8_t CMD_RESET = 0xA2; | static constexpr uint8_t CMD_RESET = 0xA2; | ||||||
| static const uint8_t CMD_RESTART = 0xA3; | static constexpr uint8_t CMD_RESTART = 0xA3; | ||||||
| static const uint8_t CMD_BLUETOOTH = 0xA4; | static constexpr uint8_t CMD_BLUETOOTH = 0xA4; | ||||||
| // Commands values | // Commands values | ||||||
| static const uint8_t CMD_MAX_MOVE_VALUE = 0x00; | static constexpr uint8_t CMD_MAX_MOVE_VALUE = 0x00; | ||||||
| static const uint8_t CMD_MAX_STILL_VALUE = 0x01; | static constexpr uint8_t CMD_MAX_STILL_VALUE = 0x01; | ||||||
| static const uint8_t CMD_DURATION_VALUE = 0x02; | static constexpr uint8_t CMD_DURATION_VALUE = 0x02; | ||||||
|  | // Header & Footer size | ||||||
|  | static constexpr uint8_t HEADER_FOOTER_SIZE = 4; | ||||||
| // Command Header & Footer | // Command Header & Footer | ||||||
| static const uint8_t CMD_FRAME_HEADER[4] = {0xFD, 0xFC, 0xFB, 0xFA}; | static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA}; | ||||||
| static const uint8_t CMD_FRAME_END[4] = {0x04, 0x03, 0x02, 0x01}; | static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01}; | ||||||
| // Data Header & Footer | // Data Header & Footer | ||||||
| static const uint8_t DATA_FRAME_HEADER[4] = {0xF4, 0xF3, 0xF2, 0xF1}; | static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1}; | ||||||
| static const uint8_t DATA_FRAME_END[4] = {0xF8, 0xF7, 0xF6, 0xF5}; | static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5}; | ||||||
|  | // MAC address the module uses when Bluetooth is disabled | ||||||
|  | static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01}; | ||||||
|  |  | ||||||
| static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } | static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; } | ||||||
|  |  | ||||||
|  | static bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) { | ||||||
|  |   for (uint8_t i = 0; i < HEADER_FOOTER_SIZE; i++) { | ||||||
|  |     if (header_footer[i] != buffer[i]) { | ||||||
|  |       return false;  // Mismatch in header/footer | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return true;  // Valid header/footer | ||||||
|  | } | ||||||
|  |  | ||||||
| void LD2410Component::dump_config() { | void LD2410Component::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, "LD2410:"); |   std::string mac_str = | ||||||
|  |       mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; | ||||||
|  |   std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], | ||||||
|  |                                     this->version_[4], this->version_[3], this->version_[2]); | ||||||
|  |   ESP_LOGCONFIG(TAG, | ||||||
|  |                 "LD2410:\n" | ||||||
|  |                 "  Firmware version: %s\n" | ||||||
|  |                 "  MAC address: %s\n" | ||||||
|  |                 "  Throttle: %u ms", | ||||||
|  |                 version.c_str(), mac_str.c_str(), this->throttle_); | ||||||
| #ifdef USE_BINARY_SENSOR | #ifdef USE_BINARY_SENSOR | ||||||
|   LOG_BINARY_SENSOR("  ", "TargetBinarySensor", this->target_binary_sensor_); |   ESP_LOGCONFIG(TAG, "Binary Sensors:"); | ||||||
|   LOG_BINARY_SENSOR("  ", "MovingTargetBinarySensor", this->moving_target_binary_sensor_); |   LOG_BINARY_SENSOR("  ", "Target", this->target_binary_sensor_); | ||||||
|   LOG_BINARY_SENSOR("  ", "StillTargetBinarySensor", this->still_target_binary_sensor_); |   LOG_BINARY_SENSOR("  ", "MovingTarget", this->moving_target_binary_sensor_); | ||||||
|   LOG_BINARY_SENSOR("  ", "OutPinPresenceStatusBinarySensor", this->out_pin_presence_status_binary_sensor_); |   LOG_BINARY_SENSOR("  ", "StillTarget", this->still_target_binary_sensor_); | ||||||
| #endif |   LOG_BINARY_SENSOR("  ", "OutPinPresenceStatus", this->out_pin_presence_status_binary_sensor_); | ||||||
| #ifdef USE_SWITCH |  | ||||||
|   LOG_SWITCH("  ", "EngineeringModeSwitch", this->engineering_mode_switch_); |  | ||||||
|   LOG_SWITCH("  ", "BluetoothSwitch", this->bluetooth_switch_); |  | ||||||
| #endif |  | ||||||
| #ifdef USE_BUTTON |  | ||||||
|   LOG_BUTTON("  ", "ResetButton", this->reset_button_); |  | ||||||
|   LOG_BUTTON("  ", "RestartButton", this->restart_button_); |  | ||||||
|   LOG_BUTTON("  ", "QueryButton", this->query_button_); |  | ||||||
| #endif | #endif | ||||||
| #ifdef USE_SENSOR | #ifdef USE_SENSOR | ||||||
|   LOG_SENSOR("  ", "LightSensor", this->light_sensor_); |   ESP_LOGCONFIG(TAG, "Sensors:"); | ||||||
|   LOG_SENSOR("  ", "MovingTargetDistanceSensor", this->moving_target_distance_sensor_); |   LOG_SENSOR("  ", "Light", this->light_sensor_); | ||||||
|   LOG_SENSOR("  ", "StillTargetDistanceSensor", this->still_target_distance_sensor_); |   LOG_SENSOR("  ", "DetectionDistance", this->detection_distance_sensor_); | ||||||
|   LOG_SENSOR("  ", "MovingTargetEnergySensor", this->moving_target_energy_sensor_); |   LOG_SENSOR("  ", "MovingTargetDistance", this->moving_target_distance_sensor_); | ||||||
|   LOG_SENSOR("  ", "StillTargetEnergySensor", this->still_target_energy_sensor_); |   LOG_SENSOR("  ", "MovingTargetEnergy", this->moving_target_energy_sensor_); | ||||||
|   LOG_SENSOR("  ", "DetectionDistanceSensor", this->detection_distance_sensor_); |   LOG_SENSOR("  ", "StillTargetDistance", this->still_target_distance_sensor_); | ||||||
|   for (sensor::Sensor *s : this->gate_still_sensors_) { |   LOG_SENSOR("  ", "StillTargetEnergy", this->still_target_energy_sensor_); | ||||||
|     LOG_SENSOR("  ", "NthGateStillSesnsor", s); |  | ||||||
|   } |  | ||||||
|   for (sensor::Sensor *s : this->gate_move_sensors_) { |   for (sensor::Sensor *s : this->gate_move_sensors_) { | ||||||
|     LOG_SENSOR("  ", "NthGateMoveSesnsor", s); |     LOG_SENSOR("  ", "GateMove", s); | ||||||
|  |   } | ||||||
|  |   for (sensor::Sensor *s : this->gate_still_sensors_) { | ||||||
|  |     LOG_SENSOR("  ", "GateStill", s); | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|   LOG_TEXT_SENSOR("  ", "VersionTextSensor", this->version_text_sensor_); |   ESP_LOGCONFIG(TAG, "Text Sensors:"); | ||||||
|   LOG_TEXT_SENSOR("  ", "MacTextSensor", this->mac_text_sensor_); |   LOG_TEXT_SENSOR("  ", "Mac", this->mac_text_sensor_); | ||||||
| #endif |   LOG_TEXT_SENSOR("  ", "Version", this->version_text_sensor_); | ||||||
| #ifdef USE_SELECT |  | ||||||
|   LOG_SELECT("  ", "LightFunctionSelect", this->light_function_select_); |  | ||||||
|   LOG_SELECT("  ", "OutPinLevelSelect", this->out_pin_level_select_); |  | ||||||
|   LOG_SELECT("  ", "DistanceResolutionSelect", this->distance_resolution_select_); |  | ||||||
|   LOG_SELECT("  ", "BaudRateSelect", this->baud_rate_select_); |  | ||||||
| #endif | #endif | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
|   LOG_NUMBER("  ", "LightThresholdNumber", this->light_threshold_number_); |   ESP_LOGCONFIG(TAG, "Numbers:"); | ||||||
|   LOG_NUMBER("  ", "MaxStillDistanceGateNumber", this->max_still_distance_gate_number_); |   LOG_NUMBER("  ", "LightThreshold", this->light_threshold_number_); | ||||||
|   LOG_NUMBER("  ", "MaxMoveDistanceGateNumber", this->max_move_distance_gate_number_); |   LOG_NUMBER("  ", "MaxMoveDistanceGate", this->max_move_distance_gate_number_); | ||||||
|   LOG_NUMBER("  ", "TimeoutNumber", this->timeout_number_); |   LOG_NUMBER("  ", "MaxStillDistanceGate", this->max_still_distance_gate_number_); | ||||||
|   for (number::Number *n : this->gate_still_threshold_numbers_) { |   LOG_NUMBER("  ", "Timeout", this->timeout_number_); | ||||||
|     LOG_NUMBER("  ", "Still Thresholds Number", n); |  | ||||||
|   } |  | ||||||
|   for (number::Number *n : this->gate_move_threshold_numbers_) { |   for (number::Number *n : this->gate_move_threshold_numbers_) { | ||||||
|     LOG_NUMBER("  ", "Move Thresholds Number", n); |     LOG_NUMBER("  ", "MoveThreshold", n); | ||||||
|  |   } | ||||||
|  |   for (number::Number *n : this->gate_still_threshold_numbers_) { | ||||||
|  |     LOG_NUMBER("  ", "StillThreshold", n); | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|   this->read_all_info(); | #ifdef USE_SELECT | ||||||
|   ESP_LOGCONFIG(TAG, |   ESP_LOGCONFIG(TAG, "Selects:"); | ||||||
|                 "  Throttle: %ums\n" |   LOG_SELECT("  ", "BaudRate", this->baud_rate_select_); | ||||||
|                 "  MAC address: %s\n" |   LOG_SELECT("  ", "DistanceResolution", this->distance_resolution_select_); | ||||||
|                 "  Firmware version: %s", |   LOG_SELECT("  ", "LightFunction", this->light_function_select_); | ||||||
|                 this->throttle_, this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_.c_str(), this->version_.c_str()); |   LOG_SELECT("  ", "OutPinLevel", this->out_pin_level_select_); | ||||||
|  | #endif | ||||||
|  | #ifdef USE_SWITCH | ||||||
|  |   ESP_LOGCONFIG(TAG, "Switches:"); | ||||||
|  |   LOG_SWITCH("  ", "Bluetooth", this->bluetooth_switch_); | ||||||
|  |   LOG_SWITCH("  ", "EngineeringMode", this->engineering_mode_switch_); | ||||||
|  | #endif | ||||||
|  | #ifdef USE_BUTTON | ||||||
|  |   ESP_LOGCONFIG(TAG, "Buttons:"); | ||||||
|  |   LOG_BUTTON("  ", "FactoryReset", this->factory_reset_button_); | ||||||
|  |   LOG_BUTTON("  ", "Query", this->query_button_); | ||||||
|  |   LOG_BUTTON("  ", "Restart", this->restart_button_); | ||||||
|  | #endif | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::setup() { | void LD2410Component::setup() { | ||||||
| @@ -246,12 +268,12 @@ void LD2410Component::read_all_info() { | |||||||
|   this->get_version_(); |   this->get_version_(); | ||||||
|   this->get_mac_(); |   this->get_mac_(); | ||||||
|   this->get_distance_resolution_(); |   this->get_distance_resolution_(); | ||||||
|   this->get_light_control_(); |   this->query_light_control_(); | ||||||
|   this->query_parameters_(); |   this->query_parameters_(); | ||||||
|   this->set_config_mode_(false); |   this->set_config_mode_(false); | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|   const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); |   const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); | ||||||
|   if (this->baud_rate_select_ != nullptr && this->baud_rate_select_->state != baud_rate) { |   if (this->baud_rate_select_ != nullptr) { | ||||||
|     this->baud_rate_select_->publish_state(baud_rate); |     this->baud_rate_select_->publish_state(baud_rate); | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
| @@ -264,66 +286,59 @@ void LD2410Component::restart_and_read_all_info() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::loop() { | void LD2410Component::loop() { | ||||||
|   const int max_line_length = 80; |   while (this->available()) { | ||||||
|   static uint8_t buffer[max_line_length]; |     this->readline_(this->read()); | ||||||
|  |  | ||||||
|   while (available()) { |  | ||||||
|     this->readline_(read(), buffer, max_line_length); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::send_command_(uint8_t command, const uint8_t *command_value, int command_value_len) { | void LD2410Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { | ||||||
|   ESP_LOGV(TAG, "Sending COMMAND %02X", command); |   ESP_LOGV(TAG, "Sending COMMAND %02X", command); | ||||||
|   // frame start bytes |   // frame header bytes | ||||||
|   this->write_array(CMD_FRAME_HEADER, 4); |   this->write_array(CMD_FRAME_HEADER, sizeof(CMD_FRAME_HEADER)); | ||||||
|   // length bytes |   // length bytes | ||||||
|   int len = 2; |   uint8_t len = 2; | ||||||
|   if (command_value != nullptr) |   if (command_value != nullptr) { | ||||||
|     len += command_value_len; |     len += command_value_len; | ||||||
|   this->write_byte(lowbyte(len)); |   } | ||||||
|   this->write_byte(highbyte(len)); |   uint8_t len_cmd[] = {lowbyte(len), highbyte(len), command, 0x00}; | ||||||
|  |   this->write_array(len_cmd, sizeof(len_cmd)); | ||||||
|   // command |  | ||||||
|   this->write_byte(lowbyte(command)); |  | ||||||
|   this->write_byte(highbyte(command)); |  | ||||||
|  |  | ||||||
|   // command value bytes |   // command value bytes | ||||||
|   if (command_value != nullptr) { |   if (command_value != nullptr) { | ||||||
|     for (int i = 0; i < command_value_len; i++) { |     for (uint8_t i = 0; i < command_value_len; i++) { | ||||||
|       this->write_byte(command_value[i]); |       this->write_byte(command_value[i]); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   // frame end bytes |   // frame footer bytes | ||||||
|   this->write_array(CMD_FRAME_END, 4); |   this->write_array(CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)); | ||||||
|   // FIXME to remove |   // FIXME to remove | ||||||
|   delay(50);  // NOLINT |   delay(50);  // NOLINT | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | void LD2410Component::handle_periodic_data_() { | ||||||
|   if (len < 12) |   // Reduce data update rate to reduce home assistant database growth | ||||||
|     return;  // 4 frame start bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame end bytes |   // Check this first to prevent unnecessary processing done in later checks/parsing | ||||||
|   if (buffer[0] != 0xF4 || buffer[1] != 0xF3 || buffer[2] != 0xF2 || buffer[3] != 0xF1)  // check 4 frame start bytes |   if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) { | ||||||
|     return; |     return; | ||||||
|   if (buffer[7] != HEAD || buffer[len - 6] != END || buffer[len - 5] != CHECK)  // Check constant values |   } | ||||||
|     return;  // data head=0xAA, data end=0x55, crc=0x00 |   // 4 frame header bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame footer bytes | ||||||
|  |   // data header=0xAA, data footer=0x55, crc=0x00 | ||||||
|   /* |   if (this->buffer_pos_ < 12 || !ld2410::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) || | ||||||
|     Reduce data update rate to prevent home assistant database size grow fast |       this->buffer_data_[7] != HEADER || this->buffer_data_[this->buffer_pos_ - 6] != FOOTER || | ||||||
|   */ |       this->buffer_data_[this->buffer_pos_ - 5] != CHECK) { | ||||||
|   int32_t current_millis = App.get_loop_component_start_time(); |  | ||||||
|   if (current_millis - last_periodic_millis_ < this->throttle_) |  | ||||||
|     return; |     return; | ||||||
|   last_periodic_millis_ = current_millis; |   } | ||||||
|  |   // Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately | ||||||
|  |   this->last_periodic_millis_ = App.get_loop_component_start_time(); | ||||||
|  |  | ||||||
|   /* |   /* | ||||||
|     Data Type: 7th |     Data Type: 7th | ||||||
|     0x01: Engineering mode |     0x01: Engineering mode | ||||||
|     0x02: Normal mode |     0x02: Normal mode | ||||||
|   */ |   */ | ||||||
|   bool engineering_mode = buffer[DATA_TYPES] == 0x01; |   bool engineering_mode = this->buffer_data_[DATA_TYPES] == 0x01; | ||||||
| #ifdef USE_SWITCH | #ifdef USE_SWITCH | ||||||
|   if (this->engineering_mode_switch_ != nullptr && |   if (this->engineering_mode_switch_ != nullptr) { | ||||||
|       current_millis - last_engineering_mode_change_millis_ > this->throttle_) { |  | ||||||
|     this->engineering_mode_switch_->publish_state(engineering_mode); |     this->engineering_mode_switch_->publish_state(engineering_mode); | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
| @@ -335,7 +350,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | |||||||
|     0x02 = Still targets |     0x02 = Still targets | ||||||
|     0x03 = Moving+Still targets |     0x03 = Moving+Still targets | ||||||
|   */ |   */ | ||||||
|   char target_state = buffer[TARGET_STATES]; |   char target_state = this->buffer_data_[TARGET_STATES]; | ||||||
|   if (this->target_binary_sensor_ != nullptr) { |   if (this->target_binary_sensor_ != nullptr) { | ||||||
|     this->target_binary_sensor_->publish_state(target_state != 0x00); |     this->target_binary_sensor_->publish_state(target_state != 0x00); | ||||||
|   } |   } | ||||||
| @@ -355,27 +370,30 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | |||||||
|   */ |   */ | ||||||
| #ifdef USE_SENSOR | #ifdef USE_SENSOR | ||||||
|   if (this->moving_target_distance_sensor_ != nullptr) { |   if (this->moving_target_distance_sensor_ != nullptr) { | ||||||
|     int new_moving_target_distance = ld2410::two_byte_to_int(buffer[MOVING_TARGET_LOW], buffer[MOVING_TARGET_HIGH]); |     int new_moving_target_distance = | ||||||
|  |         ld2410::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]); | ||||||
|     if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance) |     if (this->moving_target_distance_sensor_->get_state() != new_moving_target_distance) | ||||||
|       this->moving_target_distance_sensor_->publish_state(new_moving_target_distance); |       this->moving_target_distance_sensor_->publish_state(new_moving_target_distance); | ||||||
|   } |   } | ||||||
|   if (this->moving_target_energy_sensor_ != nullptr) { |   if (this->moving_target_energy_sensor_ != nullptr) { | ||||||
|     int new_moving_target_energy = buffer[MOVING_ENERGY]; |     int new_moving_target_energy = this->buffer_data_[MOVING_ENERGY]; | ||||||
|     if (this->moving_target_energy_sensor_->get_state() != new_moving_target_energy) |     if (this->moving_target_energy_sensor_->get_state() != new_moving_target_energy) | ||||||
|       this->moving_target_energy_sensor_->publish_state(new_moving_target_energy); |       this->moving_target_energy_sensor_->publish_state(new_moving_target_energy); | ||||||
|   } |   } | ||||||
|   if (this->still_target_distance_sensor_ != nullptr) { |   if (this->still_target_distance_sensor_ != nullptr) { | ||||||
|     int new_still_target_distance = ld2410::two_byte_to_int(buffer[STILL_TARGET_LOW], buffer[STILL_TARGET_HIGH]); |     int new_still_target_distance = | ||||||
|  |         ld2410::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]); | ||||||
|     if (this->still_target_distance_sensor_->get_state() != new_still_target_distance) |     if (this->still_target_distance_sensor_->get_state() != new_still_target_distance) | ||||||
|       this->still_target_distance_sensor_->publish_state(new_still_target_distance); |       this->still_target_distance_sensor_->publish_state(new_still_target_distance); | ||||||
|   } |   } | ||||||
|   if (this->still_target_energy_sensor_ != nullptr) { |   if (this->still_target_energy_sensor_ != nullptr) { | ||||||
|     int new_still_target_energy = buffer[STILL_ENERGY]; |     int new_still_target_energy = this->buffer_data_[STILL_ENERGY]; | ||||||
|     if (this->still_target_energy_sensor_->get_state() != new_still_target_energy) |     if (this->still_target_energy_sensor_->get_state() != new_still_target_energy) | ||||||
|       this->still_target_energy_sensor_->publish_state(new_still_target_energy); |       this->still_target_energy_sensor_->publish_state(new_still_target_energy); | ||||||
|   } |   } | ||||||
|   if (this->detection_distance_sensor_ != nullptr) { |   if (this->detection_distance_sensor_ != nullptr) { | ||||||
|     int new_detect_distance = ld2410::two_byte_to_int(buffer[DETECT_DISTANCE_LOW], buffer[DETECT_DISTANCE_HIGH]); |     int new_detect_distance = | ||||||
|  |         ld2410::two_byte_to_int(this->buffer_data_[DETECT_DISTANCE_LOW], this->buffer_data_[DETECT_DISTANCE_HIGH]); | ||||||
|     if (this->detection_distance_sensor_->get_state() != new_detect_distance) |     if (this->detection_distance_sensor_->get_state() != new_detect_distance) | ||||||
|       this->detection_distance_sensor_->publish_state(new_detect_distance); |       this->detection_distance_sensor_->publish_state(new_detect_distance); | ||||||
|   } |   } | ||||||
| @@ -388,7 +406,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | |||||||
|     for (std::vector<sensor::Sensor *>::size_type i = 0; i != this->gate_move_sensors_.size(); i++) { |     for (std::vector<sensor::Sensor *>::size_type i = 0; i != this->gate_move_sensors_.size(); i++) { | ||||||
|       sensor::Sensor *s = this->gate_move_sensors_[i]; |       sensor::Sensor *s = this->gate_move_sensors_[i]; | ||||||
|       if (s != nullptr) { |       if (s != nullptr) { | ||||||
|         s->publish_state(buffer[MOVING_SENSOR_START + i]); |         s->publish_state(this->buffer_data_[MOVING_SENSOR_START + i]); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     /* |     /* | ||||||
| @@ -397,16 +415,17 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | |||||||
|     for (std::vector<sensor::Sensor *>::size_type i = 0; i != this->gate_still_sensors_.size(); i++) { |     for (std::vector<sensor::Sensor *>::size_type i = 0; i != this->gate_still_sensors_.size(); i++) { | ||||||
|       sensor::Sensor *s = this->gate_still_sensors_[i]; |       sensor::Sensor *s = this->gate_still_sensors_[i]; | ||||||
|       if (s != nullptr) { |       if (s != nullptr) { | ||||||
|         s->publish_state(buffer[STILL_SENSOR_START + i]); |         s->publish_state(this->buffer_data_[STILL_SENSOR_START + i]); | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     /* |     /* | ||||||
|       Light sensor: 38th bytes |       Light sensor: 38th bytes | ||||||
|     */ |     */ | ||||||
|     if (this->light_sensor_ != nullptr) { |     if (this->light_sensor_ != nullptr) { | ||||||
|       int new_light_sensor = buffer[LIGHT_SENSOR]; |       int new_light_sensor = this->buffer_data_[LIGHT_SENSOR]; | ||||||
|       if (this->light_sensor_->get_state() != new_light_sensor) |       if (this->light_sensor_->get_state() != new_light_sensor) { | ||||||
|         this->light_sensor_->publish_state(new_light_sensor); |         this->light_sensor_->publish_state(new_light_sensor); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } else { |   } else { | ||||||
|     for (auto *s : this->gate_move_sensors_) { |     for (auto *s : this->gate_move_sensors_) { | ||||||
| @@ -427,7 +446,7 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | |||||||
| #ifdef USE_BINARY_SENSOR | #ifdef USE_BINARY_SENSOR | ||||||
|   if (engineering_mode) { |   if (engineering_mode) { | ||||||
|     if (this->out_pin_presence_status_binary_sensor_ != nullptr) { |     if (this->out_pin_presence_status_binary_sensor_ != nullptr) { | ||||||
|       this->out_pin_presence_status_binary_sensor_->publish_state(buffer[OUT_PIN_SENSOR] == 0x01); |       this->out_pin_presence_status_binary_sensor_->publish_state(this->buffer_data_[OUT_PIN_SENSOR] == 0x01); | ||||||
|     } |     } | ||||||
|   } else { |   } else { | ||||||
|     if (this->out_pin_presence_status_binary_sensor_ != nullptr) { |     if (this->out_pin_presence_status_binary_sensor_ != nullptr) { | ||||||
| @@ -439,127 +458,149 @@ void LD2410Component::handle_periodic_data_(uint8_t *buffer, int len) { | |||||||
|  |  | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
| std::function<void(void)> set_number_value(number::Number *n, float value) { | std::function<void(void)> set_number_value(number::Number *n, float value) { | ||||||
|   float normalized_value = value * 1.0; |   if (n != nullptr && (!n->has_state() || n->state != value)) { | ||||||
|   if (n != nullptr && (!n->has_state() || n->state != normalized_value)) { |     n->state = value; | ||||||
|     n->state = normalized_value; |     return [n, value]() { n->publish_state(value); }; | ||||||
|     return [n, normalized_value]() { n->publish_state(normalized_value); }; |  | ||||||
|   } |   } | ||||||
|   return []() {}; |   return []() {}; | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | bool LD2410Component::handle_ack_data_() { | ||||||
|   ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", buffer[COMMAND]); |   ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]); | ||||||
|   if (len < 10) { |   if (this->buffer_pos_ < 10) { | ||||||
|     ESP_LOGE(TAG, "Invalid length"); |     ESP_LOGE(TAG, "Invalid length"); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|   if (buffer[0] != 0xFD || buffer[1] != 0xFC || buffer[2] != 0xFB || buffer[3] != 0xFA) {  // check 4 frame start bytes |   if (!ld2410::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) { | ||||||
|     ESP_LOGE(TAG, "Invalid header"); |     ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str()); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|   if (buffer[COMMAND_STATUS] != 0x01) { |   if (this->buffer_data_[COMMAND_STATUS] != 0x01) { | ||||||
|     ESP_LOGE(TAG, "Invalid status"); |     ESP_LOGE(TAG, "Invalid status"); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|   if (ld2410::two_byte_to_int(buffer[8], buffer[9]) != 0x00) { |   if (ld2410::two_byte_to_int(this->buffer_data_[8], this->buffer_data_[9]) != 0x00) { | ||||||
|     ESP_LOGE(TAG, "Invalid command: %u, %u", buffer[8], buffer[9]); |     ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   switch (buffer[COMMAND]) { |   switch (this->buffer_data_[COMMAND]) { | ||||||
|     case lowbyte(CMD_ENABLE_CONF): |     case CMD_ENABLE_CONF: | ||||||
|       ESP_LOGV(TAG, "Enable conf"); |       ESP_LOGV(TAG, "Enable conf"); | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_DISABLE_CONF): |  | ||||||
|  |     case CMD_DISABLE_CONF: | ||||||
|       ESP_LOGV(TAG, "Disabled conf"); |       ESP_LOGV(TAG, "Disabled conf"); | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_SET_BAUD_RATE): |  | ||||||
|  |     case CMD_SET_BAUD_RATE: | ||||||
|       ESP_LOGV(TAG, "Baud rate change"); |       ESP_LOGV(TAG, "Baud rate change"); | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|       if (this->baud_rate_select_ != nullptr) { |       if (this->baud_rate_select_ != nullptr) { | ||||||
|         ESP_LOGE(TAG, "Configure baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); |         ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_VERSION): |  | ||||||
|       this->version_ = str_sprintf(VERSION_FMT, buffer[13], buffer[12], buffer[17], buffer[16], buffer[15], buffer[14]); |     case CMD_QUERY_VERSION: { | ||||||
|       ESP_LOGV(TAG, "Firmware version: %s", this->version_.c_str()); |       std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_)); | ||||||
|  |       std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5], | ||||||
|  |                                         this->version_[4], this->version_[3], this->version_[2]); | ||||||
|  |       ESP_LOGV(TAG, "Firmware version: %s", version.c_str()); | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|       if (this->version_text_sensor_ != nullptr) { |       if (this->version_text_sensor_ != nullptr) { | ||||||
|         this->version_text_sensor_->publish_state(this->version_); |         this->version_text_sensor_->publish_state(version); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_QUERY_DISTANCE_RESOLUTION): { |     } | ||||||
|       std::string distance_resolution = |  | ||||||
|           find_str(DISTANCE_RESOLUTIONS_BY_UINT, ld2410::two_byte_to_int(buffer[10], buffer[11])); |     case CMD_QUERY_DISTANCE_RESOLUTION: { | ||||||
|       ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution.c_str()); |       const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]); | ||||||
|  |       ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution); | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|       if (this->distance_resolution_select_ != nullptr && |       if (this->distance_resolution_select_ != nullptr) { | ||||||
|           this->distance_resolution_select_->state != distance_resolution) { |  | ||||||
|         this->distance_resolution_select_->publish_state(distance_resolution); |         this->distance_resolution_select_->publish_state(distance_resolution); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|     } break; |       break; | ||||||
|     case lowbyte(CMD_QUERY_LIGHT_CONTROL): { |     } | ||||||
|       this->light_function_ = find_str(LIGHT_FUNCTIONS_BY_UINT, buffer[10]); |  | ||||||
|       this->light_threshold_ = buffer[11] * 1.0; |     case CMD_QUERY_LIGHT_CONTROL: { | ||||||
|       this->out_pin_level_ = find_str(OUT_PIN_LEVELS_BY_UINT, buffer[12]); |       this->light_function_ = this->buffer_data_[10]; | ||||||
|       ESP_LOGV(TAG, "Light function: %s", const_cast<char *>(this->light_function_.c_str())); |       this->light_threshold_ = this->buffer_data_[11]; | ||||||
|       ESP_LOGV(TAG, "Light threshold: %f", this->light_threshold_); |       this->out_pin_level_ = this->buffer_data_[12]; | ||||||
|       ESP_LOGV(TAG, "Out pin level: %s", const_cast<char *>(this->out_pin_level_.c_str())); |       const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_); | ||||||
|  |       const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_); | ||||||
|  |       ESP_LOGV(TAG, | ||||||
|  |                "Light function is: %s\n" | ||||||
|  |                "Light threshold is: %u\n" | ||||||
|  |                "Out pin level: %s", | ||||||
|  |                light_function_str, this->light_threshold_, out_pin_level_str); | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|       if (this->light_function_select_ != nullptr && this->light_function_select_->state != this->light_function_) { |       if (this->light_function_select_ != nullptr) { | ||||||
|         this->light_function_select_->publish_state(this->light_function_); |         this->light_function_select_->publish_state(light_function_str); | ||||||
|       } |       } | ||||||
|       if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->state != this->out_pin_level_) { |       if (this->out_pin_level_select_ != nullptr) { | ||||||
|         this->out_pin_level_select_->publish_state(this->out_pin_level_); |         this->out_pin_level_select_->publish_state(out_pin_level_str); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
|       if (this->light_threshold_number_ != nullptr && |       if (this->light_threshold_number_ != nullptr) { | ||||||
|           (!this->light_threshold_number_->has_state() || |         this->light_threshold_number_->publish_state(static_cast<float>(this->light_threshold_)); | ||||||
|            this->light_threshold_number_->state != this->light_threshold_)) { |  | ||||||
|         this->light_threshold_number_->publish_state(this->light_threshold_); |  | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|     } break; |       break; | ||||||
|     case lowbyte(CMD_MAC): |     } | ||||||
|       if (len < 20) { |     case CMD_QUERY_MAC_ADDRESS: { | ||||||
|  |       if (this->buffer_pos_ < 20) { | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
|       this->mac_ = format_mac_address_pretty(&buffer[10]); |  | ||||||
|       ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str()); |       this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0; | ||||||
|  |       if (this->bluetooth_on_) { | ||||||
|  |         std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_)); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       std::string mac_str = | ||||||
|  |           mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC; | ||||||
|  |       ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str()); | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|       if (this->mac_text_sensor_ != nullptr) { |       if (this->mac_text_sensor_ != nullptr) { | ||||||
|         this->mac_text_sensor_->publish_state(this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_); |         this->mac_text_sensor_->publish_state(mac_str); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
| #ifdef USE_SWITCH | #ifdef USE_SWITCH | ||||||
|       if (this->bluetooth_switch_ != nullptr) { |       if (this->bluetooth_switch_ != nullptr) { | ||||||
|         this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC); |         this->bluetooth_switch_->publish_state(this->bluetooth_on_); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_GATE_SENS): |     } | ||||||
|  |  | ||||||
|  |     case CMD_GATE_SENS: | ||||||
|       ESP_LOGV(TAG, "Sensitivity"); |       ESP_LOGV(TAG, "Sensitivity"); | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_BLUETOOTH): |  | ||||||
|  |     case CMD_BLUETOOTH: | ||||||
|       ESP_LOGV(TAG, "Bluetooth"); |       ESP_LOGV(TAG, "Bluetooth"); | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_SET_DISTANCE_RESOLUTION): |  | ||||||
|  |     case CMD_SET_DISTANCE_RESOLUTION: | ||||||
|       ESP_LOGV(TAG, "Set distance resolution"); |       ESP_LOGV(TAG, "Set distance resolution"); | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_SET_LIGHT_CONTROL): |  | ||||||
|  |     case CMD_SET_LIGHT_CONTROL: | ||||||
|       ESP_LOGV(TAG, "Set light control"); |       ESP_LOGV(TAG, "Set light control"); | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_BT_PASSWORD): |  | ||||||
|  |     case CMD_BT_PASSWORD: | ||||||
|       ESP_LOGV(TAG, "Set bluetooth password"); |       ESP_LOGV(TAG, "Set bluetooth password"); | ||||||
|       break; |       break; | ||||||
|     case lowbyte(CMD_QUERY):  // Query parameters response |  | ||||||
|     { |     case CMD_QUERY: {  // Query parameters response | ||||||
|       if (buffer[10] != 0xAA) |       if (this->buffer_data_[10] != 0xAA) | ||||||
|         return true;  // value head=0xAA |         return true;  // value head=0xAA | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
|       /* |       /* | ||||||
| @@ -567,29 +608,31 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | |||||||
|         Still distance range: 14th byte |         Still distance range: 14th byte | ||||||
|       */ |       */ | ||||||
|       std::vector<std::function<void(void)>> updates; |       std::vector<std::function<void(void)>> updates; | ||||||
|       updates.push_back(set_number_value(this->max_move_distance_gate_number_, buffer[12])); |       updates.push_back(set_number_value(this->max_move_distance_gate_number_, this->buffer_data_[12])); | ||||||
|       updates.push_back(set_number_value(this->max_still_distance_gate_number_, buffer[13])); |       updates.push_back(set_number_value(this->max_still_distance_gate_number_, this->buffer_data_[13])); | ||||||
|       /* |       /* | ||||||
|         Moving Sensitivities: 15~23th bytes |         Moving Sensitivities: 15~23th bytes | ||||||
|       */ |       */ | ||||||
|       for (std::vector<number::Number *>::size_type i = 0; i != this->gate_move_threshold_numbers_.size(); i++) { |       for (std::vector<number::Number *>::size_type i = 0; i != this->gate_move_threshold_numbers_.size(); i++) { | ||||||
|         updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], buffer[14 + i])); |         updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[14 + i])); | ||||||
|       } |       } | ||||||
|       /* |       /* | ||||||
|         Still Sensitivities: 24~32th bytes |         Still Sensitivities: 24~32th bytes | ||||||
|       */ |       */ | ||||||
|       for (std::vector<number::Number *>::size_type i = 0; i != this->gate_still_threshold_numbers_.size(); i++) { |       for (std::vector<number::Number *>::size_type i = 0; i != this->gate_still_threshold_numbers_.size(); i++) { | ||||||
|         updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], buffer[23 + i])); |         updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[23 + i])); | ||||||
|       } |       } | ||||||
|       /* |       /* | ||||||
|         None Duration: 33~34th bytes |         None Duration: 33~34th bytes | ||||||
|       */ |       */ | ||||||
|       updates.push_back(set_number_value(this->timeout_number_, ld2410::two_byte_to_int(buffer[32], buffer[33]))); |       updates.push_back(set_number_value(this->timeout_number_, | ||||||
|  |                                          ld2410::two_byte_to_int(this->buffer_data_[32], this->buffer_data_[33]))); | ||||||
|       for (auto &update : updates) { |       for (auto &update : updates) { | ||||||
|         update(); |         update(); | ||||||
|       } |       } | ||||||
| #endif | #endif | ||||||
|     } break; |       break; | ||||||
|  |     } | ||||||
|     default: |     default: | ||||||
|       break; |       break; | ||||||
|   } |   } | ||||||
| @@ -597,59 +640,66 @@ bool LD2410Component::handle_ack_data_(uint8_t *buffer, int len) { | |||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::readline_(int readch, uint8_t *buffer, int len) { | void LD2410Component::readline_(int readch) { | ||||||
|   static int pos = 0; |   if (readch < 0) { | ||||||
|  |     return;  // No data available | ||||||
|  |   } | ||||||
|  |  | ||||||
|   if (readch >= 0) { |   if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) { | ||||||
|     if (pos < len - 1) { |     this->buffer_data_[this->buffer_pos_++] = readch; | ||||||
|       buffer[pos++] = readch; |     this->buffer_data_[this->buffer_pos_] = 0; | ||||||
|       buffer[pos] = 0; |   } else { | ||||||
|  |     // We should never get here, but just in case... | ||||||
|  |     ESP_LOGW(TAG, "Max command length exceeded; ignoring"); | ||||||
|  |     this->buffer_pos_ = 0; | ||||||
|  |   } | ||||||
|  |   if (this->buffer_pos_ < 4) { | ||||||
|  |     return;  // Not enough data to process yet | ||||||
|  |   } | ||||||
|  |   if (this->buffer_data_[this->buffer_pos_ - 4] == DATA_FRAME_FOOTER[0] && | ||||||
|  |       this->buffer_data_[this->buffer_pos_ - 3] == DATA_FRAME_FOOTER[1] && | ||||||
|  |       this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[2] && | ||||||
|  |       this->buffer_data_[this->buffer_pos_ - 1] == DATA_FRAME_FOOTER[3]) { | ||||||
|  |     ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); | ||||||
|  |     this->handle_periodic_data_(); | ||||||
|  |     this->buffer_pos_ = 0;  // Reset position index for next message | ||||||
|  |   } else if (this->buffer_data_[this->buffer_pos_ - 4] == CMD_FRAME_FOOTER[0] && | ||||||
|  |              this->buffer_data_[this->buffer_pos_ - 3] == CMD_FRAME_FOOTER[1] && | ||||||
|  |              this->buffer_data_[this->buffer_pos_ - 2] == CMD_FRAME_FOOTER[2] && | ||||||
|  |              this->buffer_data_[this->buffer_pos_ - 1] == CMD_FRAME_FOOTER[3]) { | ||||||
|  |     ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str()); | ||||||
|  |     if (this->handle_ack_data_()) { | ||||||
|  |       this->buffer_pos_ = 0;  // Reset position index for next message | ||||||
|     } else { |     } else { | ||||||
|       pos = 0; |       ESP_LOGV(TAG, "Ack Data incomplete"); | ||||||
|     } |  | ||||||
|     if (pos >= 4) { |  | ||||||
|       if (buffer[pos - 4] == 0xF8 && buffer[pos - 3] == 0xF7 && buffer[pos - 2] == 0xF6 && buffer[pos - 1] == 0xF5) { |  | ||||||
|         ESP_LOGV(TAG, "Will handle Periodic Data"); |  | ||||||
|         this->handle_periodic_data_(buffer, pos); |  | ||||||
|         pos = 0;  // Reset position index ready for next time |  | ||||||
|       } else if (buffer[pos - 4] == 0x04 && buffer[pos - 3] == 0x03 && buffer[pos - 2] == 0x02 && |  | ||||||
|                  buffer[pos - 1] == 0x01) { |  | ||||||
|         ESP_LOGV(TAG, "Will handle ACK Data"); |  | ||||||
|         if (this->handle_ack_data_(buffer, pos)) { |  | ||||||
|           pos = 0;  // Reset position index ready for next time |  | ||||||
|         } else { |  | ||||||
|           ESP_LOGV(TAG, "ACK Data incomplete"); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::set_config_mode_(bool enable) { | void LD2410Component::set_config_mode_(bool enable) { | ||||||
|   uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; |   const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF; | ||||||
|   uint8_t cmd_value[2] = {0x01, 0x00}; |   const uint8_t cmd_value[2] = {0x01, 0x00}; | ||||||
|   this->send_command_(cmd, enable ? cmd_value : nullptr, 2); |   this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value)); | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::set_bluetooth(bool enable) { | void LD2410Component::set_bluetooth(bool enable) { | ||||||
|   this->set_config_mode_(true); |   this->set_config_mode_(true); | ||||||
|   uint8_t enable_cmd_value[2] = {0x01, 0x00}; |   const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00}; | ||||||
|   uint8_t disable_cmd_value[2] = {0x00, 0x00}; |   this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value)); | ||||||
|   this->send_command_(CMD_BLUETOOTH, enable ? enable_cmd_value : disable_cmd_value, 2); |  | ||||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); |   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::set_distance_resolution(const std::string &state) { | void LD2410Component::set_distance_resolution(const std::string &state) { | ||||||
|   this->set_config_mode_(true); |   this->set_config_mode_(true); | ||||||
|   uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00}; |   const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00}; | ||||||
|   this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, 2); |   this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); | ||||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); |   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::set_baud_rate(const std::string &state) { | void LD2410Component::set_baud_rate(const std::string &state) { | ||||||
|   this->set_config_mode_(true); |   this->set_config_mode_(true); | ||||||
|   uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; |   const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; | ||||||
|   this->send_command_(CMD_SET_BAUD_RATE, cmd_value, 2); |   this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); | ||||||
|   this->set_timeout(200, [this]() { this->restart_(); }); |   this->set_timeout(200, [this]() { this->restart_(); }); | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -661,14 +711,13 @@ void LD2410Component::set_bluetooth_password(const std::string &password) { | |||||||
|   this->set_config_mode_(true); |   this->set_config_mode_(true); | ||||||
|   uint8_t cmd_value[6]; |   uint8_t cmd_value[6]; | ||||||
|   std::copy(password.begin(), password.end(), std::begin(cmd_value)); |   std::copy(password.begin(), password.end(), std::begin(cmd_value)); | ||||||
|   this->send_command_(CMD_BT_PASSWORD, cmd_value, 6); |   this->send_command_(CMD_BT_PASSWORD, cmd_value, sizeof(cmd_value)); | ||||||
|   this->set_config_mode_(false); |   this->set_config_mode_(false); | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::set_engineering_mode(bool enable) { | void LD2410Component::set_engineering_mode(bool enable) { | ||||||
|  |   const uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG; | ||||||
|   this->set_config_mode_(true); |   this->set_config_mode_(true); | ||||||
|   last_engineering_mode_change_millis_ = App.get_loop_component_start_time(); |  | ||||||
|   uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG; |  | ||||||
|   this->send_command_(cmd, nullptr, 0); |   this->send_command_(cmd, nullptr, 0); | ||||||
|   this->set_config_mode_(false); |   this->set_config_mode_(false); | ||||||
| } | } | ||||||
| @@ -682,14 +731,17 @@ void LD2410Component::factory_reset() { | |||||||
| void LD2410Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } | void LD2410Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); } | ||||||
|  |  | ||||||
| void LD2410Component::query_parameters_() { this->send_command_(CMD_QUERY, nullptr, 0); } | void LD2410Component::query_parameters_() { this->send_command_(CMD_QUERY, nullptr, 0); } | ||||||
| void LD2410Component::get_version_() { this->send_command_(CMD_VERSION, nullptr, 0); } |  | ||||||
|  | void LD2410Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); } | ||||||
|  |  | ||||||
| void LD2410Component::get_mac_() { | void LD2410Component::get_mac_() { | ||||||
|   uint8_t cmd_value[2] = {0x01, 0x00}; |   const uint8_t cmd_value[2] = {0x01, 0x00}; | ||||||
|   this->send_command_(CMD_MAC, cmd_value, 2); |   this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, sizeof(cmd_value)); | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::get_distance_resolution_() { this->send_command_(CMD_QUERY_DISTANCE_RESOLUTION, nullptr, 0); } | void LD2410Component::get_distance_resolution_() { this->send_command_(CMD_QUERY_DISTANCE_RESOLUTION, nullptr, 0); } | ||||||
|  |  | ||||||
| void LD2410Component::get_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); } | void LD2410Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); } | ||||||
|  |  | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
| void LD2410Component::set_max_distances_timeout() { | void LD2410Component::set_max_distances_timeout() { | ||||||
| @@ -719,7 +771,7 @@ void LD2410Component::set_max_distances_timeout() { | |||||||
|                        0x00, |                        0x00, | ||||||
|                        0x00}; |                        0x00}; | ||||||
|   this->set_config_mode_(true); |   this->set_config_mode_(true); | ||||||
|   this->send_command_(CMD_MAXDIST_DURATION, value, 18); |   this->send_command_(CMD_MAXDIST_DURATION, value, sizeof(value)); | ||||||
|   delay(50);  // NOLINT |   delay(50);  // NOLINT | ||||||
|   this->query_parameters_(); |   this->query_parameters_(); | ||||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); |   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||||
| @@ -749,17 +801,17 @@ void LD2410Component::set_gate_threshold(uint8_t gate) { | |||||||
|   uint8_t value[18] = {0x00, 0x00, lowbyte(gate),   highbyte(gate),   0x00, 0x00, |   uint8_t value[18] = {0x00, 0x00, lowbyte(gate),   highbyte(gate),   0x00, 0x00, | ||||||
|                        0x01, 0x00, lowbyte(motion), highbyte(motion), 0x00, 0x00, |                        0x01, 0x00, lowbyte(motion), highbyte(motion), 0x00, 0x00, | ||||||
|                        0x02, 0x00, lowbyte(still),  highbyte(still),  0x00, 0x00}; |                        0x02, 0x00, lowbyte(still),  highbyte(still),  0x00, 0x00}; | ||||||
|   this->send_command_(CMD_GATE_SENS, value, 18); |   this->send_command_(CMD_GATE_SENS, value, sizeof(value)); | ||||||
|   delay(50);  // NOLINT |   delay(50);  // NOLINT | ||||||
|   this->query_parameters_(); |   this->query_parameters_(); | ||||||
|   this->set_config_mode_(false); |   this->set_config_mode_(false); | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::set_gate_still_threshold_number(int gate, number::Number *n) { | void LD2410Component::set_gate_still_threshold_number(uint8_t gate, number::Number *n) { | ||||||
|   this->gate_still_threshold_numbers_[gate] = n; |   this->gate_still_threshold_numbers_[gate] = n; | ||||||
| } | } | ||||||
|  |  | ||||||
| void LD2410Component::set_gate_move_threshold_number(int gate, number::Number *n) { | void LD2410Component::set_gate_move_threshold_number(uint8_t gate, number::Number *n) { | ||||||
|   this->gate_move_threshold_numbers_[gate] = n; |   this->gate_move_threshold_numbers_[gate] = n; | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| @@ -767,35 +819,29 @@ void LD2410Component::set_gate_move_threshold_number(int gate, number::Number *n | |||||||
| void LD2410Component::set_light_out_control() { | void LD2410Component::set_light_out_control() { | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
|   if (this->light_threshold_number_ != nullptr && this->light_threshold_number_->has_state()) { |   if (this->light_threshold_number_ != nullptr && this->light_threshold_number_->has_state()) { | ||||||
|     this->light_threshold_ = this->light_threshold_number_->state; |     this->light_threshold_ = static_cast<uint8_t>(this->light_threshold_number_->state); | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|   if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { |   if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { | ||||||
|     this->light_function_ = this->light_function_select_->state; |     this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state); | ||||||
|   } |   } | ||||||
|   if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) { |   if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) { | ||||||
|     this->out_pin_level_ = this->out_pin_level_select_->state; |     this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state); | ||||||
|   } |   } | ||||||
| #endif | #endif | ||||||
|   if (this->light_function_.empty() || this->out_pin_level_.empty() || this->light_threshold_ < 0) { |  | ||||||
|     return; |  | ||||||
|   } |  | ||||||
|   this->set_config_mode_(true); |   this->set_config_mode_(true); | ||||||
|   uint8_t light_function = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_); |   uint8_t value[4] = {this->light_function_, this->light_threshold_, this->out_pin_level_, 0x00}; | ||||||
|   uint8_t light_threshold = static_cast<uint8_t>(this->light_threshold_); |   this->send_command_(CMD_SET_LIGHT_CONTROL, value, sizeof(value)); | ||||||
|   uint8_t out_pin_level = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_); |  | ||||||
|   uint8_t value[4] = {light_function, light_threshold, out_pin_level, 0x00}; |  | ||||||
|   this->send_command_(CMD_SET_LIGHT_CONTROL, value, 4); |  | ||||||
|   delay(50);  // NOLINT |   delay(50);  // NOLINT | ||||||
|   this->get_light_control_(); |   this->query_light_control_(); | ||||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); |   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||||
|   this->set_config_mode_(false); |   this->set_config_mode_(false); | ||||||
| } | } | ||||||
|  |  | ||||||
| #ifdef USE_SENSOR | #ifdef USE_SENSOR | ||||||
| void LD2410Component::set_gate_move_sensor(int gate, sensor::Sensor *s) { this->gate_move_sensors_[gate] = s; } | void LD2410Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) { this->gate_move_sensors_[gate] = s; } | ||||||
| void LD2410Component::set_gate_still_sensor(int gate, sensor::Sensor *s) { this->gate_still_sensors_[gate] = s; } | void LD2410Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) { this->gate_still_sensors_[gate] = s; } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| }  // namespace ld2410 | }  // namespace ld2410 | ||||||
|   | |||||||
| @@ -29,45 +29,48 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace ld2410 { | namespace ld2410 { | ||||||
|  |  | ||||||
|  | static const uint8_t MAX_LINE_LENGTH = 46;  // Max characters for serial buffer | ||||||
|  | static const uint8_t TOTAL_GATES = 9;       // Total number of gates supported by the LD2410 | ||||||
|  |  | ||||||
| class LD2410Component : public Component, public uart::UARTDevice { | class LD2410Component : public Component, public uart::UARTDevice { | ||||||
| #ifdef USE_SENSOR |  | ||||||
|   SUB_SENSOR(moving_target_distance) |  | ||||||
|   SUB_SENSOR(still_target_distance) |  | ||||||
|   SUB_SENSOR(moving_target_energy) |  | ||||||
|   SUB_SENSOR(still_target_energy) |  | ||||||
|   SUB_SENSOR(light) |  | ||||||
|   SUB_SENSOR(detection_distance) |  | ||||||
| #endif |  | ||||||
| #ifdef USE_BINARY_SENSOR | #ifdef USE_BINARY_SENSOR | ||||||
|   SUB_BINARY_SENSOR(target) |   SUB_BINARY_SENSOR(out_pin_presence_status) | ||||||
|   SUB_BINARY_SENSOR(moving_target) |   SUB_BINARY_SENSOR(moving_target) | ||||||
|   SUB_BINARY_SENSOR(still_target) |   SUB_BINARY_SENSOR(still_target) | ||||||
|   SUB_BINARY_SENSOR(out_pin_presence_status) |   SUB_BINARY_SENSOR(target) | ||||||
|  | #endif | ||||||
|  | #ifdef USE_SENSOR | ||||||
|  |   SUB_SENSOR(light) | ||||||
|  |   SUB_SENSOR(detection_distance) | ||||||
|  |   SUB_SENSOR(moving_target_distance) | ||||||
|  |   SUB_SENSOR(moving_target_energy) | ||||||
|  |   SUB_SENSOR(still_target_distance) | ||||||
|  |   SUB_SENSOR(still_target_energy) | ||||||
| #endif | #endif | ||||||
| #ifdef USE_TEXT_SENSOR | #ifdef USE_TEXT_SENSOR | ||||||
|   SUB_TEXT_SENSOR(version) |   SUB_TEXT_SENSOR(version) | ||||||
|   SUB_TEXT_SENSOR(mac) |   SUB_TEXT_SENSOR(mac) | ||||||
| #endif | #endif | ||||||
|  | #ifdef USE_NUMBER | ||||||
|  |   SUB_NUMBER(light_threshold) | ||||||
|  |   SUB_NUMBER(max_move_distance_gate) | ||||||
|  |   SUB_NUMBER(max_still_distance_gate) | ||||||
|  |   SUB_NUMBER(timeout) | ||||||
|  | #endif | ||||||
| #ifdef USE_SELECT | #ifdef USE_SELECT | ||||||
|   SUB_SELECT(distance_resolution) |  | ||||||
|   SUB_SELECT(baud_rate) |   SUB_SELECT(baud_rate) | ||||||
|  |   SUB_SELECT(distance_resolution) | ||||||
|   SUB_SELECT(light_function) |   SUB_SELECT(light_function) | ||||||
|   SUB_SELECT(out_pin_level) |   SUB_SELECT(out_pin_level) | ||||||
| #endif | #endif | ||||||
| #ifdef USE_SWITCH | #ifdef USE_SWITCH | ||||||
|   SUB_SWITCH(engineering_mode) |  | ||||||
|   SUB_SWITCH(bluetooth) |   SUB_SWITCH(bluetooth) | ||||||
|  |   SUB_SWITCH(engineering_mode) | ||||||
| #endif | #endif | ||||||
| #ifdef USE_BUTTON | #ifdef USE_BUTTON | ||||||
|   SUB_BUTTON(reset) |   SUB_BUTTON(factory_reset) | ||||||
|   SUB_BUTTON(restart) |  | ||||||
|   SUB_BUTTON(query) |   SUB_BUTTON(query) | ||||||
| #endif |   SUB_BUTTON(restart) | ||||||
| #ifdef USE_NUMBER |  | ||||||
|   SUB_NUMBER(max_still_distance_gate) |  | ||||||
|   SUB_NUMBER(max_move_distance_gate) |  | ||||||
|   SUB_NUMBER(timeout) |  | ||||||
|   SUB_NUMBER(light_threshold) |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  public: |  public: | ||||||
| @@ -76,14 +79,14 @@ class LD2410Component : public Component, public uart::UARTDevice { | |||||||
|   void loop() override; |   void loop() override; | ||||||
|   void set_light_out_control(); |   void set_light_out_control(); | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
|   void set_gate_still_threshold_number(int gate, number::Number *n); |   void set_gate_still_threshold_number(uint8_t gate, number::Number *n); | ||||||
|   void set_gate_move_threshold_number(int gate, number::Number *n); |   void set_gate_move_threshold_number(uint8_t gate, number::Number *n); | ||||||
|   void set_max_distances_timeout(); |   void set_max_distances_timeout(); | ||||||
|   void set_gate_threshold(uint8_t gate); |   void set_gate_threshold(uint8_t gate); | ||||||
| #endif | #endif | ||||||
| #ifdef USE_SENSOR | #ifdef USE_SENSOR | ||||||
|   void set_gate_move_sensor(int gate, sensor::Sensor *s); |   void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s); | ||||||
|   void set_gate_still_sensor(int gate, sensor::Sensor *s); |   void set_gate_still_sensor(uint8_t gate, sensor::Sensor *s); | ||||||
| #endif | #endif | ||||||
|   void set_throttle(uint16_t value) { this->throttle_ = value; }; |   void set_throttle(uint16_t value) { this->throttle_ = value; }; | ||||||
|   void set_bluetooth_password(const std::string &password); |   void set_bluetooth_password(const std::string &password); | ||||||
| @@ -96,33 +99,35 @@ class LD2410Component : public Component, public uart::UARTDevice { | |||||||
|   void factory_reset(); |   void factory_reset(); | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void send_command_(uint8_t command_str, const uint8_t *command_value, int command_value_len); |   void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); | ||||||
|   void set_config_mode_(bool enable); |   void set_config_mode_(bool enable); | ||||||
|   void handle_periodic_data_(uint8_t *buffer, int len); |   void handle_periodic_data_(); | ||||||
|   bool handle_ack_data_(uint8_t *buffer, int len); |   bool handle_ack_data_(); | ||||||
|   void readline_(int readch, uint8_t *buffer, int len); |   void readline_(int readch); | ||||||
|   void query_parameters_(); |   void query_parameters_(); | ||||||
|   void get_version_(); |   void get_version_(); | ||||||
|   void get_mac_(); |   void get_mac_(); | ||||||
|   void get_distance_resolution_(); |   void get_distance_resolution_(); | ||||||
|   void get_light_control_(); |   void query_light_control_(); | ||||||
|   void restart_(); |   void restart_(); | ||||||
|  |  | ||||||
|   int32_t last_periodic_millis_ = 0; |   uint32_t last_periodic_millis_ = 0; | ||||||
|   int32_t last_engineering_mode_change_millis_ = 0; |   uint16_t throttle_ = 0; | ||||||
|   uint16_t throttle_; |   uint8_t light_function_ = 0; | ||||||
|   float light_threshold_ = -1; |   uint8_t light_threshold_ = 0; | ||||||
|   std::string version_; |   uint8_t out_pin_level_ = 0; | ||||||
|   std::string mac_; |   uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer | ||||||
|   std::string out_pin_level_; |   uint8_t buffer_data_[MAX_LINE_LENGTH]; | ||||||
|   std::string light_function_; |   uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0}; | ||||||
|  |   uint8_t version_[6] = {0, 0, 0, 0, 0, 0}; | ||||||
|  |   bool bluetooth_on_{false}; | ||||||
| #ifdef USE_NUMBER | #ifdef USE_NUMBER | ||||||
|   std::vector<number::Number *> gate_still_threshold_numbers_ = std::vector<number::Number *>(9); |   std::vector<number::Number *> gate_move_threshold_numbers_ = std::vector<number::Number *>(TOTAL_GATES); | ||||||
|   std::vector<number::Number *> gate_move_threshold_numbers_ = std::vector<number::Number *>(9); |   std::vector<number::Number *> gate_still_threshold_numbers_ = std::vector<number::Number *>(TOTAL_GATES); | ||||||
| #endif | #endif | ||||||
| #ifdef USE_SENSOR | #ifdef USE_SENSOR | ||||||
|   std::vector<sensor::Sensor *> gate_still_sensors_ = std::vector<sensor::Sensor *>(9); |   std::vector<sensor::Sensor *> gate_move_sensors_ = std::vector<sensor::Sensor *>(TOTAL_GATES); | ||||||
|   std::vector<sensor::Sensor *> gate_move_sensors_ = std::vector<sensor::Sensor *>(9); |   std::vector<sensor::Sensor *> gate_still_sensors_ = std::vector<sensor::Sensor *>(TOTAL_GATES); | ||||||
| #endif | #endif | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,6 +6,7 @@ | |||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
| #include "esphome/core/preferences.h" | #include "esphome/core/preferences.h" | ||||||
| #include <limits> | #include <limits> | ||||||
|  | #include <cmath> | ||||||
| #ifdef USE_SENSOR | #ifdef USE_SENSOR | ||||||
| #include "esphome/components/sensor/sensor.h" | #include "esphome/components/sensor/sensor.h" | ||||||
| #endif | #endif | ||||||
| @@ -101,7 +102,7 @@ class LD2450Component : public Component, public uart::UARTDevice { | |||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void loop() override; |   void loop() override; | ||||||
|   void set_presence_timeout(); |   void set_presence_timeout(); | ||||||
|   void set_throttle(uint16_t value) { this->throttle_ = value; }; |   void set_throttle(uint16_t value) { this->throttle_ = value; } | ||||||
|   void read_all_info(); |   void read_all_info(); | ||||||
|   void query_zone_info(); |   void query_zone_info(); | ||||||
|   void restart_and_read_all_info(); |   void restart_and_read_all_info(); | ||||||
|   | |||||||
| @@ -167,6 +167,7 @@ async def to_code(config): | |||||||
|         cg.add(var.set_wake_up_page(config[CONF_WAKE_UP_PAGE])) |         cg.add(var.set_wake_up_page(config[CONF_WAKE_UP_PAGE])) | ||||||
|  |  | ||||||
|     if CONF_START_UP_PAGE in config: |     if CONF_START_UP_PAGE in config: | ||||||
|  |         cg.add_define("USE_NEXTION_CONF_START_UP_PAGE") | ||||||
|         cg.add(var.set_start_up_page(config[CONF_START_UP_PAGE])) |         cg.add(var.set_start_up_page(config[CONF_START_UP_PAGE])) | ||||||
|  |  | ||||||
|     cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH])) |     cg.add(var.set_auto_wake_on_touch(config[CONF_AUTO_WAKE_ON_TOUCH])) | ||||||
|   | |||||||
| @@ -167,13 +167,15 @@ void Nextion::dump_config() { | |||||||
|     ESP_LOGCONFIG(TAG, "  Touch Timeout:  %" PRIu16, this->touch_sleep_timeout_); |     ESP_LOGCONFIG(TAG, "  Touch Timeout:  %" PRIu16, this->touch_sleep_timeout_); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (this->wake_up_page_ != -1) { |   if (this->wake_up_page_ != 255) { | ||||||
|     ESP_LOGCONFIG(TAG, "  Wake Up Page:   %d", this->wake_up_page_); |     ESP_LOGCONFIG(TAG, "  Wake Up Page:   %u", this->wake_up_page_); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (this->start_up_page_ != -1) { | #ifdef USE_NEXTION_CONF_START_UP_PAGE | ||||||
|     ESP_LOGCONFIG(TAG, "  Start Up Page:  %d", this->start_up_page_); |   if (this->start_up_page_ != 255) { | ||||||
|  |     ESP_LOGCONFIG(TAG, "  Start Up Page:  %u", this->start_up_page_); | ||||||
|   } |   } | ||||||
|  | #endif  // USE_NEXTION_CONF_START_UP_PAGE | ||||||
|  |  | ||||||
| #ifdef USE_NEXTION_COMMAND_SPACING | #ifdef USE_NEXTION_COMMAND_SPACING | ||||||
|   ESP_LOGCONFIG(TAG, "  Cmd spacing:      %u ms", this->command_pacer_.get_spacing()); |   ESP_LOGCONFIG(TAG, "  Cmd spacing:      %u ms", this->command_pacer_.get_spacing()); | ||||||
| @@ -301,12 +303,14 @@ void Nextion::loop() { | |||||||
|       this->set_backlight_brightness(this->brightness_.value()); |       this->set_backlight_brightness(this->brightness_.value()); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  | #ifdef USE_NEXTION_CONF_START_UP_PAGE | ||||||
|     // Check if a startup page has been set and send the command |     // Check if a startup page has been set and send the command | ||||||
|     if (this->start_up_page_ >= 0) { |     if (this->start_up_page_ != 255) { | ||||||
|       this->goto_page(this->start_up_page_); |       this->goto_page(this->start_up_page_); | ||||||
|     } |     } | ||||||
|  | #endif  // USE_NEXTION_CONF_START_UP_PAGE | ||||||
|  |  | ||||||
|     if (this->wake_up_page_ >= 0) { |     if (this->wake_up_page_ != 255) { | ||||||
|       this->set_wake_up_page(this->wake_up_page_); |       this->set_wake_up_page(this->wake_up_page_); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1194,7 +1194,7 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | |||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode. |    * Sets which page Nextion loads when exiting sleep mode. Note this can be set even when Nextion is in sleep mode. | ||||||
|    * @param wake_up_page The page id, from 0 to the last page in Nextion. Set -1 (not set to any existing page) to |    * @param wake_up_page The page id, from 0 to the last page in Nextion. Set 255 (not set to any existing page) to | ||||||
|    * wakes up to current page. |    * wakes up to current page. | ||||||
|    * |    * | ||||||
|    * Example: |    * Example: | ||||||
| @@ -1204,11 +1204,12 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | |||||||
|    * |    * | ||||||
|    * The display will wake up to page 2. |    * The display will wake up to page 2. | ||||||
|    */ |    */ | ||||||
|   void set_wake_up_page(int16_t wake_up_page = -1); |   void set_wake_up_page(uint8_t wake_up_page = 255); | ||||||
|  |  | ||||||
|  | #ifdef USE_NEXTION_CONF_START_UP_PAGE | ||||||
|   /** |   /** | ||||||
|    * Sets which page Nextion loads when connecting to ESPHome. |    * Sets which page Nextion loads when connecting to ESPHome. | ||||||
|    * @param start_up_page The page id, from 0 to the last page in Nextion. Set -1 (not set to any existing page) to |    * @param start_up_page The page id, from 0 to the last page in Nextion. Set 255 (not set to any existing page) to | ||||||
|    * wakes up to current page. |    * wakes up to current page. | ||||||
|    * |    * | ||||||
|    * Example: |    * Example: | ||||||
| @@ -1218,7 +1219,8 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | |||||||
|    * |    * | ||||||
|    * The display will go to page 2 when it establishes a connection to ESPHome. |    * The display will go to page 2 when it establishes a connection to ESPHome. | ||||||
|    */ |    */ | ||||||
|   void set_start_up_page(int16_t start_up_page = -1) { this->start_up_page_ = start_up_page; } |   void set_start_up_page(uint8_t start_up_page = 255) { this->start_up_page_ = start_up_page; } | ||||||
|  | #endif  // USE_NEXTION_CONF_START_UP_PAGE | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Sets if Nextion should auto-wake from sleep when touch press occurs. |    * Sets if Nextion should auto-wake from sleep when touch press occurs. | ||||||
| @@ -1344,8 +1346,10 @@ class Nextion : public NextionBase, public PollingComponent, public uart::UARTDe | |||||||
|   void process_serial_(); |   void process_serial_(); | ||||||
|   bool is_updating_ = false; |   bool is_updating_ = false; | ||||||
|   uint16_t touch_sleep_timeout_ = 0; |   uint16_t touch_sleep_timeout_ = 0; | ||||||
|   int16_t wake_up_page_ = -1; |   uint8_t wake_up_page_ = 255; | ||||||
|   int16_t start_up_page_ = -1; | #ifdef USE_NEXTION_CONF_START_UP_PAGE | ||||||
|  |   uint8_t start_up_page_ = 255; | ||||||
|  | #endif  // USE_NEXTION_CONF_START_UP_PAGE | ||||||
|   bool auto_wake_on_touch_ = true; |   bool auto_wake_on_touch_ = true; | ||||||
|   bool exit_reparse_on_start_ = false; |   bool exit_reparse_on_start_ = false; | ||||||
|   bool skip_connection_handshake_ = false; |   bool skip_connection_handshake_ = false; | ||||||
|   | |||||||
| @@ -10,7 +10,7 @@ static const char *const TAG = "nextion"; | |||||||
| // Sleep safe commands | // Sleep safe commands | ||||||
| void Nextion::soft_reset() { this->send_command_("rest"); } | void Nextion::soft_reset() { this->send_command_("rest"); } | ||||||
|  |  | ||||||
| void Nextion::set_wake_up_page(int16_t wake_up_page) { | void Nextion::set_wake_up_page(uint8_t wake_up_page) { | ||||||
|   this->wake_up_page_ = wake_up_page; |   this->wake_up_page_ = wake_up_page; | ||||||
|   this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true); |   this->add_no_result_to_queue_with_set_internal_("wake_up_page", "wup", wake_up_page, true); | ||||||
| } | } | ||||||
|   | |||||||
| @@ -7,6 +7,8 @@ namespace scd4x { | |||||||
|  |  | ||||||
| static const char *const TAG = "scd4x"; | static const char *const TAG = "scd4x"; | ||||||
|  |  | ||||||
|  | static const uint16_t SCD41_ID = 0x1408; | ||||||
|  | static const uint16_t SCD40_ID = 0x440; | ||||||
| static const uint16_t SCD4X_CMD_GET_SERIAL_NUMBER = 0x3682; | static const uint16_t SCD4X_CMD_GET_SERIAL_NUMBER = 0x3682; | ||||||
| static const uint16_t SCD4X_CMD_TEMPERATURE_OFFSET = 0x241d; | static const uint16_t SCD4X_CMD_TEMPERATURE_OFFSET = 0x241d; | ||||||
| static const uint16_t SCD4X_CMD_ALTITUDE_COMPENSATION = 0x2427; | static const uint16_t SCD4X_CMD_ALTITUDE_COMPENSATION = 0x2427; | ||||||
| @@ -23,8 +25,6 @@ static const uint16_t SCD4X_CMD_STOP_MEASUREMENTS = 0x3f86; | |||||||
| static const uint16_t SCD4X_CMD_FACTORY_RESET = 0x3632; | static const uint16_t SCD4X_CMD_FACTORY_RESET = 0x3632; | ||||||
| static const uint16_t SCD4X_CMD_GET_FEATURESET = 0x202f; | static const uint16_t SCD4X_CMD_GET_FEATURESET = 0x202f; | ||||||
| static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f; | static const float SCD4X_TEMPERATURE_OFFSET_MULTIPLIER = (1 << 16) / 175.0f; | ||||||
| static const uint16_t SCD41_ID = 0x1408; |  | ||||||
| static const uint16_t SCD40_ID = 0x440; |  | ||||||
|  |  | ||||||
| void SCD4XComponent::setup() { | void SCD4XComponent::setup() { | ||||||
|   ESP_LOGCONFIG(TAG, "Running setup"); |   ESP_LOGCONFIG(TAG, "Running setup"); | ||||||
| @@ -51,47 +51,66 @@ void SCD4XComponent::setup() { | |||||||
|  |  | ||||||
|       if (!this->write_command(SCD4X_CMD_TEMPERATURE_OFFSET, |       if (!this->write_command(SCD4X_CMD_TEMPERATURE_OFFSET, | ||||||
|                                (uint16_t) (temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) { |                                (uint16_t) (temperature_offset_ * SCD4X_TEMPERATURE_OFFSET_MULTIPLIER))) { | ||||||
|         ESP_LOGE(TAG, "Error setting temperature offset."); |         ESP_LOGE(TAG, "Error setting temperature offset"); | ||||||
|         this->error_code_ = MEASUREMENT_INIT_FAILED; |         this->error_code_ = MEASUREMENT_INIT_FAILED; | ||||||
|         this->mark_failed(); |         this->mark_failed(); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // If pressure compensation available use it |       // If pressure compensation available use it, else use altitude | ||||||
|       // else use altitude |       if (this->ambient_pressure_compensation_) { | ||||||
|       if (ambient_pressure_compensation_) { |         if (!this->update_ambient_pressure_compensation_(this->ambient_pressure_)) { | ||||||
|         if (!this->update_ambient_pressure_compensation_(ambient_pressure_)) { |           ESP_LOGE(TAG, "Error setting ambient pressure compensation"); | ||||||
|           ESP_LOGE(TAG, "Error setting ambient pressure compensation."); |  | ||||||
|           this->error_code_ = MEASUREMENT_INIT_FAILED; |           this->error_code_ = MEASUREMENT_INIT_FAILED; | ||||||
|           this->mark_failed(); |           this->mark_failed(); | ||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|       } else { |       } else { | ||||||
|         if (!this->write_command(SCD4X_CMD_ALTITUDE_COMPENSATION, altitude_compensation_)) { |         if (!this->write_command(SCD4X_CMD_ALTITUDE_COMPENSATION, this->altitude_compensation_)) { | ||||||
|           ESP_LOGE(TAG, "Error setting altitude compensation."); |           ESP_LOGE(TAG, "Error setting altitude compensation"); | ||||||
|           this->error_code_ = MEASUREMENT_INIT_FAILED; |           this->error_code_ = MEASUREMENT_INIT_FAILED; | ||||||
|           this->mark_failed(); |           this->mark_failed(); | ||||||
|           return; |           return; | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (!this->write_command(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, enable_asc_ ? 1 : 0)) { |       if (!this->write_command(SCD4X_CMD_AUTOMATIC_SELF_CALIBRATION, this->enable_asc_ ? 1 : 0)) { | ||||||
|         ESP_LOGE(TAG, "Error setting automatic self calibration."); |         ESP_LOGE(TAG, "Error setting automatic self calibration"); | ||||||
|         this->error_code_ = MEASUREMENT_INIT_FAILED; |         this->error_code_ = MEASUREMENT_INIT_FAILED; | ||||||
|         this->mark_failed(); |         this->mark_failed(); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       initialized_ = true; |       this->initialized_ = true; | ||||||
|       // Finally start sensor measurements |       // Finally start sensor measurements | ||||||
|       this->start_measurement_(); |       this->start_measurement_(); | ||||||
|       ESP_LOGD(TAG, "Sensor initialized"); |  | ||||||
|     }); |     }); | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| void SCD4XComponent::dump_config() { | void SCD4XComponent::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, "scd4x:"); |   static const char *const MM_PERIODIC_STR = "Periodic (5s)"; | ||||||
|  |   static const char *const MM_LOW_POWER_PERIODIC_STR = "Low power periodic (30s)"; | ||||||
|  |   static const char *const MM_SINGLE_SHOT_STR = "Single shot"; | ||||||
|  |   static const char *const MM_SINGLE_SHOT_RHT_ONLY_STR = "Single shot rht only"; | ||||||
|  |   const char *measurement_mode_str = MM_PERIODIC_STR; | ||||||
|  |  | ||||||
|  |   switch (this->measurement_mode_) { | ||||||
|  |     case PERIODIC: | ||||||
|  |       // measurement_mode_str = MM_PERIODIC_STR; | ||||||
|  |       break; | ||||||
|  |     case LOW_POWER_PERIODIC: | ||||||
|  |       measurement_mode_str = MM_LOW_POWER_PERIODIC_STR; | ||||||
|  |       break; | ||||||
|  |     case SINGLE_SHOT: | ||||||
|  |       measurement_mode_str = MM_SINGLE_SHOT_STR; | ||||||
|  |       break; | ||||||
|  |     case SINGLE_SHOT_RHT_ONLY: | ||||||
|  |       measurement_mode_str = MM_SINGLE_SHOT_RHT_ONLY_STR; | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGCONFIG(TAG, "SCD4X:"); | ||||||
|   LOG_I2C_DEVICE(this); |   LOG_I2C_DEVICE(this); | ||||||
|   if (this->is_failed()) { |   if (this->is_failed()) { | ||||||
|     switch (this->error_code_) { |     switch (this->error_code_) { | ||||||
| @@ -102,16 +121,20 @@ void SCD4XComponent::dump_config() { | |||||||
|         ESP_LOGW(TAG, "Measurement Initialization failed"); |         ESP_LOGW(TAG, "Measurement Initialization failed"); | ||||||
|         break; |         break; | ||||||
|       case SERIAL_NUMBER_IDENTIFICATION_FAILED: |       case SERIAL_NUMBER_IDENTIFICATION_FAILED: | ||||||
|         ESP_LOGW(TAG, "Unable to read sensor firmware version"); |         ESP_LOGW(TAG, "Unable to read firmware version"); | ||||||
|         break; |         break; | ||||||
|       default: |       default: | ||||||
|         ESP_LOGW(TAG, "Unknown setup error"); |         ESP_LOGW(TAG, "Unknown setup error"); | ||||||
|         break; |         break; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   ESP_LOGCONFIG(TAG, "  Automatic self calibration: %s", ONOFF(this->enable_asc_)); |   ESP_LOGCONFIG(TAG, | ||||||
|  |                 "  Automatic self calibration: %s\n" | ||||||
|  |                 "  Measurement mode: %s\n" | ||||||
|  |                 "  Temperature offset: %.2f °C", | ||||||
|  |                 ONOFF(this->enable_asc_), measurement_mode_str, this->temperature_offset_); | ||||||
|   if (this->ambient_pressure_source_ != nullptr) { |   if (this->ambient_pressure_source_ != nullptr) { | ||||||
|     ESP_LOGCONFIG(TAG, "  Dynamic ambient pressure compensation using sensor '%s'", |     ESP_LOGCONFIG(TAG, "  Dynamic ambient pressure compensation using '%s'", | ||||||
|                   this->ambient_pressure_source_->get_name().c_str()); |                   this->ambient_pressure_source_->get_name().c_str()); | ||||||
|   } else { |   } else { | ||||||
|     if (this->ambient_pressure_compensation_) { |     if (this->ambient_pressure_compensation_) { | ||||||
| @@ -126,21 +149,6 @@ void SCD4XComponent::dump_config() { | |||||||
|                     this->altitude_compensation_); |                     this->altitude_compensation_); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|   switch (this->measurement_mode_) { |  | ||||||
|     case PERIODIC: |  | ||||||
|       ESP_LOGCONFIG(TAG, "  Measurement mode: periodic (5s)"); |  | ||||||
|       break; |  | ||||||
|     case LOW_POWER_PERIODIC: |  | ||||||
|       ESP_LOGCONFIG(TAG, "  Measurement mode: low power periodic (30s)"); |  | ||||||
|       break; |  | ||||||
|     case SINGLE_SHOT: |  | ||||||
|       ESP_LOGCONFIG(TAG, "  Measurement mode: single shot"); |  | ||||||
|       break; |  | ||||||
|     case SINGLE_SHOT_RHT_ONLY: |  | ||||||
|       ESP_LOGCONFIG(TAG, "  Measurement mode: single shot rht only"); |  | ||||||
|       break; |  | ||||||
|   } |  | ||||||
|   ESP_LOGCONFIG(TAG, "  Temperature offset: %.2f °C", this->temperature_offset_); |  | ||||||
|   LOG_UPDATE_INTERVAL(this); |   LOG_UPDATE_INTERVAL(this); | ||||||
|   LOG_SENSOR("  ", "CO2", this->co2_sensor_); |   LOG_SENSOR("  ", "CO2", this->co2_sensor_); | ||||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); |   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||||
| @@ -148,20 +156,20 @@ void SCD4XComponent::dump_config() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void SCD4XComponent::update() { | void SCD4XComponent::update() { | ||||||
|   if (!initialized_) { |   if (!this->initialized_) { | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (this->ambient_pressure_source_ != nullptr) { |   if (this->ambient_pressure_source_ != nullptr) { | ||||||
|     float pressure = this->ambient_pressure_source_->state; |     float pressure = this->ambient_pressure_source_->state; | ||||||
|     if (!std::isnan(pressure)) { |     if (!std::isnan(pressure)) { | ||||||
|       set_ambient_pressure_compensation(pressure); |       this->set_ambient_pressure_compensation(pressure); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   uint32_t wait_time = 0; |   uint32_t wait_time = 0; | ||||||
|   if (this->measurement_mode_ == SINGLE_SHOT || this->measurement_mode_ == SINGLE_SHOT_RHT_ONLY) { |   if (this->measurement_mode_ == SINGLE_SHOT || this->measurement_mode_ == SINGLE_SHOT_RHT_ONLY) { | ||||||
|     start_measurement_(); |     this->start_measurement_(); | ||||||
|     wait_time = |     wait_time = | ||||||
|         this->measurement_mode_ == SINGLE_SHOT ? 5000 : 50;  // Single shot measurement takes 5 secs rht mode 50 ms |         this->measurement_mode_ == SINGLE_SHOT ? 5000 : 50;  // Single shot measurement takes 5 secs rht mode 50 ms | ||||||
|   } |   } | ||||||
| @@ -176,12 +184,12 @@ void SCD4XComponent::update() { | |||||||
|  |  | ||||||
|     if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { |     if (!this->read_data(raw_read_status) || raw_read_status == 0x00) { | ||||||
|       this->status_set_warning(); |       this->status_set_warning(); | ||||||
|       ESP_LOGW(TAG, "Data not ready yet!"); |       ESP_LOGW(TAG, "Data not ready"); | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) { |     if (!this->write_command(SCD4X_CMD_READ_MEASUREMENT)) { | ||||||
|       ESP_LOGW(TAG, "Error reading measurement!"); |       ESP_LOGW(TAG, "Error reading measurement"); | ||||||
|       this->status_set_warning(); |       this->status_set_warning(); | ||||||
|       return;  // NO RETRY |       return;  // NO RETRY | ||||||
|     } |     } | ||||||
| @@ -218,7 +226,7 @@ bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentrati | |||||||
|   } |   } | ||||||
|   this->set_timeout(500, [this, current_co2_concentration]() { |   this->set_timeout(500, [this, current_co2_concentration]() { | ||||||
|     if (this->write_command(SCD4X_CMD_PERFORM_FORCED_CALIBRATION, current_co2_concentration)) { |     if (this->write_command(SCD4X_CMD_PERFORM_FORCED_CALIBRATION, current_co2_concentration)) { | ||||||
|       ESP_LOGD(TAG, "setting forced calibration Co2 level %d ppm", current_co2_concentration); |       ESP_LOGD(TAG, "Setting forced calibration Co2 level %d ppm", current_co2_concentration); | ||||||
|       // frc takes 400 ms |       // frc takes 400 ms | ||||||
|       // because this method will be used very rarly |       // because this method will be used very rarly | ||||||
|       // the simple approach with delay is ok |       // the simple approach with delay is ok | ||||||
| @@ -226,11 +234,11 @@ bool SCD4XComponent::perform_forced_calibration(uint16_t current_co2_concentrati | |||||||
|       if (!this->start_measurement_()) { |       if (!this->start_measurement_()) { | ||||||
|         return false; |         return false; | ||||||
|       } else { |       } else { | ||||||
|         ESP_LOGD(TAG, "forced calibration complete"); |         ESP_LOGD(TAG, "Forced calibration complete"); | ||||||
|       } |       } | ||||||
|       return true; |       return true; | ||||||
|     } else { |     } else { | ||||||
|       ESP_LOGE(TAG, "force calibration failed"); |       ESP_LOGE(TAG, "Force calibration failed"); | ||||||
|       this->error_code_ = FRC_FAILED; |       this->error_code_ = FRC_FAILED; | ||||||
|       this->status_set_warning(); |       this->status_set_warning(); | ||||||
|       return false; |       return false; | ||||||
| @@ -261,25 +269,25 @@ bool SCD4XComponent::factory_reset() { | |||||||
| void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_hpa) { | void SCD4XComponent::set_ambient_pressure_compensation(float pressure_in_hpa) { | ||||||
|   ambient_pressure_compensation_ = true; |   ambient_pressure_compensation_ = true; | ||||||
|   uint16_t new_ambient_pressure = (uint16_t) pressure_in_hpa; |   uint16_t new_ambient_pressure = (uint16_t) pressure_in_hpa; | ||||||
|   if (!initialized_) { |   if (!this->initialized_) { | ||||||
|     ambient_pressure_ = new_ambient_pressure; |     this->ambient_pressure_ = new_ambient_pressure; | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   // Only send pressure value if it has changed since last update |   // Only send pressure value if it has changed since last update | ||||||
|   if (new_ambient_pressure != ambient_pressure_) { |   if (new_ambient_pressure != this->ambient_pressure_) { | ||||||
|     update_ambient_pressure_compensation_(new_ambient_pressure); |     this->update_ambient_pressure_compensation_(new_ambient_pressure); | ||||||
|     ambient_pressure_ = new_ambient_pressure; |     this->ambient_pressure_ = new_ambient_pressure; | ||||||
|   } else { |   } else { | ||||||
|     ESP_LOGD(TAG, "ambient pressure compensation skipped - no change required"); |     ESP_LOGD(TAG, "Ambient pressure compensation skipped; no change required"); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_hpa) { | bool SCD4XComponent::update_ambient_pressure_compensation_(uint16_t pressure_in_hpa) { | ||||||
|   if (this->write_command(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, pressure_in_hpa)) { |   if (this->write_command(SCD4X_CMD_AMBIENT_PRESSURE_COMPENSATION, pressure_in_hpa)) { | ||||||
|     ESP_LOGD(TAG, "setting ambient pressure compensation to %d hPa", pressure_in_hpa); |     ESP_LOGD(TAG, "Setting ambient pressure compensation to %d hPa", pressure_in_hpa); | ||||||
|     return true; |     return true; | ||||||
|   } else { |   } else { | ||||||
|     ESP_LOGE(TAG, "Error setting ambient pressure compensation."); |     ESP_LOGE(TAG, "Error setting ambient pressure compensation"); | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -304,7 +312,7 @@ bool SCD4XComponent::start_measurement_() { | |||||||
|   static uint8_t remaining_retries = 3; |   static uint8_t remaining_retries = 3; | ||||||
|   while (remaining_retries) { |   while (remaining_retries) { | ||||||
|     if (!this->write_command(measurement_command)) { |     if (!this->write_command(measurement_command)) { | ||||||
|       ESP_LOGE(TAG, "Error starting measurements."); |       ESP_LOGE(TAG, "Error starting measurements"); | ||||||
|       this->error_code_ = MEASUREMENT_INIT_FAILED; |       this->error_code_ = MEASUREMENT_INIT_FAILED; | ||||||
|       this->status_set_warning(); |       this->status_set_warning(); | ||||||
|       if (--remaining_retries == 0) |       if (--remaining_retries == 0) | ||||||
|   | |||||||
| @@ -8,14 +8,20 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace scd4x { | namespace scd4x { | ||||||
|  |  | ||||||
| enum ERRORCODE { | enum ErrorCode : uint8_t { | ||||||
|   COMMUNICATION_FAILED, |   COMMUNICATION_FAILED, | ||||||
|   SERIAL_NUMBER_IDENTIFICATION_FAILED, |   SERIAL_NUMBER_IDENTIFICATION_FAILED, | ||||||
|   MEASUREMENT_INIT_FAILED, |   MEASUREMENT_INIT_FAILED, | ||||||
|   FRC_FAILED, |   FRC_FAILED, | ||||||
|   UNKNOWN |   UNKNOWN, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum MeasurementMode : uint8_t { | ||||||
|  |   PERIODIC, | ||||||
|  |   LOW_POWER_PERIODIC, | ||||||
|  |   SINGLE_SHOT, | ||||||
|  |   SINGLE_SHOT_RHT_ONLY, | ||||||
| }; | }; | ||||||
| enum MeasurementMode { PERIODIC, LOW_POWER_PERIODIC, SINGLE_SHOT, SINGLE_SHOT_RHT_ONLY }; |  | ||||||
|  |  | ||||||
| class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { | class SCD4XComponent : public PollingComponent, public sensirion_common::SensirionI2CDevice { | ||||||
|  public: |  public: | ||||||
| @@ -39,15 +45,14 @@ class SCD4XComponent : public PollingComponent, public sensirion_common::Sensiri | |||||||
|  protected: |  protected: | ||||||
|   bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa); |   bool update_ambient_pressure_compensation_(uint16_t pressure_in_hpa); | ||||||
|   bool start_measurement_(); |   bool start_measurement_(); | ||||||
|   ERRORCODE error_code_; |  | ||||||
|  |  | ||||||
|   bool initialized_{false}; |  | ||||||
|  |  | ||||||
|   float temperature_offset_; |  | ||||||
|   uint16_t altitude_compensation_; |   uint16_t altitude_compensation_; | ||||||
|   bool ambient_pressure_compensation_; |  | ||||||
|   uint16_t ambient_pressure_; |   uint16_t ambient_pressure_; | ||||||
|  |   bool initialized_{false}; | ||||||
|  |   bool ambient_pressure_compensation_; | ||||||
|   bool enable_asc_; |   bool enable_asc_; | ||||||
|  |   float temperature_offset_; | ||||||
|  |   ErrorCode error_code_; | ||||||
|   MeasurementMode measurement_mode_{PERIODIC}; |   MeasurementMode measurement_mode_{PERIODIC}; | ||||||
|   sensor::Sensor *co2_sensor_{nullptr}; |   sensor::Sensor *co2_sensor_{nullptr}; | ||||||
|   sensor::Sensor *temperature_sensor_{nullptr}; |   sensor::Sensor *temperature_sensor_{nullptr}; | ||||||
|   | |||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | scheduler_heap_stress_component_ns = cg.esphome_ns.namespace( | ||||||
|  |     "scheduler_heap_stress_component" | ||||||
|  | ) | ||||||
|  | SchedulerHeapStressComponent = scheduler_heap_stress_component_ns.class_( | ||||||
|  |     "SchedulerHeapStressComponent", cg.Component | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SchedulerHeapStressComponent), | ||||||
|  |     } | ||||||
|  | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
| @@ -0,0 +1,104 @@ | |||||||
|  | #include "heap_scheduler_stress_component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include <thread> | ||||||
|  | #include <atomic> | ||||||
|  | #include <vector> | ||||||
|  | #include <chrono> | ||||||
|  | #include <random> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_heap_stress_component { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "scheduler_heap_stress"; | ||||||
|  |  | ||||||
|  | void SchedulerHeapStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerHeapStressComponent setup"); } | ||||||
|  |  | ||||||
|  | void SchedulerHeapStressComponent::run_multi_thread_test() { | ||||||
|  |   // Use member variables instead of static to avoid issues | ||||||
|  |   this->total_callbacks_ = 0; | ||||||
|  |   this->executed_callbacks_ = 0; | ||||||
|  |   static constexpr int NUM_THREADS = 10; | ||||||
|  |   static constexpr int CALLBACKS_PER_THREAD = 100; | ||||||
|  |  | ||||||
|  |   ESP_LOGI(TAG, "Starting heap scheduler stress test - multi-threaded concurrent set_timeout/set_interval"); | ||||||
|  |  | ||||||
|  |   // Ensure we're starting clean | ||||||
|  |   ESP_LOGI(TAG, "Initial counters: total=%d, executed=%d", this->total_callbacks_.load(), | ||||||
|  |            this->executed_callbacks_.load()); | ||||||
|  |  | ||||||
|  |   // Track start time | ||||||
|  |   auto start_time = std::chrono::steady_clock::now(); | ||||||
|  |  | ||||||
|  |   // Create threads | ||||||
|  |   std::vector<std::thread> threads; | ||||||
|  |  | ||||||
|  |   ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks", NUM_THREADS, CALLBACKS_PER_THREAD); | ||||||
|  |  | ||||||
|  |   threads.reserve(NUM_THREADS); | ||||||
|  |   for (int i = 0; i < NUM_THREADS; i++) { | ||||||
|  |     threads.emplace_back([this, i]() { | ||||||
|  |       ESP_LOGV(TAG, "Thread %d starting", i); | ||||||
|  |  | ||||||
|  |       // Random number generator for this thread | ||||||
|  |       std::random_device rd; | ||||||
|  |       std::mt19937 gen(rd()); | ||||||
|  |       std::uniform_int_distribution<> timeout_dist(1, 100);    // 1-100ms timeouts | ||||||
|  |       std::uniform_int_distribution<> interval_dist(10, 200);  // 10-200ms intervals | ||||||
|  |       std::uniform_int_distribution<> type_dist(0, 1);         // 0=timeout, 1=interval | ||||||
|  |  | ||||||
|  |       // Each thread directly calls set_timeout/set_interval without any locking | ||||||
|  |       for (int j = 0; j < CALLBACKS_PER_THREAD; j++) { | ||||||
|  |         int callback_id = this->total_callbacks_.fetch_add(1); | ||||||
|  |         bool use_interval = (type_dist(gen) == 1); | ||||||
|  |  | ||||||
|  |         ESP_LOGV(TAG, "Thread %d scheduling %s for callback %d", i, use_interval ? "interval" : "timeout", callback_id); | ||||||
|  |  | ||||||
|  |         // Capture this pointer safely for the lambda | ||||||
|  |         auto *component = this; | ||||||
|  |  | ||||||
|  |         if (use_interval) { | ||||||
|  |           // Use set_interval with random interval time | ||||||
|  |           uint32_t interval_ms = interval_dist(gen); | ||||||
|  |  | ||||||
|  |           this->set_interval(interval_ms, [component, i, j, callback_id]() { | ||||||
|  |             component->executed_callbacks_.fetch_add(1); | ||||||
|  |             ESP_LOGV(TAG, "Executed interval %d (thread %d, index %d)", callback_id, i, j); | ||||||
|  |  | ||||||
|  |             // Cancel the interval after first execution to avoid flooding | ||||||
|  |             return false; | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           ESP_LOGV(TAG, "Thread %d scheduled interval %d with %u ms interval", i, callback_id, interval_ms); | ||||||
|  |         } else { | ||||||
|  |           // Use set_timeout with random timeout | ||||||
|  |           uint32_t timeout_ms = timeout_dist(gen); | ||||||
|  |  | ||||||
|  |           this->set_timeout(timeout_ms, [component, i, j, callback_id]() { | ||||||
|  |             component->executed_callbacks_.fetch_add(1); | ||||||
|  |             ESP_LOGV(TAG, "Executed timeout %d (thread %d, index %d)", callback_id, i, j); | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |           ESP_LOGV(TAG, "Thread %d scheduled timeout %d with %u ms delay", i, callback_id, timeout_ms); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Small random delay to increase contention | ||||||
|  |         if (j % 10 == 0) { | ||||||
|  |           std::this_thread::sleep_for(std::chrono::microseconds(100)); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       ESP_LOGV(TAG, "Thread %d finished", i); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Wait for all threads to complete | ||||||
|  |   for (auto &t : threads) { | ||||||
|  |     t.join(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   auto end_time = std::chrono::steady_clock::now(); | ||||||
|  |   auto thread_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count(); | ||||||
|  |   ESP_LOGI(TAG, "All threads finished in %lldms. Created %d callbacks", thread_time, this->total_callbacks_.load()); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_heap_stress_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include <atomic> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_heap_stress_component { | ||||||
|  |  | ||||||
|  | class SchedulerHeapStressComponent : public Component { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::LATE; } | ||||||
|  |  | ||||||
|  |   void run_multi_thread_test(); | ||||||
|  |  | ||||||
|  |  private: | ||||||
|  |   std::atomic<int> total_callbacks_{0}; | ||||||
|  |   std::atomic<int> executed_callbacks_{0}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_heap_stress_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | scheduler_rapid_cancellation_component_ns = cg.esphome_ns.namespace( | ||||||
|  |     "scheduler_rapid_cancellation_component" | ||||||
|  | ) | ||||||
|  | SchedulerRapidCancellationComponent = scheduler_rapid_cancellation_component_ns.class_( | ||||||
|  |     "SchedulerRapidCancellationComponent", cg.Component | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SchedulerRapidCancellationComponent), | ||||||
|  |     } | ||||||
|  | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
| @@ -0,0 +1,77 @@ | |||||||
|  | #include "rapid_cancellation_component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include <thread> | ||||||
|  | #include <vector> | ||||||
|  | #include <chrono> | ||||||
|  | #include <random> | ||||||
|  | #include <sstream> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_rapid_cancellation_component { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "scheduler_rapid_cancellation"; | ||||||
|  |  | ||||||
|  | void SchedulerRapidCancellationComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRapidCancellationComponent setup"); } | ||||||
|  |  | ||||||
|  | void SchedulerRapidCancellationComponent::run_rapid_cancellation_test() { | ||||||
|  |   ESP_LOGI(TAG, "Starting rapid cancellation test - multiple threads racing on same timeout names"); | ||||||
|  |  | ||||||
|  |   // Reset counters | ||||||
|  |   this->total_scheduled_ = 0; | ||||||
|  |   this->total_executed_ = 0; | ||||||
|  |  | ||||||
|  |   static constexpr int NUM_THREADS = 4;              // Number of threads to create | ||||||
|  |   static constexpr int NUM_NAMES = 10;               // Only 10 unique names | ||||||
|  |   static constexpr int OPERATIONS_PER_THREAD = 100;  // Each thread does 100 operations | ||||||
|  |  | ||||||
|  |   // Create threads that will all fight over the same timeout names | ||||||
|  |   std::vector<std::thread> threads; | ||||||
|  |   threads.reserve(NUM_THREADS); | ||||||
|  |  | ||||||
|  |   for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) { | ||||||
|  |     threads.emplace_back([this, thread_id]() { | ||||||
|  |       for (int i = 0; i < OPERATIONS_PER_THREAD; i++) { | ||||||
|  |         // Use modulo to ensure multiple threads use the same names | ||||||
|  |         int name_index = i % NUM_NAMES; | ||||||
|  |         std::stringstream ss; | ||||||
|  |         ss << "shared_timeout_" << name_index; | ||||||
|  |         std::string name = ss.str(); | ||||||
|  |  | ||||||
|  |         // All threads schedule timeouts - this will implicitly cancel existing ones | ||||||
|  |         this->set_timeout(name, 100, [this, name]() { | ||||||
|  |           this->total_executed_.fetch_add(1); | ||||||
|  |           ESP_LOGI(TAG, "Executed callback '%s'", name.c_str()); | ||||||
|  |         }); | ||||||
|  |         this->total_scheduled_.fetch_add(1); | ||||||
|  |  | ||||||
|  |         // Small delay to increase chance of race conditions | ||||||
|  |         if (i % 10 == 0) { | ||||||
|  |           std::this_thread::sleep_for(std::chrono::microseconds(100)); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Wait for all threads to complete | ||||||
|  |   for (auto &t : threads) { | ||||||
|  |     t.join(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGI(TAG, "All threads completed. Scheduled: %d", this->total_scheduled_.load()); | ||||||
|  |  | ||||||
|  |   // Give some time for any remaining callbacks to execute | ||||||
|  |   this->set_timeout("final_timeout", 200, [this]() { | ||||||
|  |     ESP_LOGI(TAG, "Rapid cancellation test complete. Final stats:"); | ||||||
|  |     ESP_LOGI(TAG, "  Total scheduled: %d", this->total_scheduled_.load()); | ||||||
|  |     ESP_LOGI(TAG, "  Total executed: %d", this->total_executed_.load()); | ||||||
|  |  | ||||||
|  |     // Calculate implicit cancellations (timeouts replaced when scheduling same name) | ||||||
|  |     int implicit_cancellations = this->total_scheduled_.load() - this->total_executed_.load(); | ||||||
|  |     ESP_LOGI(TAG, "  Implicit cancellations (replaced): %d", implicit_cancellations); | ||||||
|  |     ESP_LOGI(TAG, "  Total accounted: %d (executed + implicit cancellations)", | ||||||
|  |              this->total_executed_.load() + implicit_cancellations); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_rapid_cancellation_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include <atomic> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_rapid_cancellation_component { | ||||||
|  |  | ||||||
|  | class SchedulerRapidCancellationComponent : public Component { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::LATE; } | ||||||
|  |  | ||||||
|  |   void run_rapid_cancellation_test(); | ||||||
|  |  | ||||||
|  |  private: | ||||||
|  |   std::atomic<int> total_scheduled_{0}; | ||||||
|  |   std::atomic<int> total_executed_{0}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_rapid_cancellation_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | scheduler_recursive_timeout_component_ns = cg.esphome_ns.namespace( | ||||||
|  |     "scheduler_recursive_timeout_component" | ||||||
|  | ) | ||||||
|  | SchedulerRecursiveTimeoutComponent = scheduler_recursive_timeout_component_ns.class_( | ||||||
|  |     "SchedulerRecursiveTimeoutComponent", cg.Component | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SchedulerRecursiveTimeoutComponent), | ||||||
|  |     } | ||||||
|  | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
| @@ -0,0 +1,40 @@ | |||||||
|  | #include "recursive_timeout_component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_recursive_timeout_component { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "scheduler_recursive_timeout"; | ||||||
|  |  | ||||||
|  | void SchedulerRecursiveTimeoutComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerRecursiveTimeoutComponent setup"); } | ||||||
|  |  | ||||||
|  | void SchedulerRecursiveTimeoutComponent::run_recursive_timeout_test() { | ||||||
|  |   ESP_LOGI(TAG, "Starting recursive timeout test - scheduling timeout from within timeout"); | ||||||
|  |  | ||||||
|  |   // Reset state | ||||||
|  |   this->nested_level_ = 0; | ||||||
|  |  | ||||||
|  |   // Schedule the initial timeout with 1ms delay | ||||||
|  |   this->set_timeout(1, [this]() { | ||||||
|  |     ESP_LOGI(TAG, "Executing initial timeout"); | ||||||
|  |     this->nested_level_ = 1; | ||||||
|  |  | ||||||
|  |     // From within this timeout, schedule another timeout with 1ms delay | ||||||
|  |     this->set_timeout(1, [this]() { | ||||||
|  |       ESP_LOGI(TAG, "Executing nested timeout 1"); | ||||||
|  |       this->nested_level_ = 2; | ||||||
|  |  | ||||||
|  |       // From within this nested timeout, schedule yet another timeout with 1ms delay | ||||||
|  |       this->set_timeout(1, [this]() { | ||||||
|  |         ESP_LOGI(TAG, "Executing nested timeout 2"); | ||||||
|  |         this->nested_level_ = 3; | ||||||
|  |  | ||||||
|  |         // Test complete | ||||||
|  |         ESP_LOGI(TAG, "Recursive timeout test complete - all %d levels executed", this->nested_level_); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_recursive_timeout_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_recursive_timeout_component { | ||||||
|  |  | ||||||
|  | class SchedulerRecursiveTimeoutComponent : public Component { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::LATE; } | ||||||
|  |  | ||||||
|  |   void run_recursive_timeout_test(); | ||||||
|  |  | ||||||
|  |  private: | ||||||
|  |   int nested_level_{0}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_recursive_timeout_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,23 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | scheduler_simultaneous_callbacks_component_ns = cg.esphome_ns.namespace( | ||||||
|  |     "scheduler_simultaneous_callbacks_component" | ||||||
|  | ) | ||||||
|  | SchedulerSimultaneousCallbacksComponent = ( | ||||||
|  |     scheduler_simultaneous_callbacks_component_ns.class_( | ||||||
|  |         "SchedulerSimultaneousCallbacksComponent", cg.Component | ||||||
|  |     ) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SchedulerSimultaneousCallbacksComponent), | ||||||
|  |     } | ||||||
|  | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
| @@ -0,0 +1,109 @@ | |||||||
|  | #include "simultaneous_callbacks_component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include <thread> | ||||||
|  | #include <vector> | ||||||
|  | #include <chrono> | ||||||
|  | #include <sstream> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_simultaneous_callbacks_component { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "scheduler_simultaneous_callbacks"; | ||||||
|  |  | ||||||
|  | void SchedulerSimultaneousCallbacksComponent::setup() { | ||||||
|  |   ESP_LOGCONFIG(TAG, "SchedulerSimultaneousCallbacksComponent setup"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerSimultaneousCallbacksComponent::run_simultaneous_callbacks_test() { | ||||||
|  |   ESP_LOGI(TAG, "Starting simultaneous callbacks test - 10 threads scheduling 100 callbacks each for 1ms from now"); | ||||||
|  |  | ||||||
|  |   // Reset counters | ||||||
|  |   this->total_scheduled_ = 0; | ||||||
|  |   this->total_executed_ = 0; | ||||||
|  |   this->callbacks_at_once_ = 0; | ||||||
|  |   this->max_concurrent_ = 0; | ||||||
|  |  | ||||||
|  |   static constexpr int NUM_THREADS = 10; | ||||||
|  |   static constexpr int CALLBACKS_PER_THREAD = 100; | ||||||
|  |   static constexpr uint32_t DELAY_MS = 1;  // All callbacks scheduled for 1ms from now | ||||||
|  |  | ||||||
|  |   // Create threads for concurrent scheduling | ||||||
|  |   std::vector<std::thread> threads; | ||||||
|  |   threads.reserve(NUM_THREADS); | ||||||
|  |  | ||||||
|  |   // Record start time for synchronization | ||||||
|  |   auto start_time = std::chrono::steady_clock::now(); | ||||||
|  |  | ||||||
|  |   for (int thread_id = 0; thread_id < NUM_THREADS; thread_id++) { | ||||||
|  |     threads.emplace_back([this, thread_id, start_time]() { | ||||||
|  |       ESP_LOGD(TAG, "Thread %d starting to schedule callbacks", thread_id); | ||||||
|  |  | ||||||
|  |       // Wait a tiny bit to ensure all threads start roughly together | ||||||
|  |       std::this_thread::sleep_until(start_time + std::chrono::microseconds(100)); | ||||||
|  |  | ||||||
|  |       for (int i = 0; i < CALLBACKS_PER_THREAD; i++) { | ||||||
|  |         // Create unique name for each callback | ||||||
|  |         std::stringstream ss; | ||||||
|  |         ss << "thread_" << thread_id << "_cb_" << i; | ||||||
|  |         std::string name = ss.str(); | ||||||
|  |  | ||||||
|  |         // Schedule callback for exactly DELAY_MS from now | ||||||
|  |         this->set_timeout(name, DELAY_MS, [this, thread_id, i, name]() { | ||||||
|  |           // Increment concurrent counter atomically | ||||||
|  |           int current = this->callbacks_at_once_.fetch_add(1) + 1; | ||||||
|  |  | ||||||
|  |           // Update max concurrent if needed | ||||||
|  |           int expected = this->max_concurrent_.load(); | ||||||
|  |           while (current > expected && !this->max_concurrent_.compare_exchange_weak(expected, current)) { | ||||||
|  |             // Loop until we successfully update or someone else set a higher value | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           ESP_LOGV(TAG, "Callback executed: %s (concurrent: %d)", name.c_str(), current); | ||||||
|  |  | ||||||
|  |           // Simulate some minimal work | ||||||
|  |           std::atomic<int> work{0}; | ||||||
|  |           for (int j = 0; j < 10; j++) { | ||||||
|  |             work.fetch_add(j); | ||||||
|  |           } | ||||||
|  |  | ||||||
|  |           // Increment executed counter | ||||||
|  |           this->total_executed_.fetch_add(1); | ||||||
|  |  | ||||||
|  |           // Decrement concurrent counter | ||||||
|  |           this->callbacks_at_once_.fetch_sub(1); | ||||||
|  |         }); | ||||||
|  |  | ||||||
|  |         this->total_scheduled_.fetch_add(1); | ||||||
|  |         ESP_LOGV(TAG, "Scheduled callback %s", name.c_str()); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       ESP_LOGD(TAG, "Thread %d completed scheduling", thread_id); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Wait for all threads to complete scheduling | ||||||
|  |   for (auto &t : threads) { | ||||||
|  |     t.join(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   ESP_LOGI(TAG, "All threads completed scheduling. Total scheduled: %d", this->total_scheduled_.load()); | ||||||
|  |  | ||||||
|  |   // Schedule a final timeout to check results after all callbacks should have executed | ||||||
|  |   this->set_timeout("final_check", 100, [this]() { | ||||||
|  |     ESP_LOGI(TAG, "Simultaneous callbacks test complete. Final executed count: %d", this->total_executed_.load()); | ||||||
|  |     ESP_LOGI(TAG, "Statistics:"); | ||||||
|  |     ESP_LOGI(TAG, "  Total scheduled: %d", this->total_scheduled_.load()); | ||||||
|  |     ESP_LOGI(TAG, "  Total executed: %d", this->total_executed_.load()); | ||||||
|  |     ESP_LOGI(TAG, "  Max concurrent callbacks: %d", this->max_concurrent_.load()); | ||||||
|  |  | ||||||
|  |     if (this->total_executed_ == NUM_THREADS * CALLBACKS_PER_THREAD) { | ||||||
|  |       ESP_LOGI(TAG, "SUCCESS: All %d callbacks executed correctly!", this->total_executed_.load()); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGE(TAG, "FAILURE: Expected %d callbacks but only %d executed", NUM_THREADS * CALLBACKS_PER_THREAD, | ||||||
|  |                this->total_executed_.load()); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_simultaneous_callbacks_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,24 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include <atomic> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_simultaneous_callbacks_component { | ||||||
|  |  | ||||||
|  | class SchedulerSimultaneousCallbacksComponent : public Component { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::LATE; } | ||||||
|  |  | ||||||
|  |   void run_simultaneous_callbacks_test(); | ||||||
|  |  | ||||||
|  |  private: | ||||||
|  |   std::atomic<int> total_scheduled_{0}; | ||||||
|  |   std::atomic<int> total_executed_{0}; | ||||||
|  |   std::atomic<int> callbacks_at_once_{0}; | ||||||
|  |   std::atomic<int> max_concurrent_{0}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_simultaneous_callbacks_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | scheduler_string_lifetime_component_ns = cg.esphome_ns.namespace( | ||||||
|  |     "scheduler_string_lifetime_component" | ||||||
|  | ) | ||||||
|  | SchedulerStringLifetimeComponent = scheduler_string_lifetime_component_ns.class_( | ||||||
|  |     "SchedulerStringLifetimeComponent", cg.Component | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SchedulerStringLifetimeComponent), | ||||||
|  |     } | ||||||
|  | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
| @@ -0,0 +1,233 @@ | |||||||
|  | #include "string_lifetime_component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include <memory> | ||||||
|  | #include <thread> | ||||||
|  | #include <chrono> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_string_lifetime_component { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "scheduler_string_lifetime"; | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringLifetimeComponent setup"); } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::run_string_lifetime_test() { | ||||||
|  |   ESP_LOGI(TAG, "Starting string lifetime tests"); | ||||||
|  |  | ||||||
|  |   this->tests_passed_ = 0; | ||||||
|  |   this->tests_failed_ = 0; | ||||||
|  |  | ||||||
|  |   // Run each test | ||||||
|  |   test_temporary_string_lifetime(); | ||||||
|  |   test_scope_exit_string(); | ||||||
|  |   test_vector_reallocation(); | ||||||
|  |   test_string_move_semantics(); | ||||||
|  |   test_lambda_capture_lifetime(); | ||||||
|  |  | ||||||
|  |   // Schedule final check | ||||||
|  |   this->set_timeout("final_check", 200, [this]() { | ||||||
|  |     ESP_LOGI(TAG, "String lifetime tests complete"); | ||||||
|  |     ESP_LOGI(TAG, "Tests passed: %d", this->tests_passed_); | ||||||
|  |     ESP_LOGI(TAG, "Tests failed: %d", this->tests_failed_); | ||||||
|  |  | ||||||
|  |     if (this->tests_failed_ == 0) { | ||||||
|  |       ESP_LOGI(TAG, "SUCCESS: All string lifetime tests passed!"); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGE(TAG, "FAILURE: %d string lifetime tests failed!", this->tests_failed_); | ||||||
|  |     } | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::test_temporary_string_lifetime() { | ||||||
|  |   ESP_LOGI(TAG, "Test 1: Temporary string lifetime for timeout names"); | ||||||
|  |  | ||||||
|  |   // Test with a temporary string that goes out of scope immediately | ||||||
|  |   { | ||||||
|  |     std::string temp_name = "temp_callback_" + std::to_string(12345); | ||||||
|  |  | ||||||
|  |     // Schedule with temporary string name - scheduler must copy/store this | ||||||
|  |     this->set_timeout(temp_name, 1, [this]() { | ||||||
|  |       ESP_LOGD(TAG, "Callback for temp string name executed"); | ||||||
|  |       this->tests_passed_++; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // String goes out of scope here, but scheduler should have made a copy | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Test with rvalue string as name | ||||||
|  |   this->set_timeout(std::string("rvalue_test"), 2, [this]() { | ||||||
|  |     ESP_LOGD(TAG, "Rvalue string name callback executed"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Test cancelling with reconstructed string | ||||||
|  |   { | ||||||
|  |     std::string cancel_name = "cancel_test_" + std::to_string(999); | ||||||
|  |     this->set_timeout(cancel_name, 100, [this]() { | ||||||
|  |       ESP_LOGE(TAG, "This should have been cancelled!"); | ||||||
|  |       this->tests_failed_++; | ||||||
|  |     }); | ||||||
|  |   }  // cancel_name goes out of scope | ||||||
|  |  | ||||||
|  |   // Reconstruct the same string to cancel | ||||||
|  |   std::string cancel_name_2 = "cancel_test_" + std::to_string(999); | ||||||
|  |   bool cancelled = this->cancel_timeout(cancel_name_2); | ||||||
|  |   if (cancelled) { | ||||||
|  |     ESP_LOGD(TAG, "Successfully cancelled with reconstructed string"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGE(TAG, "Failed to cancel with reconstructed string"); | ||||||
|  |     this->tests_failed_++; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::test_scope_exit_string() { | ||||||
|  |   ESP_LOGI(TAG, "Test 2: Scope exit string names"); | ||||||
|  |  | ||||||
|  |   // Create string names in a limited scope | ||||||
|  |   { | ||||||
|  |     std::string scoped_name = "scoped_timeout_" + std::to_string(555); | ||||||
|  |  | ||||||
|  |     // Schedule with scoped string name | ||||||
|  |     this->set_timeout(scoped_name, 3, [this]() { | ||||||
|  |       ESP_LOGD(TAG, "Scoped name callback executed"); | ||||||
|  |       this->tests_passed_++; | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // scoped_name goes out of scope here | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Test with dynamically allocated string name | ||||||
|  |   { | ||||||
|  |     auto *dynamic_name = new std::string("dynamic_timeout_" + std::to_string(777)); | ||||||
|  |  | ||||||
|  |     this->set_timeout(*dynamic_name, 4, [this, dynamic_name]() { | ||||||
|  |       ESP_LOGD(TAG, "Dynamic string name callback executed"); | ||||||
|  |       this->tests_passed_++; | ||||||
|  |       delete dynamic_name;  // Clean up in callback | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     // Pointer goes out of scope but string object remains until callback | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Test multiple timeouts with same dynamically created name | ||||||
|  |   for (int i = 0; i < 3; i++) { | ||||||
|  |     std::string loop_name = "loop_timeout_" + std::to_string(i); | ||||||
|  |     this->set_timeout(loop_name, 5 + i * 1, [this, i]() { | ||||||
|  |       ESP_LOGD(TAG, "Loop timeout %d executed", i); | ||||||
|  |       this->tests_passed_++; | ||||||
|  |     }); | ||||||
|  |     // loop_name destroyed and recreated each iteration | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::test_vector_reallocation() { | ||||||
|  |   ESP_LOGI(TAG, "Test 3: Vector reallocation stress on timeout names"); | ||||||
|  |  | ||||||
|  |   // Create a vector that will reallocate | ||||||
|  |   std::vector<std::string> names; | ||||||
|  |   names.reserve(2);  // Small initial capacity to force reallocation | ||||||
|  |  | ||||||
|  |   // Schedule callbacks with string names from vector | ||||||
|  |   for (int i = 0; i < 10; i++) { | ||||||
|  |     names.push_back("vector_cb_" + std::to_string(i)); | ||||||
|  |     // Use the string from vector as timeout name | ||||||
|  |     this->set_timeout(names.back(), 8 + i * 1, [this, i]() { | ||||||
|  |       ESP_LOGV(TAG, "Vector name callback %d executed", i); | ||||||
|  |       this->tests_passed_++; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Force reallocation by adding more elements | ||||||
|  |   // This will move all strings to new memory locations | ||||||
|  |   for (int i = 10; i < 50; i++) { | ||||||
|  |     names.push_back("realloc_trigger_" + std::to_string(i)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Add more timeouts after reallocation to ensure old names still work | ||||||
|  |   for (int i = 50; i < 55; i++) { | ||||||
|  |     names.push_back("post_realloc_" + std::to_string(i)); | ||||||
|  |     this->set_timeout(names.back(), 20 + (i - 50), [this]() { | ||||||
|  |       ESP_LOGV(TAG, "Post-reallocation callback executed"); | ||||||
|  |       this->tests_passed_++; | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Clear the vector while timeouts are still pending | ||||||
|  |   names.clear(); | ||||||
|  |   ESP_LOGD(TAG, "Vector cleared - all string names destroyed"); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::test_string_move_semantics() { | ||||||
|  |   ESP_LOGI(TAG, "Test 4: String move semantics for timeout names"); | ||||||
|  |  | ||||||
|  |   // Test moving string names | ||||||
|  |   std::string original = "move_test_original"; | ||||||
|  |   std::string moved = std::move(original); | ||||||
|  |  | ||||||
|  |   // Schedule with moved string as name | ||||||
|  |   this->set_timeout(moved, 30, [this]() { | ||||||
|  |     ESP_LOGD(TAG, "Moved string name callback executed"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // original is now empty, try to use it as a different timeout name | ||||||
|  |   original = "reused_after_move"; | ||||||
|  |   this->set_timeout(original, 32, [this]() { | ||||||
|  |     ESP_LOGD(TAG, "Reused string name callback executed"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void SchedulerStringLifetimeComponent::test_lambda_capture_lifetime() { | ||||||
|  |   ESP_LOGI(TAG, "Test 5: Complex timeout name scenarios"); | ||||||
|  |  | ||||||
|  |   // Test scheduling with name built in lambda | ||||||
|  |   [this]() { | ||||||
|  |     std::string lambda_name = "lambda_built_name_" + std::to_string(888); | ||||||
|  |     this->set_timeout(lambda_name, 38, [this]() { | ||||||
|  |       ESP_LOGD(TAG, "Lambda-built name callback executed"); | ||||||
|  |       this->tests_passed_++; | ||||||
|  |     }); | ||||||
|  |   }();  // Lambda executes and lambda_name is destroyed | ||||||
|  |  | ||||||
|  |   // Test with shared_ptr name | ||||||
|  |   auto shared_name = std::make_shared<std::string>("shared_ptr_timeout"); | ||||||
|  |   this->set_timeout(*shared_name, 40, [this, shared_name]() { | ||||||
|  |     ESP_LOGD(TAG, "Shared_ptr name callback executed"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   }); | ||||||
|  |   shared_name.reset();  // Release the shared_ptr | ||||||
|  |  | ||||||
|  |   // Test overwriting timeout with same name | ||||||
|  |   std::string overwrite_name = "overwrite_test"; | ||||||
|  |   this->set_timeout(overwrite_name, 1000, [this]() { | ||||||
|  |     ESP_LOGE(TAG, "This should have been overwritten!"); | ||||||
|  |     this->tests_failed_++; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Overwrite with shorter timeout | ||||||
|  |   this->set_timeout(overwrite_name, 42, [this]() { | ||||||
|  |     ESP_LOGD(TAG, "Overwritten timeout executed"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Test very long string name | ||||||
|  |   std::string long_name; | ||||||
|  |   for (int i = 0; i < 100; i++) { | ||||||
|  |     long_name += "very_long_timeout_name_segment_" + std::to_string(i) + "_"; | ||||||
|  |   } | ||||||
|  |   this->set_timeout(long_name, 44, [this]() { | ||||||
|  |     ESP_LOGD(TAG, "Very long name timeout executed"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   // Test empty string as name | ||||||
|  |   this->set_timeout("", 46, [this]() { | ||||||
|  |     ESP_LOGD(TAG, "Empty string name timeout executed"); | ||||||
|  |     this->tests_passed_++; | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_string_lifetime_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include <vector> | ||||||
|  | #include <string> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_string_lifetime_component { | ||||||
|  |  | ||||||
|  | class SchedulerStringLifetimeComponent : public Component { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::LATE; } | ||||||
|  |  | ||||||
|  |   void run_string_lifetime_test(); | ||||||
|  |  | ||||||
|  |  private: | ||||||
|  |   void test_temporary_string_lifetime(); | ||||||
|  |   void test_scope_exit_string(); | ||||||
|  |   void test_vector_reallocation(); | ||||||
|  |   void test_string_move_semantics(); | ||||||
|  |   void test_lambda_capture_lifetime(); | ||||||
|  |  | ||||||
|  |   int tests_passed_{0}; | ||||||
|  |   int tests_failed_{0}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_string_lifetime_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,21 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.const import CONF_ID | ||||||
|  |  | ||||||
|  | scheduler_string_name_stress_component_ns = cg.esphome_ns.namespace( | ||||||
|  |     "scheduler_string_name_stress_component" | ||||||
|  | ) | ||||||
|  | SchedulerStringNameStressComponent = scheduler_string_name_stress_component_ns.class_( | ||||||
|  |     "SchedulerStringNameStressComponent", cg.Component | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CONFIG_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(SchedulerStringNameStressComponent), | ||||||
|  |     } | ||||||
|  | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def to_code(config): | ||||||
|  |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|  |     await cg.register_component(var, config) | ||||||
| @@ -0,0 +1,110 @@ | |||||||
|  | #include "string_name_stress_component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  | #include <thread> | ||||||
|  | #include <atomic> | ||||||
|  | #include <vector> | ||||||
|  | #include <chrono> | ||||||
|  | #include <string> | ||||||
|  | #include <sstream> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_string_name_stress_component { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "scheduler_string_name_stress"; | ||||||
|  |  | ||||||
|  | void SchedulerStringNameStressComponent::setup() { ESP_LOGCONFIG(TAG, "SchedulerStringNameStressComponent setup"); } | ||||||
|  |  | ||||||
|  | void SchedulerStringNameStressComponent::run_string_name_stress_test() { | ||||||
|  |   // Use member variables to reset state | ||||||
|  |   this->total_callbacks_ = 0; | ||||||
|  |   this->executed_callbacks_ = 0; | ||||||
|  |   static constexpr int NUM_THREADS = 10; | ||||||
|  |   static constexpr int CALLBACKS_PER_THREAD = 100; | ||||||
|  |  | ||||||
|  |   ESP_LOGI(TAG, "Starting string name stress test - multi-threaded set_timeout with std::string names"); | ||||||
|  |   ESP_LOGI(TAG, "This test specifically uses dynamic string names to test memory management"); | ||||||
|  |  | ||||||
|  |   // Track start time | ||||||
|  |   auto start_time = std::chrono::steady_clock::now(); | ||||||
|  |  | ||||||
|  |   // Create threads | ||||||
|  |   std::vector<std::thread> threads; | ||||||
|  |  | ||||||
|  |   ESP_LOGI(TAG, "Creating %d threads, each will schedule %d callbacks with dynamic names", NUM_THREADS, | ||||||
|  |            CALLBACKS_PER_THREAD); | ||||||
|  |  | ||||||
|  |   threads.reserve(NUM_THREADS); | ||||||
|  |   for (int i = 0; i < NUM_THREADS; i++) { | ||||||
|  |     threads.emplace_back([this, i]() { | ||||||
|  |       ESP_LOGV(TAG, "Thread %d starting", i); | ||||||
|  |  | ||||||
|  |       // Each thread schedules callbacks with dynamically created string names | ||||||
|  |       for (int j = 0; j < CALLBACKS_PER_THREAD; j++) { | ||||||
|  |         int callback_id = this->total_callbacks_.fetch_add(1); | ||||||
|  |  | ||||||
|  |         // Create a dynamic string name - this will test memory management | ||||||
|  |         std::stringstream ss; | ||||||
|  |         ss << "thread_" << i << "_callback_" << j << "_id_" << callback_id; | ||||||
|  |         std::string dynamic_name = ss.str(); | ||||||
|  |  | ||||||
|  |         ESP_LOGV(TAG, "Thread %d scheduling timeout with dynamic name: %s", i, dynamic_name.c_str()); | ||||||
|  |  | ||||||
|  |         // Capture necessary values for the lambda | ||||||
|  |         auto *component = this; | ||||||
|  |  | ||||||
|  |         // Schedule with std::string name - this tests the string overload | ||||||
|  |         // Use varying delays to stress the heap scheduler | ||||||
|  |         uint32_t delay = 1 + (callback_id % 50); | ||||||
|  |  | ||||||
|  |         // Also test nested scheduling from callbacks | ||||||
|  |         if (j % 10 == 0) { | ||||||
|  |           // Every 10th callback schedules another callback | ||||||
|  |           this->set_timeout(dynamic_name, delay, [component, i, j, callback_id]() { | ||||||
|  |             component->executed_callbacks_.fetch_add(1); | ||||||
|  |             ESP_LOGV(TAG, "Executed string-named callback %d (nested scheduler)", callback_id); | ||||||
|  |  | ||||||
|  |             // Schedule another timeout from within this callback with a new dynamic name | ||||||
|  |             std::string nested_name = "nested_from_" + std::to_string(callback_id); | ||||||
|  |             component->set_timeout(nested_name, 1, [component, callback_id]() { | ||||||
|  |               ESP_LOGV(TAG, "Executed nested string-named callback from %d", callback_id); | ||||||
|  |             }); | ||||||
|  |           }); | ||||||
|  |         } else { | ||||||
|  |           // Regular callback | ||||||
|  |           this->set_timeout(dynamic_name, delay, [component, i, j, callback_id]() { | ||||||
|  |             component->executed_callbacks_.fetch_add(1); | ||||||
|  |             ESP_LOGV(TAG, "Executed string-named callback %d", callback_id); | ||||||
|  |           }); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         // Add some timing variations to increase race conditions | ||||||
|  |         if (j % 5 == 0) { | ||||||
|  |           std::this_thread::sleep_for(std::chrono::microseconds(100)); | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       ESP_LOGV(TAG, "Thread %d finished scheduling", i); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // Wait for all threads to complete scheduling | ||||||
|  |   for (auto &t : threads) { | ||||||
|  |     t.join(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   auto end_time = std::chrono::steady_clock::now(); | ||||||
|  |   auto thread_time = std::chrono::duration_cast<std::chrono::milliseconds>(end_time - start_time).count(); | ||||||
|  |   ESP_LOGI(TAG, "All threads finished scheduling in %lldms. Created %d callbacks with dynamic names", thread_time, | ||||||
|  |            this->total_callbacks_.load()); | ||||||
|  |  | ||||||
|  |   // Give some time for callbacks to execute | ||||||
|  |   ESP_LOGI(TAG, "Waiting for callbacks to execute..."); | ||||||
|  |  | ||||||
|  |   // Schedule a final callback to signal completion | ||||||
|  |   this->set_timeout("test_complete", 2000, [this]() { | ||||||
|  |     ESP_LOGI(TAG, "String name stress test complete. Executed %d of %d callbacks", this->executed_callbacks_.load(), | ||||||
|  |              this->total_callbacks_.load()); | ||||||
|  |   }); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_string_name_stress_component | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,22 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include <atomic> | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace scheduler_string_name_stress_component { | ||||||
|  |  | ||||||
|  | class SchedulerStringNameStressComponent : public Component { | ||||||
|  |  public: | ||||||
|  |   void setup() override; | ||||||
|  |   float get_setup_priority() const override { return setup_priority::LATE; } | ||||||
|  |  | ||||||
|  |   void run_string_name_stress_test(); | ||||||
|  |  | ||||||
|  |  private: | ||||||
|  |   std::atomic<int> total_callbacks_{0}; | ||||||
|  |   std::atomic<int> executed_callbacks_{0}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace scheduler_string_name_stress_component | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										38
									
								
								tests/integration/fixtures/scheduler_heap_stress.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								tests/integration/fixtures/scheduler_heap_stress.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | esphome: | ||||||
|  |   name: scheduler-heap-stress-test | ||||||
|  |  | ||||||
|  | external_components: | ||||||
|  |   - source: | ||||||
|  |       type: local | ||||||
|  |       path: EXTERNAL_COMPONENT_PATH | ||||||
|  |     components: [scheduler_heap_stress_component] | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: VERBOSE | ||||||
|  |  | ||||||
|  | scheduler_heap_stress_component: | ||||||
|  |   id: heap_stress | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: run_heap_stress_test | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(heap_stress)->run_multi_thread_test(); | ||||||
|  |  | ||||||
|  | event: | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Complete" | ||||||
|  |     id: test_complete | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "test_finished" | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Result" | ||||||
|  |     id: test_result | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "passed" | ||||||
|  |       - "failed" | ||||||
							
								
								
									
										38
									
								
								tests/integration/fixtures/scheduler_rapid_cancellation.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								tests/integration/fixtures/scheduler_rapid_cancellation.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | esphome: | ||||||
|  |   name: sched-rapid-cancel-test | ||||||
|  |  | ||||||
|  | external_components: | ||||||
|  |   - source: | ||||||
|  |       type: local | ||||||
|  |       path: EXTERNAL_COMPONENT_PATH | ||||||
|  |     components: [scheduler_rapid_cancellation_component] | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: VERBOSE | ||||||
|  |  | ||||||
|  | scheduler_rapid_cancellation_component: | ||||||
|  |   id: rapid_cancel | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: run_rapid_cancellation_test | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(rapid_cancel)->run_rapid_cancellation_test(); | ||||||
|  |  | ||||||
|  | event: | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Complete" | ||||||
|  |     id: test_complete | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "test_finished" | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Result" | ||||||
|  |     id: test_result | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "passed" | ||||||
|  |       - "failed" | ||||||
							
								
								
									
										38
									
								
								tests/integration/fixtures/scheduler_recursive_timeout.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								tests/integration/fixtures/scheduler_recursive_timeout.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | esphome: | ||||||
|  |   name: sched-recursive-timeout | ||||||
|  |  | ||||||
|  | external_components: | ||||||
|  |   - source: | ||||||
|  |       type: local | ||||||
|  |       path: EXTERNAL_COMPONENT_PATH | ||||||
|  |     components: [scheduler_recursive_timeout_component] | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: VERBOSE | ||||||
|  |  | ||||||
|  | scheduler_recursive_timeout_component: | ||||||
|  |   id: recursive_timeout | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: run_recursive_timeout_test | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(recursive_timeout)->run_recursive_timeout_test(); | ||||||
|  |  | ||||||
|  | event: | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Complete" | ||||||
|  |     id: test_complete | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "test_finished" | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Result" | ||||||
|  |     id: test_result | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "passed" | ||||||
|  |       - "failed" | ||||||
| @@ -0,0 +1,23 @@ | |||||||
|  | esphome: | ||||||
|  |   name: sched-simul-callbacks-test | ||||||
|  |  | ||||||
|  | external_components: | ||||||
|  |   - source: | ||||||
|  |       type: local | ||||||
|  |       path: EXTERNAL_COMPONENT_PATH | ||||||
|  |     components: [scheduler_simultaneous_callbacks_component] | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: INFO | ||||||
|  |  | ||||||
|  | scheduler_simultaneous_callbacks_component: | ||||||
|  |   id: simultaneous_callbacks | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: run_simultaneous_callbacks_test | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(simultaneous_callbacks)->run_simultaneous_callbacks_test(); | ||||||
							
								
								
									
										23
									
								
								tests/integration/fixtures/scheduler_string_lifetime.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								tests/integration/fixtures/scheduler_string_lifetime.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,23 @@ | |||||||
|  | esphome: | ||||||
|  |   name: scheduler-string-lifetime-test | ||||||
|  |  | ||||||
|  | external_components: | ||||||
|  |   - source: | ||||||
|  |       type: local | ||||||
|  |       path: EXTERNAL_COMPONENT_PATH | ||||||
|  |     components: [scheduler_string_lifetime_component] | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: DEBUG | ||||||
|  |  | ||||||
|  | scheduler_string_lifetime_component: | ||||||
|  |   id: string_lifetime | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: run_string_lifetime_test | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(string_lifetime)->run_string_lifetime_test(); | ||||||
							
								
								
									
										38
									
								
								tests/integration/fixtures/scheduler_string_name_stress.yaml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								tests/integration/fixtures/scheduler_string_name_stress.yaml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,38 @@ | |||||||
|  | esphome: | ||||||
|  |   name: sched-string-name-stress | ||||||
|  |  | ||||||
|  | external_components: | ||||||
|  |   - source: | ||||||
|  |       type: local | ||||||
|  |       path: EXTERNAL_COMPONENT_PATH | ||||||
|  |     components: [scheduler_string_name_stress_component] | ||||||
|  |  | ||||||
|  | host: | ||||||
|  |  | ||||||
|  | logger: | ||||||
|  |   level: VERBOSE | ||||||
|  |  | ||||||
|  | scheduler_string_name_stress_component: | ||||||
|  |   id: string_stress | ||||||
|  |  | ||||||
|  | api: | ||||||
|  |   services: | ||||||
|  |     - service: run_string_name_stress_test | ||||||
|  |       then: | ||||||
|  |         - lambda: |- | ||||||
|  |             id(string_stress)->run_string_name_stress_test(); | ||||||
|  |  | ||||||
|  | event: | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Complete" | ||||||
|  |     id: test_complete | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "test_finished" | ||||||
|  |   - platform: template | ||||||
|  |     name: "Test Result" | ||||||
|  |     id: test_result | ||||||
|  |     device_class: button | ||||||
|  |     event_types: | ||||||
|  |       - "passed" | ||||||
|  |       - "failed" | ||||||
							
								
								
									
										140
									
								
								tests/integration/test_scheduler_heap_stress.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								tests/integration/test_scheduler_heap_stress.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,140 @@ | |||||||
|  | """Stress test for heap scheduler thread safety with multiple threads.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | from pathlib import Path | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | from aioesphomeapi import UserService | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_heap_stress( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that set_timeout/set_interval doesn't crash when called rapidly from multiple threads.""" | ||||||
|  |  | ||||||
|  |     # Get the absolute path to the external components directory | ||||||
|  |     external_components_path = str( | ||||||
|  |         Path(__file__).parent / "fixtures" / "external_components" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Replace the placeholder in the YAML config with the actual path | ||||||
|  |     yaml_config = yaml_config.replace( | ||||||
|  |         "EXTERNAL_COMPONENT_PATH", external_components_path | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Create a future to signal test completion | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     test_complete_future: asyncio.Future[None] = loop.create_future() | ||||||
|  |  | ||||||
|  |     # Track executed timeouts/intervals and their order | ||||||
|  |     executed_callbacks: set[int] = set() | ||||||
|  |     thread_executions: dict[ | ||||||
|  |         int, list[int] | ||||||
|  |     ] = {}  # thread_id -> list of indices in execution order | ||||||
|  |     callback_types: dict[int, str] = {}  # callback_id -> "timeout" or "interval" | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         # Track all executed callbacks with thread and index info | ||||||
|  |         match = re.search( | ||||||
|  |             r"Executed (timeout|interval) (\d+) \(thread (\d+), index (\d+)\)", line | ||||||
|  |         ) | ||||||
|  |         if not match: | ||||||
|  |             # Also check for the completion message | ||||||
|  |             if "All threads finished" in line and "Created 1000 callbacks" in line: | ||||||
|  |                 # Give scheduler some time to execute callbacks | ||||||
|  |                 pass | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         callback_type = match.group(1) | ||||||
|  |         callback_id = int(match.group(2)) | ||||||
|  |         thread_id = int(match.group(3)) | ||||||
|  |         index = int(match.group(4)) | ||||||
|  |  | ||||||
|  |         # Only count each callback ID once (intervals might fire multiple times) | ||||||
|  |         if callback_id not in executed_callbacks: | ||||||
|  |             executed_callbacks.add(callback_id) | ||||||
|  |             callback_types[callback_id] = callback_type | ||||||
|  |  | ||||||
|  |         # Track execution order per thread | ||||||
|  |         if thread_id not in thread_executions: | ||||||
|  |             thread_executions[thread_id] = [] | ||||||
|  |  | ||||||
|  |         # Only append if this is a new execution for this thread | ||||||
|  |         if index not in thread_executions[thread_id]: | ||||||
|  |             thread_executions[thread_id].append(index) | ||||||
|  |  | ||||||
|  |         # Check if we've executed all 1000 callbacks (0-999) | ||||||
|  |         if len(executed_callbacks) >= 1000 and not test_complete_future.done(): | ||||||
|  |             test_complete_future.set_result(None) | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "scheduler-heap-stress-test" | ||||||
|  |  | ||||||
|  |         # List entities and services | ||||||
|  |         _, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test service | ||||||
|  |         run_stress_test_service: UserService | None = None | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "run_heap_stress_test": | ||||||
|  |                 run_stress_test_service = service | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert run_stress_test_service is not None, ( | ||||||
|  |             "run_heap_stress_test service not found" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Call the run_heap_stress_test service to start the test | ||||||
|  |         client.execute_service(run_stress_test_service, {}) | ||||||
|  |  | ||||||
|  |         # Wait for all callbacks to execute (should be quick, but give more time for scheduling) | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=60.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             # Report how many we got | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"Stress test timed out. Only {len(executed_callbacks)} of " | ||||||
|  |                 f"1000 callbacks executed. Missing IDs: " | ||||||
|  |                 f"{sorted(set(range(1000)) - executed_callbacks)[:10]}..." | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # Verify all callbacks executed | ||||||
|  |         assert len(executed_callbacks) == 1000, ( | ||||||
|  |             f"Expected 1000 callbacks, got {len(executed_callbacks)}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify we have all IDs from 0-999 | ||||||
|  |         expected_ids = set(range(1000)) | ||||||
|  |         missing_ids = expected_ids - executed_callbacks | ||||||
|  |         assert not missing_ids, f"Missing callback IDs: {sorted(missing_ids)}" | ||||||
|  |  | ||||||
|  |         # Verify we have a mix of timeouts and intervals | ||||||
|  |         timeout_count = sum(1 for t in callback_types.values() if t == "timeout") | ||||||
|  |         interval_count = sum(1 for t in callback_types.values() if t == "interval") | ||||||
|  |         assert timeout_count > 0, "No timeouts were executed" | ||||||
|  |         assert interval_count > 0, "No intervals were executed" | ||||||
|  |  | ||||||
|  |         # Verify each thread executed callbacks | ||||||
|  |         for thread_id, indices in thread_executions.items(): | ||||||
|  |             assert len(indices) == 100, ( | ||||||
|  |                 f"Thread {thread_id} executed {len(indices)} callbacks, expected 100" | ||||||
|  |             ) | ||||||
|  |         # Total should be 1000 callbacks | ||||||
|  |         total_callbacks = timeout_count + interval_count | ||||||
|  |         assert total_callbacks == 1000, ( | ||||||
|  |             f"Expected 1000 total callbacks but got {total_callbacks}" | ||||||
|  |         ) | ||||||
							
								
								
									
										142
									
								
								tests/integration/test_scheduler_rapid_cancellation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										142
									
								
								tests/integration/test_scheduler_rapid_cancellation.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,142 @@ | |||||||
|  | """Rapid cancellation test - schedule and immediately cancel timeouts with string names.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | from pathlib import Path | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | from aioesphomeapi import UserService | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_rapid_cancellation( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test rapid schedule/cancel cycles that might expose race conditions.""" | ||||||
|  |  | ||||||
|  |     # Get the absolute path to the external components directory | ||||||
|  |     external_components_path = str( | ||||||
|  |         Path(__file__).parent / "fixtures" / "external_components" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Replace the placeholder in the YAML config with the actual path | ||||||
|  |     yaml_config = yaml_config.replace( | ||||||
|  |         "EXTERNAL_COMPONENT_PATH", external_components_path | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Create a future to signal test completion | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     test_complete_future: asyncio.Future[None] = loop.create_future() | ||||||
|  |  | ||||||
|  |     # Track test progress | ||||||
|  |     test_stats = { | ||||||
|  |         "log_count": 0, | ||||||
|  |         "errors": [], | ||||||
|  |         "summary_scheduled": None, | ||||||
|  |         "final_scheduled": 0, | ||||||
|  |         "final_executed": 0, | ||||||
|  |         "final_implicit_cancellations": 0, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         # Count log lines | ||||||
|  |         test_stats["log_count"] += 1 | ||||||
|  |  | ||||||
|  |         # Check for errors (only ERROR level, not WARN) | ||||||
|  |         if "ERROR" in line: | ||||||
|  |             test_stats["errors"].append(line) | ||||||
|  |  | ||||||
|  |         # Parse summary statistics | ||||||
|  |         if "All threads completed. Scheduled:" in line: | ||||||
|  |             # Extract the scheduled count from the summary | ||||||
|  |             if match := re.search(r"Scheduled: (\d+)", line): | ||||||
|  |                 test_stats["summary_scheduled"] = int(match.group(1)) | ||||||
|  |         elif "Total scheduled:" in line: | ||||||
|  |             if match := re.search(r"Total scheduled: (\d+)", line): | ||||||
|  |                 test_stats["final_scheduled"] = int(match.group(1)) | ||||||
|  |         elif "Total executed:" in line: | ||||||
|  |             if match := re.search(r"Total executed: (\d+)", line): | ||||||
|  |                 test_stats["final_executed"] = int(match.group(1)) | ||||||
|  |         elif "Implicit cancellations (replaced):" in line: | ||||||
|  |             if match := re.search(r"Implicit cancellations \(replaced\): (\d+)", line): | ||||||
|  |                 test_stats["final_implicit_cancellations"] = int(match.group(1)) | ||||||
|  |  | ||||||
|  |         # Check for crash indicators | ||||||
|  |         if any( | ||||||
|  |             indicator in line.lower() | ||||||
|  |             for indicator in ["segfault", "abort", "assertion", "heap corruption"] | ||||||
|  |         ): | ||||||
|  |             if not test_complete_future.done(): | ||||||
|  |                 test_complete_future.set_exception(Exception(f"Crash detected: {line}")) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         # Check for completion | ||||||
|  |         if ( | ||||||
|  |             "Rapid cancellation test complete" in line | ||||||
|  |             and not test_complete_future.done() | ||||||
|  |         ): | ||||||
|  |             test_complete_future.set_result(None) | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "sched-rapid-cancel-test" | ||||||
|  |  | ||||||
|  |         # List entities and services | ||||||
|  |         _, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test service | ||||||
|  |         run_test_service: UserService | None = None | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "run_rapid_cancellation_test": | ||||||
|  |                 run_test_service = service | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert run_test_service is not None, ( | ||||||
|  |             "run_rapid_cancellation_test service not found" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Call the service to start the test | ||||||
|  |         client.execute_service(run_test_service, {}) | ||||||
|  |  | ||||||
|  |         # Wait for test to complete with timeout | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=10.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail(f"Test timed out. Stats: {test_stats}") | ||||||
|  |  | ||||||
|  |         # Check for any errors | ||||||
|  |         assert len(test_stats["errors"]) == 0, ( | ||||||
|  |             f"Errors detected: {test_stats['errors']}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Check that we received log messages | ||||||
|  |         assert test_stats["log_count"] > 0, "No log messages received" | ||||||
|  |  | ||||||
|  |         # Check the summary line to verify all threads scheduled their operations | ||||||
|  |         assert test_stats["summary_scheduled"] == 400, ( | ||||||
|  |             f"Expected summary to show 400 scheduled operations but got {test_stats['summary_scheduled']}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Check final statistics | ||||||
|  |         assert test_stats["final_scheduled"] == 400, ( | ||||||
|  |             f"Expected final stats to show 400 scheduled but got {test_stats['final_scheduled']}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         assert test_stats["final_executed"] == 10, ( | ||||||
|  |             f"Expected final stats to show 10 executed but got {test_stats['final_executed']}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         assert test_stats["final_implicit_cancellations"] == 390, ( | ||||||
|  |             f"Expected final stats to show 390 implicit cancellations but got {test_stats['final_implicit_cancellations']}" | ||||||
|  |         ) | ||||||
							
								
								
									
										101
									
								
								tests/integration/test_scheduler_recursive_timeout.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										101
									
								
								tests/integration/test_scheduler_recursive_timeout.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,101 @@ | |||||||
|  | """Test for recursive timeout scheduling - scheduling timeouts from within timeout callbacks.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | from pathlib import Path | ||||||
|  |  | ||||||
|  | from aioesphomeapi import UserService | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_recursive_timeout( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that scheduling timeouts from within timeout callbacks works correctly.""" | ||||||
|  |  | ||||||
|  |     # Get the absolute path to the external components directory | ||||||
|  |     external_components_path = str( | ||||||
|  |         Path(__file__).parent / "fixtures" / "external_components" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Replace the placeholder in the YAML config with the actual path | ||||||
|  |     yaml_config = yaml_config.replace( | ||||||
|  |         "EXTERNAL_COMPONENT_PATH", external_components_path | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Create a future to signal test completion | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     test_complete_future: asyncio.Future[None] = loop.create_future() | ||||||
|  |  | ||||||
|  |     # Track execution sequence | ||||||
|  |     execution_sequence: list[str] = [] | ||||||
|  |     expected_sequence = [ | ||||||
|  |         "initial_timeout", | ||||||
|  |         "nested_timeout_1", | ||||||
|  |         "nested_timeout_2", | ||||||
|  |         "test_complete", | ||||||
|  |     ] | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         # Track execution sequence | ||||||
|  |         if "Executing initial timeout" in line: | ||||||
|  |             execution_sequence.append("initial_timeout") | ||||||
|  |         elif "Executing nested timeout 1" in line: | ||||||
|  |             execution_sequence.append("nested_timeout_1") | ||||||
|  |         elif "Executing nested timeout 2" in line: | ||||||
|  |             execution_sequence.append("nested_timeout_2") | ||||||
|  |         elif "Recursive timeout test complete" in line: | ||||||
|  |             execution_sequence.append("test_complete") | ||||||
|  |             if not test_complete_future.done(): | ||||||
|  |                 test_complete_future.set_result(None) | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "sched-recursive-timeout" | ||||||
|  |  | ||||||
|  |         # List entities and services | ||||||
|  |         _, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test service | ||||||
|  |         run_test_service: UserService | None = None | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "run_recursive_timeout_test": | ||||||
|  |                 run_test_service = service | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert run_test_service is not None, ( | ||||||
|  |             "run_recursive_timeout_test service not found" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Call the service to start the test | ||||||
|  |         client.execute_service(run_test_service, {}) | ||||||
|  |  | ||||||
|  |         # Wait for test to complete | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=10.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"Recursive timeout test timed out. Got sequence: {execution_sequence}" | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # Verify execution sequence | ||||||
|  |         assert execution_sequence == expected_sequence, ( | ||||||
|  |             f"Execution sequence mismatch. Expected {expected_sequence}, " | ||||||
|  |             f"got {execution_sequence}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify we got exactly 4 events (Initial + Level 1 + Level 2 + Complete) | ||||||
|  |         assert len(execution_sequence) == 4, ( | ||||||
|  |             f"Expected 4 events but got {len(execution_sequence)}" | ||||||
|  |         ) | ||||||
							
								
								
									
										123
									
								
								tests/integration/test_scheduler_simultaneous_callbacks.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										123
									
								
								tests/integration/test_scheduler_simultaneous_callbacks.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,123 @@ | |||||||
|  | """Simultaneous callbacks test - schedule many callbacks for the same time from multiple threads.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | from pathlib import Path | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | from aioesphomeapi import UserService | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_simultaneous_callbacks( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test scheduling many callbacks for the exact same time from multiple threads.""" | ||||||
|  |  | ||||||
|  |     # Get the absolute path to the external components directory | ||||||
|  |     external_components_path = str( | ||||||
|  |         Path(__file__).parent / "fixtures" / "external_components" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Replace the placeholder in the YAML config with the actual path | ||||||
|  |     yaml_config = yaml_config.replace( | ||||||
|  |         "EXTERNAL_COMPONENT_PATH", external_components_path | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Create a future to signal test completion | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     test_complete_future: asyncio.Future[None] = loop.create_future() | ||||||
|  |  | ||||||
|  |     # Track test progress | ||||||
|  |     test_stats = { | ||||||
|  |         "scheduled": 0, | ||||||
|  |         "executed": 0, | ||||||
|  |         "expected": 1000,  # 10 threads * 100 callbacks | ||||||
|  |         "errors": [], | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         # Track operations | ||||||
|  |         if "Scheduled callback" in line: | ||||||
|  |             test_stats["scheduled"] += 1 | ||||||
|  |         elif "Callback executed" in line: | ||||||
|  |             test_stats["executed"] += 1 | ||||||
|  |         elif "ERROR" in line: | ||||||
|  |             test_stats["errors"].append(line) | ||||||
|  |  | ||||||
|  |         # Check for crash indicators | ||||||
|  |         if any( | ||||||
|  |             indicator in line.lower() | ||||||
|  |             for indicator in ["segfault", "abort", "assertion", "heap corruption"] | ||||||
|  |         ): | ||||||
|  |             if not test_complete_future.done(): | ||||||
|  |                 test_complete_future.set_exception(Exception(f"Crash detected: {line}")) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         # Check for completion with final count | ||||||
|  |         if "Final executed count:" in line: | ||||||
|  |             # Extract number from log line like: "[07:59:47][I][simultaneous_callbacks:093]: Simultaneous callbacks test complete. Final executed count: 1000" | ||||||
|  |             match = re.search(r"Final executed count:\s*(\d+)", line) | ||||||
|  |             if match: | ||||||
|  |                 test_stats["final_count"] = int(match.group(1)) | ||||||
|  |  | ||||||
|  |         # Check for completion | ||||||
|  |         if ( | ||||||
|  |             "Simultaneous callbacks test complete" in line | ||||||
|  |             and not test_complete_future.done() | ||||||
|  |         ): | ||||||
|  |             test_complete_future.set_result(None) | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "sched-simul-callbacks-test" | ||||||
|  |  | ||||||
|  |         # List entities and services | ||||||
|  |         _, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test service | ||||||
|  |         run_test_service: UserService | None = None | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "run_simultaneous_callbacks_test": | ||||||
|  |                 run_test_service = service | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert run_test_service is not None, ( | ||||||
|  |             "run_simultaneous_callbacks_test service not found" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Call the service to start the test | ||||||
|  |         client.execute_service(run_test_service, {}) | ||||||
|  |  | ||||||
|  |         # Wait for test to complete | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=30.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail(f"Simultaneous callbacks test timed out. Stats: {test_stats}") | ||||||
|  |  | ||||||
|  |         # Check for any errors | ||||||
|  |         assert len(test_stats["errors"]) == 0, ( | ||||||
|  |             f"Errors detected: {test_stats['errors']}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify all callbacks executed using the final count from C++ | ||||||
|  |         final_count = test_stats.get("final_count", 0) | ||||||
|  |         assert final_count == test_stats["expected"], ( | ||||||
|  |             f"Expected {test_stats['expected']} callbacks, but only {final_count} executed" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # The final_count is the authoritative count from the C++ component | ||||||
|  |         assert final_count == 1000, ( | ||||||
|  |             f"Expected 1000 executed callbacks but got {final_count}" | ||||||
|  |         ) | ||||||
							
								
								
									
										125
									
								
								tests/integration/test_scheduler_string_lifetime.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								tests/integration/test_scheduler_string_lifetime.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,125 @@ | |||||||
|  | """String lifetime test - verify scheduler handles string destruction correctly.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | from pathlib import Path | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | from aioesphomeapi import UserService | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_string_lifetime( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that scheduler correctly handles string lifetimes when strings go out of scope.""" | ||||||
|  |  | ||||||
|  |     # Get the absolute path to the external components directory | ||||||
|  |     external_components_path = str( | ||||||
|  |         Path(__file__).parent / "fixtures" / "external_components" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Replace the placeholder in the YAML config with the actual path | ||||||
|  |     yaml_config = yaml_config.replace( | ||||||
|  |         "EXTERNAL_COMPONENT_PATH", external_components_path | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Create a future to signal test completion | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     test_complete_future: asyncio.Future[None] = loop.create_future() | ||||||
|  |  | ||||||
|  |     # Track test progress | ||||||
|  |     test_stats = { | ||||||
|  |         "tests_passed": 0, | ||||||
|  |         "tests_failed": 0, | ||||||
|  |         "errors": [], | ||||||
|  |         "use_after_free_detected": False, | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         # Track test results from the C++ test output | ||||||
|  |         if "Tests passed:" in line and "string_lifetime" in line: | ||||||
|  |             # Extract the number from "Tests passed: 32" | ||||||
|  |             match = re.search(r"Tests passed:\s*(\d+)", line) | ||||||
|  |             if match: | ||||||
|  |                 test_stats["tests_passed"] = int(match.group(1)) | ||||||
|  |         elif "Tests failed:" in line and "string_lifetime" in line: | ||||||
|  |             match = re.search(r"Tests failed:\s*(\d+)", line) | ||||||
|  |             if match: | ||||||
|  |                 test_stats["tests_failed"] = int(match.group(1)) | ||||||
|  |         elif "ERROR" in line and "string_lifetime" in line: | ||||||
|  |             test_stats["errors"].append(line) | ||||||
|  |  | ||||||
|  |         # Check for memory corruption indicators | ||||||
|  |         if any( | ||||||
|  |             indicator in line.lower() | ||||||
|  |             for indicator in [ | ||||||
|  |                 "use after free", | ||||||
|  |                 "heap corruption", | ||||||
|  |                 "segfault", | ||||||
|  |                 "abort", | ||||||
|  |                 "assertion", | ||||||
|  |                 "sanitizer", | ||||||
|  |                 "bad memory", | ||||||
|  |                 "invalid pointer", | ||||||
|  |             ] | ||||||
|  |         ): | ||||||
|  |             test_stats["use_after_free_detected"] = True | ||||||
|  |             if not test_complete_future.done(): | ||||||
|  |                 test_complete_future.set_exception( | ||||||
|  |                     Exception(f"Memory corruption detected: {line}") | ||||||
|  |                 ) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         # Check for completion | ||||||
|  |         if "String lifetime tests complete" in line and not test_complete_future.done(): | ||||||
|  |             test_complete_future.set_result(None) | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "scheduler-string-lifetime-test" | ||||||
|  |  | ||||||
|  |         # List entities and services | ||||||
|  |         _, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test service | ||||||
|  |         run_test_service: UserService | None = None | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "run_string_lifetime_test": | ||||||
|  |                 run_test_service = service | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert run_test_service is not None, ( | ||||||
|  |             "run_string_lifetime_test service not found" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Call the service to start the test | ||||||
|  |         client.execute_service(run_test_service, {}) | ||||||
|  |  | ||||||
|  |         # Wait for test to complete | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=30.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail(f"String lifetime test timed out. Stats: {test_stats}") | ||||||
|  |  | ||||||
|  |         # Check for use-after-free | ||||||
|  |         assert not test_stats["use_after_free_detected"], "Use-after-free detected!" | ||||||
|  |  | ||||||
|  |         # Check for any errors | ||||||
|  |         assert test_stats["tests_failed"] == 0, f"Tests failed: {test_stats['errors']}" | ||||||
|  |  | ||||||
|  |         # Verify we had the expected number of passing tests | ||||||
|  |         assert test_stats["tests_passed"] == 30, ( | ||||||
|  |             f"Expected exactly 30 tests to pass, but got {test_stats['tests_passed']}" | ||||||
|  |         ) | ||||||
							
								
								
									
										116
									
								
								tests/integration/test_scheduler_string_name_stress.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								tests/integration/test_scheduler_string_name_stress.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | |||||||
|  | """Stress test for heap scheduler with std::string names from multiple threads.""" | ||||||
|  |  | ||||||
|  | import asyncio | ||||||
|  | from pathlib import Path | ||||||
|  | import re | ||||||
|  |  | ||||||
|  | from aioesphomeapi import UserService | ||||||
|  | import pytest | ||||||
|  |  | ||||||
|  | from .types import APIClientConnectedFactory, RunCompiledFunction | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @pytest.mark.asyncio | ||||||
|  | async def test_scheduler_string_name_stress( | ||||||
|  |     yaml_config: str, | ||||||
|  |     run_compiled: RunCompiledFunction, | ||||||
|  |     api_client_connected: APIClientConnectedFactory, | ||||||
|  | ) -> None: | ||||||
|  |     """Test that set_timeout/set_interval with std::string names doesn't crash when called from multiple threads.""" | ||||||
|  |  | ||||||
|  |     # Get the absolute path to the external components directory | ||||||
|  |     external_components_path = str( | ||||||
|  |         Path(__file__).parent / "fixtures" / "external_components" | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Replace the placeholder in the YAML config with the actual path | ||||||
|  |     yaml_config = yaml_config.replace( | ||||||
|  |         "EXTERNAL_COMPONENT_PATH", external_components_path | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |     # Create a future to signal test completion | ||||||
|  |     loop = asyncio.get_running_loop() | ||||||
|  |     test_complete_future: asyncio.Future[None] = loop.create_future() | ||||||
|  |  | ||||||
|  |     # Track executed callbacks and any crashes | ||||||
|  |     executed_callbacks: set[int] = set() | ||||||
|  |     error_messages: list[str] = [] | ||||||
|  |  | ||||||
|  |     def on_log_line(line: str) -> None: | ||||||
|  |         # Check for crash indicators | ||||||
|  |         if any( | ||||||
|  |             indicator in line.lower() | ||||||
|  |             for indicator in [ | ||||||
|  |                 "segfault", | ||||||
|  |                 "abort", | ||||||
|  |                 "assertion", | ||||||
|  |                 "heap corruption", | ||||||
|  |                 "use after free", | ||||||
|  |             ] | ||||||
|  |         ): | ||||||
|  |             error_messages.append(line) | ||||||
|  |             if not test_complete_future.done(): | ||||||
|  |                 test_complete_future.set_exception(Exception(f"Crash detected: {line}")) | ||||||
|  |             return | ||||||
|  |  | ||||||
|  |         # Track executed callbacks | ||||||
|  |         match = re.search(r"Executed string-named callback (\d+)", line) | ||||||
|  |         if match: | ||||||
|  |             callback_id = int(match.group(1)) | ||||||
|  |             executed_callbacks.add(callback_id) | ||||||
|  |  | ||||||
|  |         # Check for completion | ||||||
|  |         if ( | ||||||
|  |             "String name stress test complete" in line | ||||||
|  |             and not test_complete_future.done() | ||||||
|  |         ): | ||||||
|  |             test_complete_future.set_result(None) | ||||||
|  |  | ||||||
|  |     async with ( | ||||||
|  |         run_compiled(yaml_config, line_callback=on_log_line), | ||||||
|  |         api_client_connected() as client, | ||||||
|  |     ): | ||||||
|  |         # Verify we can connect | ||||||
|  |         device_info = await client.device_info() | ||||||
|  |         assert device_info is not None | ||||||
|  |         assert device_info.name == "sched-string-name-stress" | ||||||
|  |  | ||||||
|  |         # List entities and services | ||||||
|  |         _, services = await asyncio.wait_for( | ||||||
|  |             client.list_entities_services(), timeout=5.0 | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Find our test service | ||||||
|  |         run_stress_test_service: UserService | None = None | ||||||
|  |         for service in services: | ||||||
|  |             if service.name == "run_string_name_stress_test": | ||||||
|  |                 run_stress_test_service = service | ||||||
|  |                 break | ||||||
|  |  | ||||||
|  |         assert run_stress_test_service is not None, ( | ||||||
|  |             "run_string_name_stress_test service not found" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Call the service to start the test | ||||||
|  |         client.execute_service(run_stress_test_service, {}) | ||||||
|  |  | ||||||
|  |         # Wait for test to complete or crash | ||||||
|  |         try: | ||||||
|  |             await asyncio.wait_for(test_complete_future, timeout=30.0) | ||||||
|  |         except asyncio.TimeoutError: | ||||||
|  |             pytest.fail( | ||||||
|  |                 f"String name stress test timed out. Executed {len(executed_callbacks)} callbacks. " | ||||||
|  |                 f"This might indicate a deadlock." | ||||||
|  |             ) | ||||||
|  |  | ||||||
|  |         # Verify no errors occurred (crashes already handled by exception) | ||||||
|  |         assert not error_messages, f"Errors detected during test: {error_messages}" | ||||||
|  |  | ||||||
|  |         # Verify we executed all 1000 callbacks (10 threads × 100 callbacks each) | ||||||
|  |         assert len(executed_callbacks) == 1000, ( | ||||||
|  |             f"Expected 1000 callbacks but got {len(executed_callbacks)}" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |         # Verify each callback ID was executed exactly once | ||||||
|  |         for i in range(1000): | ||||||
|  |             assert i in executed_callbacks, f"Callback {i} was not executed" | ||||||
		Reference in New Issue
	
	Block a user