mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 15:12:06 +00:00 
			
		
		
		
	Merge branch 'dev' into esp32_touch_isr
This commit is contained in:
		
							
								
								
									
										2
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							| @@ -49,7 +49,7 @@ jobs: | ||||
|         with: | ||||
|           python-version: "3.10" | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3.10.0 | ||||
|         uses: docker/setup-buildx-action@v3.11.1 | ||||
|  | ||||
|       - name: Set TAG | ||||
|         run: | | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -99,7 +99,7 @@ jobs: | ||||
|           python-version: "3.10" | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3.10.0 | ||||
|         uses: docker/setup-buildx-action@v3.11.1 | ||||
|  | ||||
|       - name: Log in to docker hub | ||||
|         uses: docker/login-action@v3.4.0 | ||||
| @@ -178,7 +178,7 @@ jobs: | ||||
|           merge-multiple: true | ||||
|  | ||||
|       - name: Set up Docker Buildx | ||||
|         uses: docker/setup-buildx-action@v3.10.0 | ||||
|         uses: docker/setup-buildx-action@v3.11.1 | ||||
|  | ||||
|       - name: Log in to docker hub | ||||
|         if: matrix.registry == 'dockerhub' | ||||
|   | ||||
| @@ -4,7 +4,7 @@ | ||||
| repos: | ||||
|   - repo: https://github.com/astral-sh/ruff-pre-commit | ||||
|     # Ruff version. | ||||
|     rev: v0.11.10 | ||||
|     rev: v0.12.0 | ||||
|     hooks: | ||||
|       # Run the linter. | ||||
|       - id: ruff | ||||
|   | ||||
| @@ -520,6 +520,7 @@ esphome/components/xiaomi_lywsd03mmc/* @ahpohl | ||||
| esphome/components/xiaomi_mhoc303/* @drug123 | ||||
| esphome/components/xiaomi_mhoc401/* @vevsvevs | ||||
| esphome/components/xiaomi_rtcgq02lm/* @jesserockz | ||||
| esphome/components/xiaomi_xmwsdj04mmc/* @medusalix | ||||
| esphome/components/xl9535/* @mreditor97 | ||||
| esphome/components/xpt2046/touchscreen/* @nielsnl68 @numo68 | ||||
| esphome/components/xxtea/* @clydebarrow | ||||
|   | ||||
| @@ -22,6 +22,7 @@ from esphome.cpp_generator import (  # noqa: F401 | ||||
|     TemplateArguments, | ||||
|     add, | ||||
|     add_build_flag, | ||||
|     add_build_unflag, | ||||
|     add_define, | ||||
|     add_global, | ||||
|     add_library, | ||||
| @@ -34,6 +35,7 @@ from esphome.cpp_generator import (  # noqa: F401 | ||||
|     process_lambda, | ||||
|     progmem_array, | ||||
|     safe_exp, | ||||
|     set_cpp_standard, | ||||
|     statement, | ||||
|     static_const_array, | ||||
|     templatable, | ||||
|   | ||||
| @@ -193,14 +193,13 @@ void AcDimmer::setup() { | ||||
|   setTimer1Callback(&timer_interrupt); | ||||
| #endif | ||||
| #ifdef USE_ESP32 | ||||
|   // 80 Divider -> 1 count=1µs | ||||
|   dimmer_timer = timerBegin(0, 80, true); | ||||
|   timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr, true); | ||||
|   // timer frequency of 1mhz | ||||
|   dimmer_timer = timerBegin(1000000); | ||||
|   timerAttachInterrupt(dimmer_timer, &AcDimmerDataStore::s_timer_intr); | ||||
|   // For ESP32, we can't use dynamic interval calculation because the timerX functions | ||||
|   // are not callable from ISR (placed in flash storage). | ||||
|   // Here we just use an interrupt firing every 50 µs. | ||||
|   timerAlarmWrite(dimmer_timer, 50, true); | ||||
|   timerAlarmEnable(dimmer_timer); | ||||
|   timerAlarm(dimmer_timer, 50, true, 0); | ||||
| #endif | ||||
| } | ||||
| void AcDimmer::write_state(float state) { | ||||
|   | ||||
| @@ -17,7 +17,11 @@ void Anova::setup() { | ||||
|   this->current_request_ = 0; | ||||
| } | ||||
|  | ||||
| void Anova::loop() {} | ||||
| void Anova::loop() { | ||||
|   // Parent BLEClientNode has a loop() method, but this component uses | ||||
|   // polling via update() and BLE callbacks so loop isn't needed | ||||
|   this->disable_loop(); | ||||
| } | ||||
|  | ||||
| void Anova::control(const ClimateCall &call) { | ||||
|   if (call.get_mode().has_value()) { | ||||
|   | ||||
| @@ -266,6 +266,7 @@ enum EntityCategory { | ||||
| // ==================== BINARY SENSOR ==================== | ||||
| message ListEntitiesBinarySensorResponse { | ||||
|   option (id) = 12; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_BINARY_SENSOR"; | ||||
|  | ||||
| @@ -282,6 +283,7 @@ message ListEntitiesBinarySensorResponse { | ||||
| } | ||||
| message BinarySensorStateResponse { | ||||
|   option (id) = 21; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_BINARY_SENSOR"; | ||||
|   option (no_delay) = true; | ||||
| @@ -296,6 +298,7 @@ message BinarySensorStateResponse { | ||||
| // ==================== COVER ==================== | ||||
| message ListEntitiesCoverResponse { | ||||
|   option (id) = 13; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_COVER"; | ||||
|  | ||||
| @@ -325,6 +328,7 @@ enum CoverOperation { | ||||
| } | ||||
| message CoverStateResponse { | ||||
|   option (id) = 22; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_COVER"; | ||||
|   option (no_delay) = true; | ||||
| @@ -367,6 +371,7 @@ message CoverCommandRequest { | ||||
| // ==================== FAN ==================== | ||||
| message ListEntitiesFanResponse { | ||||
|   option (id) = 14; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_FAN"; | ||||
|  | ||||
| @@ -395,6 +400,7 @@ enum FanDirection { | ||||
| } | ||||
| message FanStateResponse { | ||||
|   option (id) = 23; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_FAN"; | ||||
|   option (no_delay) = true; | ||||
| @@ -444,6 +450,7 @@ enum ColorMode { | ||||
| } | ||||
| message ListEntitiesLightResponse { | ||||
|   option (id) = 15; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_LIGHT"; | ||||
|  | ||||
| @@ -467,6 +474,7 @@ message ListEntitiesLightResponse { | ||||
| } | ||||
| message LightStateResponse { | ||||
|   option (id) = 24; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_LIGHT"; | ||||
|   option (no_delay) = true; | ||||
| @@ -536,6 +544,7 @@ enum SensorLastResetType { | ||||
|  | ||||
| message ListEntitiesSensorResponse { | ||||
|   option (id) = 16; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_SENSOR"; | ||||
|  | ||||
| @@ -557,6 +566,7 @@ message ListEntitiesSensorResponse { | ||||
| } | ||||
| message SensorStateResponse { | ||||
|   option (id) = 25; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_SENSOR"; | ||||
|   option (no_delay) = true; | ||||
| @@ -571,6 +581,7 @@ message SensorStateResponse { | ||||
| // ==================== SWITCH ==================== | ||||
| message ListEntitiesSwitchResponse { | ||||
|   option (id) = 17; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_SWITCH"; | ||||
|  | ||||
| @@ -587,6 +598,7 @@ message ListEntitiesSwitchResponse { | ||||
| } | ||||
| message SwitchStateResponse { | ||||
|   option (id) = 26; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_SWITCH"; | ||||
|   option (no_delay) = true; | ||||
| @@ -607,6 +619,7 @@ message SwitchCommandRequest { | ||||
| // ==================== TEXT SENSOR ==================== | ||||
| message ListEntitiesTextSensorResponse { | ||||
|   option (id) = 18; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_TEXT_SENSOR"; | ||||
|  | ||||
| @@ -622,6 +635,7 @@ message ListEntitiesTextSensorResponse { | ||||
| } | ||||
| message TextSensorStateResponse { | ||||
|   option (id) = 27; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_TEXT_SENSOR"; | ||||
|   option (no_delay) = true; | ||||
| @@ -789,6 +803,7 @@ message ExecuteServiceRequest { | ||||
| // ==================== CAMERA ==================== | ||||
| message ListEntitiesCameraResponse { | ||||
|   option (id) = 43; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_ESP32_CAMERA"; | ||||
|  | ||||
| @@ -869,6 +884,7 @@ enum ClimatePreset { | ||||
| } | ||||
| message ListEntitiesClimateResponse { | ||||
|   option (id) = 46; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_CLIMATE"; | ||||
|  | ||||
| @@ -903,6 +919,7 @@ message ListEntitiesClimateResponse { | ||||
| } | ||||
| message ClimateStateResponse { | ||||
|   option (id) = 47; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_CLIMATE"; | ||||
|   option (no_delay) = true; | ||||
| @@ -964,6 +981,7 @@ enum NumberMode { | ||||
| } | ||||
| message ListEntitiesNumberResponse { | ||||
|   option (id) = 49; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_NUMBER"; | ||||
|  | ||||
| @@ -984,6 +1002,7 @@ message ListEntitiesNumberResponse { | ||||
| } | ||||
| message NumberStateResponse { | ||||
|   option (id) = 50; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_NUMBER"; | ||||
|   option (no_delay) = true; | ||||
| @@ -1007,6 +1026,7 @@ message NumberCommandRequest { | ||||
| // ==================== SELECT ==================== | ||||
| message ListEntitiesSelectResponse { | ||||
|   option (id) = 52; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_SELECT"; | ||||
|  | ||||
| @@ -1022,6 +1042,7 @@ message ListEntitiesSelectResponse { | ||||
| } | ||||
| message SelectStateResponse { | ||||
|   option (id) = 53; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_SELECT"; | ||||
|   option (no_delay) = true; | ||||
| @@ -1045,6 +1066,7 @@ message SelectCommandRequest { | ||||
| // ==================== SIREN ==================== | ||||
| message ListEntitiesSirenResponse { | ||||
|   option (id) = 55; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_SIREN"; | ||||
|  | ||||
| @@ -1062,6 +1084,7 @@ message ListEntitiesSirenResponse { | ||||
| } | ||||
| message SirenStateResponse { | ||||
|   option (id) = 56; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_SIREN"; | ||||
|   option (no_delay) = true; | ||||
| @@ -1102,6 +1125,7 @@ enum LockCommand  { | ||||
| } | ||||
| message ListEntitiesLockResponse { | ||||
|   option (id) = 58; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_LOCK"; | ||||
|  | ||||
| @@ -1123,6 +1147,7 @@ message ListEntitiesLockResponse { | ||||
| } | ||||
| message LockStateResponse { | ||||
|   option (id) = 59; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_LOCK"; | ||||
|   option (no_delay) = true; | ||||
| @@ -1145,6 +1170,7 @@ message LockCommandRequest { | ||||
| // ==================== BUTTON ==================== | ||||
| message ListEntitiesButtonResponse { | ||||
|   option (id) = 61; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_BUTTON"; | ||||
|  | ||||
| @@ -1196,6 +1222,7 @@ message MediaPlayerSupportedFormat { | ||||
| } | ||||
| message ListEntitiesMediaPlayerResponse { | ||||
|   option (id) = 63; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_MEDIA_PLAYER"; | ||||
|  | ||||
| @@ -1214,6 +1241,7 @@ message ListEntitiesMediaPlayerResponse { | ||||
| } | ||||
| message MediaPlayerStateResponse { | ||||
|   option (id) = 64; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_MEDIA_PLAYER"; | ||||
|   option (no_delay) = true; | ||||
| @@ -1615,6 +1643,7 @@ enum VoiceAssistantEvent { | ||||
|   VOICE_ASSISTANT_STT_VAD_END = 12; | ||||
|   VOICE_ASSISTANT_TTS_STREAM_START = 98; | ||||
|   VOICE_ASSISTANT_TTS_STREAM_END = 99; | ||||
|   VOICE_ASSISTANT_INTENT_PROGRESS = 100; | ||||
| } | ||||
|  | ||||
| message VoiceAssistantEventData { | ||||
| @@ -1735,6 +1764,7 @@ enum AlarmControlPanelStateCommand { | ||||
|  | ||||
| message ListEntitiesAlarmControlPanelResponse { | ||||
|   option (id) = 94; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_ALARM_CONTROL_PANEL"; | ||||
|  | ||||
| @@ -1752,6 +1782,7 @@ message ListEntitiesAlarmControlPanelResponse { | ||||
|  | ||||
| message AlarmControlPanelStateResponse { | ||||
|   option (id) = 95; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_ALARM_CONTROL_PANEL"; | ||||
|   option (no_delay) = true; | ||||
| @@ -1776,6 +1807,7 @@ enum TextMode { | ||||
| } | ||||
| message ListEntitiesTextResponse { | ||||
|   option (id) = 97; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_TEXT"; | ||||
|  | ||||
| @@ -1794,6 +1826,7 @@ message ListEntitiesTextResponse { | ||||
| } | ||||
| message TextStateResponse { | ||||
|   option (id) = 98; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_TEXT"; | ||||
|   option (no_delay) = true; | ||||
| @@ -1818,6 +1851,7 @@ message TextCommandRequest { | ||||
| // ==================== DATETIME DATE ==================== | ||||
| message ListEntitiesDateResponse { | ||||
|   option (id) = 100; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_DATETIME_DATE"; | ||||
|  | ||||
| @@ -1832,6 +1866,7 @@ message ListEntitiesDateResponse { | ||||
| } | ||||
| message DateStateResponse { | ||||
|   option (id) = 101; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_DATETIME_DATE"; | ||||
|   option (no_delay) = true; | ||||
| @@ -1859,6 +1894,7 @@ message DateCommandRequest { | ||||
| // ==================== DATETIME TIME ==================== | ||||
| message ListEntitiesTimeResponse { | ||||
|   option (id) = 103; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_DATETIME_TIME"; | ||||
|  | ||||
| @@ -1873,6 +1909,7 @@ message ListEntitiesTimeResponse { | ||||
| } | ||||
| message TimeStateResponse { | ||||
|   option (id) = 104; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_DATETIME_TIME"; | ||||
|   option (no_delay) = true; | ||||
| @@ -1900,6 +1937,7 @@ message TimeCommandRequest { | ||||
| // ==================== EVENT ==================== | ||||
| message ListEntitiesEventResponse { | ||||
|   option (id) = 107; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_EVENT"; | ||||
|  | ||||
| @@ -1917,6 +1955,7 @@ message ListEntitiesEventResponse { | ||||
| } | ||||
| message EventResponse { | ||||
|   option (id) = 108; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_EVENT"; | ||||
|  | ||||
| @@ -1927,6 +1966,7 @@ message EventResponse { | ||||
| // ==================== VALVE ==================== | ||||
| message ListEntitiesValveResponse { | ||||
|   option (id) = 109; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_VALVE"; | ||||
|  | ||||
| @@ -1952,6 +1992,7 @@ enum ValveOperation { | ||||
| } | ||||
| message ValveStateResponse { | ||||
|   option (id) = 110; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_VALVE"; | ||||
|   option (no_delay) = true; | ||||
| @@ -1976,6 +2017,7 @@ message ValveCommandRequest { | ||||
| // ==================== DATETIME DATETIME ==================== | ||||
| message ListEntitiesDateTimeResponse { | ||||
|   option (id) = 112; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_DATETIME_DATETIME"; | ||||
|  | ||||
| @@ -1990,6 +2032,7 @@ message ListEntitiesDateTimeResponse { | ||||
| } | ||||
| message DateTimeStateResponse { | ||||
|   option (id) = 113; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_DATETIME_DATETIME"; | ||||
|   option (no_delay) = true; | ||||
| @@ -2013,6 +2056,7 @@ message DateTimeCommandRequest { | ||||
| // ==================== UPDATE ==================== | ||||
| message ListEntitiesUpdateResponse { | ||||
|   option (id) = 116; | ||||
|   option (base_class) = "InfoResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_UPDATE"; | ||||
|  | ||||
| @@ -2028,6 +2072,7 @@ message ListEntitiesUpdateResponse { | ||||
| } | ||||
| message UpdateStateResponse { | ||||
|   option (id) = 117; | ||||
|   option (base_class) = "StateResponseProtoMessage"; | ||||
|   option (source) = SOURCE_SERVER; | ||||
|   option (ifdef) = "USE_UPDATE"; | ||||
|   option (no_delay) = true; | ||||
|   | ||||
| @@ -61,8 +61,8 @@ void APIConnection::start() { | ||||
|   APIError err = this->helper_->init(); | ||||
|   if (err != APIError::OK) { | ||||
|     on_fatal_error(); | ||||
|     ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), | ||||
|              errno); | ||||
|     ESP_LOGW(TAG, "%s: Helper init failed: %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|              api_error_to_str(err), errno); | ||||
|     return; | ||||
|   } | ||||
|   this->client_info_ = helper_->getpeername(); | ||||
| @@ -91,7 +91,7 @@ void APIConnection::loop() { | ||||
|     // when network is disconnected force disconnect immediately | ||||
|     // don't wait for timeout | ||||
|     this->on_fatal_error(); | ||||
|     ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", this->client_combined_info_.c_str()); | ||||
|     ESP_LOGW(TAG, "%s: Network unavailable; disconnecting", this->get_client_combined_info().c_str()); | ||||
|     return; | ||||
|   } | ||||
|   if (this->next_close_) { | ||||
| @@ -104,7 +104,7 @@ void APIConnection::loop() { | ||||
|   APIError err = this->helper_->loop(); | ||||
|   if (err != APIError::OK) { | ||||
|     on_fatal_error(); | ||||
|     ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(), | ||||
|     ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|              api_error_to_str(err), errno); | ||||
|     return; | ||||
|   } | ||||
| @@ -118,12 +118,12 @@ void APIConnection::loop() { | ||||
|     } else if (err != APIError::OK) { | ||||
|       on_fatal_error(); | ||||
|       if (err == APIError::SOCKET_READ_FAILED && errno == ECONNRESET) { | ||||
|         ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str()); | ||||
|         ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); | ||||
|       } else if (err == APIError::CONNECTION_CLOSED) { | ||||
|         ESP_LOGW(TAG, "%s: Connection closed", this->client_combined_info_.c_str()); | ||||
|         ESP_LOGW(TAG, "%s: Connection closed", this->get_client_combined_info().c_str()); | ||||
|       } else { | ||||
|         ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), | ||||
|                  errno); | ||||
|         ESP_LOGW(TAG, "%s: Reading failed: %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|                  api_error_to_str(err), errno); | ||||
|       } | ||||
|       return; | ||||
|     } else { | ||||
| @@ -157,7 +157,7 @@ void APIConnection::loop() { | ||||
|     // Disconnect if not responded within 2.5*keepalive | ||||
|     if (now - this->last_traffic_ > (KEEPALIVE_TIMEOUT_MS * 5) / 2) { | ||||
|       on_fatal_error(); | ||||
|       ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->client_combined_info_.c_str()); | ||||
|       ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str()); | ||||
|     } | ||||
|   } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && now > this->next_ping_retry_) { | ||||
|     ESP_LOGVV(TAG, "Sending keepalive PING"); | ||||
| @@ -166,7 +166,7 @@ void APIConnection::loop() { | ||||
|       this->next_ping_retry_ = now + ping_retry_interval; | ||||
|       this->ping_retries_++; | ||||
|       std::string warn_str = str_sprintf("%s: Sending keepalive failed %u time(s);", | ||||
|                                          this->client_combined_info_.c_str(), this->ping_retries_); | ||||
|                                          this->get_client_combined_info().c_str(), this->ping_retries_); | ||||
|       if (this->ping_retries_ >= max_ping_retries) { | ||||
|         on_fatal_error(); | ||||
|         ESP_LOGE(TAG, "%s disconnecting", warn_str.c_str()); | ||||
| @@ -233,7 +233,7 @@ DisconnectResponse APIConnection::disconnect(const DisconnectRequest &msg) { | ||||
|   // remote initiated disconnect_client | ||||
|   // don't close yet, we still need to send the disconnect response | ||||
|   // close will happen on next loop | ||||
|   ESP_LOGD(TAG, "%s disconnected", this->client_combined_info_.c_str()); | ||||
|   ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str()); | ||||
|   this->next_close_ = true; | ||||
|   DisconnectResponse resp; | ||||
|   return resp; | ||||
| @@ -248,25 +248,41 @@ void APIConnection::on_disconnect_response(const DisconnectResponse &value) { | ||||
| uint16_t APIConnection::encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, | ||||
|                                                  uint32_t remaining_size, bool is_single) { | ||||
|   // Calculate size | ||||
|   uint32_t size = 0; | ||||
|   msg.calculate_size(size); | ||||
|   uint32_t calculated_size = 0; | ||||
|   msg.calculate_size(calculated_size); | ||||
|  | ||||
|   // Cache frame sizes to avoid repeated virtual calls | ||||
|   const uint8_t header_padding = conn->helper_->frame_header_padding(); | ||||
|   const uint8_t footer_size = conn->helper_->frame_footer_size(); | ||||
|  | ||||
|   // Calculate total size with padding for buffer allocation | ||||
|   uint16_t total_size = | ||||
|       static_cast<uint16_t>(size) + conn->helper_->frame_header_padding() + conn->helper_->frame_footer_size(); | ||||
|   size_t total_calculated_size = calculated_size + header_padding + footer_size; | ||||
|  | ||||
|   // Check if it fits | ||||
|   if (total_size > remaining_size) { | ||||
|   if (total_calculated_size > remaining_size) { | ||||
|     return 0;  // Doesn't fit | ||||
|   } | ||||
|  | ||||
|   // Allocate exact buffer space needed (just the payload, not the overhead) | ||||
|   ProtoWriteBuffer buffer = | ||||
|       is_single ? conn->allocate_single_message_buffer(size) : conn->allocate_batch_message_buffer(size); | ||||
|   // Allocate buffer space - pass payload size, allocation functions add header/footer space | ||||
|   ProtoWriteBuffer buffer = is_single ? conn->allocate_single_message_buffer(calculated_size) | ||||
|                                       : conn->allocate_batch_message_buffer(calculated_size); | ||||
|  | ||||
|   // Get buffer size after allocation (which includes header padding) | ||||
|   std::vector<uint8_t> &shared_buf = conn->parent_->get_shared_buffer_ref(); | ||||
|   size_t size_before_encode = shared_buf.size(); | ||||
|  | ||||
|   // Encode directly into buffer | ||||
|   msg.encode(buffer); | ||||
|   return total_size; | ||||
|  | ||||
|   // Calculate actual encoded size (not including header that was already added) | ||||
|   size_t actual_payload_size = shared_buf.size() - size_before_encode; | ||||
|  | ||||
|   // Return actual total size (header + actual payload + footer) | ||||
|   size_t actual_total_size = header_padding + actual_payload_size + footer_size; | ||||
|  | ||||
|   // Verify that calculate_size() returned the correct value | ||||
|   assert(calculated_size == actual_payload_size); | ||||
|   return static_cast<uint16_t>(actual_total_size); | ||||
| } | ||||
|  | ||||
| #ifdef USE_BINARY_SENSOR | ||||
| @@ -285,7 +301,7 @@ uint16_t APIConnection::try_send_binary_sensor_state(EntityBase *entity, APIConn | ||||
|   BinarySensorStateResponse resp; | ||||
|   resp.state = binary_sensor->state; | ||||
|   resp.missing_state = !binary_sensor->has_state(); | ||||
|   resp.key = binary_sensor->get_object_id_hash(); | ||||
|   fill_entity_state_base(binary_sensor, resp); | ||||
|   return encode_message_to_buffer(resp, BinarySensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
|  | ||||
| @@ -319,7 +335,7 @@ uint16_t APIConnection::try_send_cover_state(EntityBase *entity, APIConnection * | ||||
|   if (traits.get_supports_tilt()) | ||||
|     msg.tilt = cover->tilt; | ||||
|   msg.current_operation = static_cast<enums::CoverOperation>(cover->current_operation); | ||||
|   msg.key = cover->get_object_id_hash(); | ||||
|   fill_entity_state_base(cover, msg); | ||||
|   return encode_message_to_buffer(msg, CoverStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| uint16_t APIConnection::try_send_cover_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | ||||
| @@ -387,7 +403,7 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co | ||||
|     msg.direction = static_cast<enums::FanDirection>(fan->direction); | ||||
|   if (traits.supports_preset_modes()) | ||||
|     msg.preset_mode = fan->preset_mode; | ||||
|   msg.key = fan->get_object_id_hash(); | ||||
|   fill_entity_state_base(fan, msg); | ||||
|   return encode_message_to_buffer(msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | ||||
| @@ -454,7 +470,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection * | ||||
|   resp.warm_white = values.get_warm_white(); | ||||
|   if (light->supports_effects()) | ||||
|     resp.effect = light->get_effect_name(); | ||||
|   resp.key = light->get_object_id_hash(); | ||||
|   fill_entity_state_base(light, resp); | ||||
|   return encode_message_to_buffer(resp, LightStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | ||||
| @@ -536,7 +552,7 @@ uint16_t APIConnection::try_send_sensor_state(EntityBase *entity, APIConnection | ||||
|   SensorStateResponse resp; | ||||
|   resp.state = sensor->state; | ||||
|   resp.missing_state = !sensor->has_state(); | ||||
|   resp.key = sensor->get_object_id_hash(); | ||||
|   fill_entity_state_base(sensor, resp); | ||||
|   return encode_message_to_buffer(resp, SensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
|  | ||||
| @@ -570,7 +586,7 @@ uint16_t APIConnection::try_send_switch_state(EntityBase *entity, APIConnection | ||||
|   auto *a_switch = static_cast<switch_::Switch *>(entity); | ||||
|   SwitchStateResponse resp; | ||||
|   resp.state = a_switch->state; | ||||
|   resp.key = a_switch->get_object_id_hash(); | ||||
|   fill_entity_state_base(a_switch, resp); | ||||
|   return encode_message_to_buffer(resp, SwitchStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
|  | ||||
| @@ -613,7 +629,7 @@ uint16_t APIConnection::try_send_text_sensor_state(EntityBase *entity, APIConnec | ||||
|   TextSensorStateResponse resp; | ||||
|   resp.state = text_sensor->state; | ||||
|   resp.missing_state = !text_sensor->has_state(); | ||||
|   resp.key = text_sensor->get_object_id_hash(); | ||||
|   fill_entity_state_base(text_sensor, resp); | ||||
|   return encode_message_to_buffer(resp, TextSensorStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| uint16_t APIConnection::try_send_text_sensor_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size, | ||||
| @@ -637,7 +653,7 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection | ||||
|                                                bool is_single) { | ||||
|   auto *climate = static_cast<climate::Climate *>(entity); | ||||
|   ClimateStateResponse resp; | ||||
|   resp.key = climate->get_object_id_hash(); | ||||
|   fill_entity_state_base(climate, resp); | ||||
|   auto traits = climate->get_traits(); | ||||
|   resp.mode = static_cast<enums::ClimateMode>(climate->mode); | ||||
|   resp.action = static_cast<enums::ClimateAction>(climate->action); | ||||
| @@ -746,7 +762,7 @@ uint16_t APIConnection::try_send_number_state(EntityBase *entity, APIConnection | ||||
|   NumberStateResponse resp; | ||||
|   resp.state = number->state; | ||||
|   resp.missing_state = !number->has_state(); | ||||
|   resp.key = number->get_object_id_hash(); | ||||
|   fill_entity_state_base(number, resp); | ||||
|   return encode_message_to_buffer(resp, NumberStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
|  | ||||
| @@ -787,7 +803,7 @@ uint16_t APIConnection::try_send_date_state(EntityBase *entity, APIConnection *c | ||||
|   resp.year = date->year; | ||||
|   resp.month = date->month; | ||||
|   resp.day = date->day; | ||||
|   resp.key = date->get_object_id_hash(); | ||||
|   fill_entity_state_base(date, resp); | ||||
|   return encode_message_to_buffer(resp, DateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| void APIConnection::send_date_info(datetime::DateEntity *date) { | ||||
| @@ -824,7 +840,7 @@ uint16_t APIConnection::try_send_time_state(EntityBase *entity, APIConnection *c | ||||
|   resp.hour = time->hour; | ||||
|   resp.minute = time->minute; | ||||
|   resp.second = time->second; | ||||
|   resp.key = time->get_object_id_hash(); | ||||
|   fill_entity_state_base(time, resp); | ||||
|   return encode_message_to_buffer(resp, TimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| void APIConnection::send_time_info(datetime::TimeEntity *time) { | ||||
| @@ -863,7 +879,7 @@ uint16_t APIConnection::try_send_datetime_state(EntityBase *entity, APIConnectio | ||||
|     ESPTime state = datetime->state_as_esptime(); | ||||
|     resp.epoch_seconds = state.timestamp; | ||||
|   } | ||||
|   resp.key = datetime->get_object_id_hash(); | ||||
|   fill_entity_state_base(datetime, resp); | ||||
|   return encode_message_to_buffer(resp, DateTimeStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| void APIConnection::send_datetime_info(datetime::DateTimeEntity *datetime) { | ||||
| @@ -902,7 +918,7 @@ uint16_t APIConnection::try_send_text_state(EntityBase *entity, APIConnection *c | ||||
|   TextStateResponse resp; | ||||
|   resp.state = text->state; | ||||
|   resp.missing_state = !text->has_state(); | ||||
|   resp.key = text->get_object_id_hash(); | ||||
|   fill_entity_state_base(text, resp); | ||||
|   return encode_message_to_buffer(resp, TextStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
|  | ||||
| @@ -943,7 +959,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection | ||||
|   SelectStateResponse resp; | ||||
|   resp.state = select->state; | ||||
|   resp.missing_state = !select->has_state(); | ||||
|   resp.key = select->get_object_id_hash(); | ||||
|   fill_entity_state_base(select, resp); | ||||
|   return encode_message_to_buffer(resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
|  | ||||
| @@ -1003,7 +1019,7 @@ uint16_t APIConnection::try_send_lock_state(EntityBase *entity, APIConnection *c | ||||
|   auto *a_lock = static_cast<lock::Lock *>(entity); | ||||
|   LockStateResponse resp; | ||||
|   resp.state = static_cast<enums::LockState>(a_lock->state); | ||||
|   resp.key = a_lock->get_object_id_hash(); | ||||
|   fill_entity_state_base(a_lock, resp); | ||||
|   return encode_message_to_buffer(resp, LockStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
|  | ||||
| @@ -1047,7 +1063,7 @@ uint16_t APIConnection::try_send_valve_state(EntityBase *entity, APIConnection * | ||||
|   ValveStateResponse resp; | ||||
|   resp.position = valve->position; | ||||
|   resp.current_operation = static_cast<enums::ValveOperation>(valve->current_operation); | ||||
|   resp.key = valve->get_object_id_hash(); | ||||
|   fill_entity_state_base(valve, resp); | ||||
|   return encode_message_to_buffer(resp, ValveStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| void APIConnection::send_valve_info(valve::Valve *valve) { | ||||
| @@ -1095,7 +1111,7 @@ uint16_t APIConnection::try_send_media_player_state(EntityBase *entity, APIConne | ||||
|   resp.state = static_cast<enums::MediaPlayerState>(report_state); | ||||
|   resp.volume = media_player->volume; | ||||
|   resp.muted = media_player->is_muted(); | ||||
|   resp.key = media_player->get_object_id_hash(); | ||||
|   fill_entity_state_base(media_player, resp); | ||||
|   return encode_message_to_buffer(resp, MediaPlayerStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| void APIConnection::send_media_player_info(media_player::MediaPlayer *media_player) { | ||||
| @@ -1359,7 +1375,7 @@ uint16_t APIConnection::try_send_alarm_control_panel_state(EntityBase *entity, A | ||||
|   auto *a_alarm_control_panel = static_cast<alarm_control_panel::AlarmControlPanel *>(entity); | ||||
|   AlarmControlPanelStateResponse resp; | ||||
|   resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state()); | ||||
|   resp.key = a_alarm_control_panel->get_object_id_hash(); | ||||
|   fill_entity_state_base(a_alarm_control_panel, resp); | ||||
|   return encode_message_to_buffer(resp, AlarmControlPanelStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| void APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { | ||||
| @@ -1423,7 +1439,7 @@ uint16_t APIConnection::try_send_event_response(event::Event *event, const std:: | ||||
|                                                 uint32_t remaining_size, bool is_single) { | ||||
|   EventResponse resp; | ||||
|   resp.event_type = event_type; | ||||
|   resp.key = event->get_object_id_hash(); | ||||
|   fill_entity_state_base(event, resp); | ||||
|   return encode_message_to_buffer(resp, EventResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
|  | ||||
| @@ -1461,7 +1477,7 @@ uint16_t APIConnection::try_send_update_state(EntityBase *entity, APIConnection | ||||
|     resp.release_summary = update->update_info.summary; | ||||
|     resp.release_url = update->update_info.release_url; | ||||
|   } | ||||
|   resp.key = update->get_object_id_hash(); | ||||
|   fill_entity_state_base(update, resp); | ||||
|   return encode_message_to_buffer(resp, UpdateStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| void APIConnection::send_update_info(update::UpdateEntity *update) { | ||||
| @@ -1522,14 +1538,13 @@ bool APIConnection::try_send_log_message(int level, const char *tag, const char | ||||
|   buffer.encode_string(3, line, line_length);             // string message = 3 | ||||
|  | ||||
|   // SubscribeLogsResponse - 29 | ||||
|   return this->send_buffer(buffer, 29); | ||||
|   return this->send_buffer(buffer, SubscribeLogsResponse::MESSAGE_TYPE); | ||||
| } | ||||
|  | ||||
| HelloResponse APIConnection::hello(const HelloRequest &msg) { | ||||
|   this->client_info_ = msg.client_info; | ||||
|   this->client_peername_ = this->helper_->getpeername(); | ||||
|   this->client_combined_info_ = this->client_info_ + " (" + this->client_peername_ + ")"; | ||||
|   this->helper_->set_log_info(this->client_combined_info_); | ||||
|   this->helper_->set_log_info(this->get_client_combined_info()); | ||||
|   this->client_api_version_major_ = msg.api_version_major; | ||||
|   this->client_api_version_minor_ = msg.api_version_minor; | ||||
|   ESP_LOGV(TAG, "Hello from client: '%s' | %s | API Version %" PRIu32 ".%" PRIu32, this->client_info_.c_str(), | ||||
| @@ -1551,7 +1566,7 @@ ConnectResponse APIConnection::connect(const ConnectRequest &msg) { | ||||
|   // bool invalid_password = 1; | ||||
|   resp.invalid_password = !correct; | ||||
|   if (correct) { | ||||
|     ESP_LOGD(TAG, "%s connected", this->client_combined_info_.c_str()); | ||||
|     ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str()); | ||||
|     this->connection_state_ = ConnectionState::AUTHENTICATED; | ||||
|     this->parent_->get_client_connected_trigger()->trigger(this->client_info_, this->client_peername_); | ||||
| #ifdef USE_HOMEASSISTANT_TIME | ||||
| @@ -1657,7 +1672,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { | ||||
|   APIError err = this->helper_->loop(); | ||||
|   if (err != APIError::OK) { | ||||
|     on_fatal_error(); | ||||
|     ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->client_combined_info_.c_str(), | ||||
|     ESP_LOGW(TAG, "%s: Socket operation failed: %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|              api_error_to_str(err), errno); | ||||
|     return false; | ||||
|   } | ||||
| @@ -1669,7 +1684,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) { | ||||
|   return false; | ||||
| } | ||||
| bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) { | ||||
|   if (!this->try_to_clear_buffer(message_type != 29)) {  // SubscribeLogsResponse | ||||
|   if (!this->try_to_clear_buffer(message_type != SubscribeLogsResponse::MESSAGE_TYPE)) {  // SubscribeLogsResponse | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
| @@ -1679,10 +1694,10 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) | ||||
|   if (err != APIError::OK) { | ||||
|     on_fatal_error(); | ||||
|     if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { | ||||
|       ESP_LOGW(TAG, "%s: Connection reset", this->client_combined_info_.c_str()); | ||||
|       ESP_LOGW(TAG, "%s: Connection reset", this->get_client_combined_info().c_str()); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), | ||||
|                errno); | ||||
|       ESP_LOGW(TAG, "%s: Packet write failed %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|                api_error_to_str(err), errno); | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
| @@ -1691,11 +1706,11 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) | ||||
| } | ||||
| void APIConnection::on_unauthenticated_access() { | ||||
|   this->on_fatal_error(); | ||||
|   ESP_LOGD(TAG, "%s requested access without authentication", this->client_combined_info_.c_str()); | ||||
|   ESP_LOGD(TAG, "%s requested access without authentication", this->get_client_combined_info().c_str()); | ||||
| } | ||||
| void APIConnection::on_no_setup_connection() { | ||||
|   this->on_fatal_error(); | ||||
|   ESP_LOGD(TAG, "%s requested access without full connection", this->client_combined_info_.c_str()); | ||||
|   ESP_LOGD(TAG, "%s requested access without full connection", this->get_client_combined_info().c_str()); | ||||
| } | ||||
| void APIConnection::on_fatal_error() { | ||||
|   this->helper_->close(); | ||||
| @@ -1791,7 +1806,7 @@ void APIConnection::process_batch_() { | ||||
|   this->batch_first_message_ = true; | ||||
|  | ||||
|   size_t items_processed = 0; | ||||
|   uint32_t remaining_size = MAX_PACKET_SIZE; | ||||
|   uint16_t remaining_size = std::numeric_limits<uint16_t>::max(); | ||||
|  | ||||
|   // Track where each message's header padding begins in the buffer | ||||
|   // For plaintext: this is where the 6-byte header padding starts | ||||
| @@ -1816,11 +1831,15 @@ void APIConnection::process_batch_() { | ||||
|     packet_info.emplace_back(item.message_type, current_offset, proto_payload_size); | ||||
|  | ||||
|     // Update tracking variables | ||||
|     items_processed++; | ||||
|     // After first message, set remaining size to MAX_PACKET_SIZE to avoid fragmentation | ||||
|     if (items_processed == 1) { | ||||
|       remaining_size = MAX_PACKET_SIZE; | ||||
|     } | ||||
|     remaining_size -= payload_size; | ||||
|     // Calculate where the next message's header padding will start | ||||
|     // Current buffer size + footer space (that prepare_message_buffer will add for this message) | ||||
|     current_offset = this->parent_->get_shared_buffer_ref().size() + footer_size; | ||||
|     items_processed++; | ||||
|   } | ||||
|  | ||||
|   if (items_processed == 0) { | ||||
| @@ -1840,10 +1859,10 @@ void APIConnection::process_batch_() { | ||||
|   if (err != APIError::OK && err != APIError::WOULD_BLOCK) { | ||||
|     on_fatal_error(); | ||||
|     if (err == APIError::SOCKET_WRITE_FAILED && errno == ECONNRESET) { | ||||
|       ESP_LOGW(TAG, "%s: Connection reset during batch write", this->client_combined_info_.c_str()); | ||||
|       ESP_LOGW(TAG, "%s: Connection reset during batch write", this->get_client_combined_info().c_str()); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->client_combined_info_.c_str(), api_error_to_str(err), | ||||
|                errno); | ||||
|       ESP_LOGW(TAG, "%s: Batch write failed %s errno=%d", this->get_client_combined_info().c_str(), | ||||
|                api_error_to_str(err), errno); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -240,8 +240,8 @@ class APIConnection : public APIServerConnection { | ||||
|     // - Header padding: space for protocol headers (7 bytes for Noise, 6 for Plaintext) | ||||
|     // - Footer: space for MAC (16 bytes for Noise, 0 for Plaintext) | ||||
|     shared_buf.reserve(reserve_size + header_padding + this->helper_->frame_footer_size()); | ||||
|     // Insert header padding bytes so message encoding starts at the correct position | ||||
|     shared_buf.insert(shared_buf.begin(), header_padding, 0); | ||||
|     // Resize to add header padding so message encoding starts at the correct position | ||||
|     shared_buf.resize(header_padding); | ||||
|     return {&shared_buf}; | ||||
|   } | ||||
|  | ||||
| @@ -249,47 +249,47 @@ class APIConnection : public APIServerConnection { | ||||
|   ProtoWriteBuffer prepare_message_buffer(uint16_t message_size, bool is_first_message) { | ||||
|     // Get reference to shared buffer (it maintains state between batch messages) | ||||
|     std::vector<uint8_t> &shared_buf = this->parent_->get_shared_buffer_ref(); | ||||
|     size_t current_size = shared_buf.size(); | ||||
|  | ||||
|     if (is_first_message) { | ||||
|       // For first message, initialize buffer with header padding | ||||
|       uint8_t header_padding = this->helper_->frame_header_padding(); | ||||
|       shared_buf.clear(); | ||||
|       shared_buf.reserve(message_size + header_padding); | ||||
|       shared_buf.resize(header_padding); | ||||
|       // Fill header padding with zeros | ||||
|       std::fill(shared_buf.begin(), shared_buf.end(), 0); | ||||
|     } else { | ||||
|       // For subsequent messages, add footer space for previous message and header for this message | ||||
|       uint8_t footer_size = this->helper_->frame_footer_size(); | ||||
|       uint8_t header_padding = this->helper_->frame_header_padding(); | ||||
|  | ||||
|       // Reserve additional space for everything | ||||
|       shared_buf.reserve(current_size + footer_size + header_padding + message_size); | ||||
|  | ||||
|       // Single resize to add both footer and header padding | ||||
|       size_t new_size = current_size + footer_size + header_padding; | ||||
|       shared_buf.resize(new_size); | ||||
|  | ||||
|       // Fill the newly added bytes with zeros (footer + header padding) | ||||
|       std::fill(shared_buf.begin() + current_size, shared_buf.end(), 0); | ||||
|     } | ||||
|  | ||||
|     size_t current_size = shared_buf.size(); | ||||
|  | ||||
|     // Calculate padding to add: | ||||
|     // - First message: just header padding | ||||
|     // - Subsequent messages: footer for previous message + header padding for this message | ||||
|     size_t padding_to_add = is_first_message | ||||
|                                 ? this->helper_->frame_header_padding() | ||||
|                                 : this->helper_->frame_header_padding() + this->helper_->frame_footer_size(); | ||||
|  | ||||
|     // Reserve space for padding + message | ||||
|     shared_buf.reserve(current_size + padding_to_add + message_size); | ||||
|  | ||||
|     // Resize to add the padding bytes | ||||
|     shared_buf.resize(current_size + padding_to_add); | ||||
|  | ||||
|     return {&shared_buf}; | ||||
|   } | ||||
|  | ||||
|   bool try_to_clear_buffer(bool log_out_of_space); | ||||
|   bool send_buffer(ProtoWriteBuffer buffer, uint16_t message_type) override; | ||||
|  | ||||
|   std::string get_client_combined_info() const { return this->client_combined_info_; } | ||||
|   std::string get_client_combined_info() const { | ||||
|     if (this->client_info_ == this->client_peername_) { | ||||
|       // Before Hello message, both are the same (just IP:port) | ||||
|       return this->client_info_; | ||||
|     } | ||||
|     return this->client_info_ + " (" + this->client_peername_ + ")"; | ||||
|   } | ||||
|  | ||||
|   // Buffer allocator methods for batch processing | ||||
|   ProtoWriteBuffer allocate_single_message_buffer(uint16_t size); | ||||
|   ProtoWriteBuffer allocate_batch_message_buffer(uint16_t size); | ||||
|  | ||||
|  protected: | ||||
|   // Helper function to fill common entity fields | ||||
|   template<typename ResponseT> static void fill_entity_info_base(esphome::EntityBase *entity, ResponseT &response) { | ||||
|   // Helper function to fill common entity info fields | ||||
|   static void fill_entity_info_base(esphome::EntityBase *entity, InfoResponseProtoMessage &response) { | ||||
|     // Set common fields that are shared by all entity types | ||||
|     response.key = entity->get_object_id_hash(); | ||||
|     response.object_id = entity->get_object_id(); | ||||
| @@ -303,6 +303,11 @@ class APIConnection : public APIServerConnection { | ||||
|     response.entity_category = static_cast<enums::EntityCategory>(entity->get_entity_category()); | ||||
|   } | ||||
|  | ||||
|   // Helper function to fill common entity state fields | ||||
|   static void fill_entity_state_base(esphome::EntityBase *entity, StateResponseProtoMessage &response) { | ||||
|     response.key = entity->get_object_id_hash(); | ||||
|   } | ||||
|  | ||||
|   // Non-template helper to encode any ProtoMessage | ||||
|   static uint16_t encode_message_to_buffer(ProtoMessage &msg, uint16_t message_type, APIConnection *conn, | ||||
|                                            uint32_t remaining_size, bool is_single); | ||||
| @@ -433,37 +438,44 @@ class APIConnection : public APIServerConnection { | ||||
|   // Helper function to get estimated message size for buffer pre-allocation | ||||
|   static uint16_t get_estimated_message_size(uint16_t message_type); | ||||
|  | ||||
|   enum class ConnectionState { | ||||
|   // Pointers first (4 bytes each, naturally aligned) | ||||
|   std::unique_ptr<APIFrameHelper> helper_; | ||||
|   APIServer *parent_; | ||||
|  | ||||
|   // 4-byte aligned types | ||||
|   uint32_t last_traffic_; | ||||
|   uint32_t next_ping_retry_{0}; | ||||
|   int state_subs_at_ = -1; | ||||
|  | ||||
|   // Strings (12 bytes each on 32-bit) | ||||
|   std::string client_info_; | ||||
|   std::string client_peername_; | ||||
|  | ||||
|   // 2-byte aligned types | ||||
|   uint16_t client_api_version_major_{0}; | ||||
|   uint16_t client_api_version_minor_{0}; | ||||
|  | ||||
|   // Group all 1-byte types together to minimize padding | ||||
|   enum class ConnectionState : uint8_t { | ||||
|     WAITING_FOR_HELLO, | ||||
|     CONNECTED, | ||||
|     AUTHENTICATED, | ||||
|   } connection_state_{ConnectionState::WAITING_FOR_HELLO}; | ||||
|  | ||||
|   uint8_t log_subscription_{ESPHOME_LOG_LEVEL_NONE}; | ||||
|   bool remove_{false}; | ||||
|  | ||||
|   std::unique_ptr<APIFrameHelper> helper_; | ||||
|  | ||||
|   std::string client_info_; | ||||
|   std::string client_peername_; | ||||
|   std::string client_combined_info_; | ||||
|   uint32_t client_api_version_major_{0}; | ||||
|   uint32_t client_api_version_minor_{0}; | ||||
| #ifdef USE_ESP32_CAMERA | ||||
|   esp32_camera::CameraImageReader image_reader_; | ||||
| #endif | ||||
|  | ||||
|   bool state_subscription_{false}; | ||||
|   int log_subscription_{ESPHOME_LOG_LEVEL_NONE}; | ||||
|   uint32_t last_traffic_; | ||||
|   uint32_t next_ping_retry_{0}; | ||||
|   uint8_t ping_retries_{0}; | ||||
|   bool sent_ping_{false}; | ||||
|   bool service_call_subscription_{false}; | ||||
|   bool next_close_ = false; | ||||
|   APIServer *parent_; | ||||
|   uint8_t ping_retries_{0}; | ||||
|   // 8 bytes used, no padding needed | ||||
|  | ||||
|   // Larger objects at the end | ||||
|   InitialStateIterator initial_state_iterator_; | ||||
|   ListEntitiesIterator list_entities_iterator_; | ||||
|   int state_subs_at_ = -1; | ||||
| #ifdef USE_ESP32_CAMERA | ||||
|   esp32_camera::CameraImageReader image_reader_; | ||||
| #endif | ||||
|  | ||||
|   // Function pointer type for message encoding | ||||
|   using MessageCreatorPtr = uint16_t (*)(EntityBase *, APIConnection *, uint32_t remaining_size, bool is_single); | ||||
|   | ||||
| @@ -125,38 +125,6 @@ class APIFrameHelper { | ||||
|     const uint8_t *current_data() const { return data.data() + offset; } | ||||
|   }; | ||||
|  | ||||
|   // Queue of data buffers to be sent | ||||
|   std::deque<SendBuffer> tx_buf_; | ||||
|  | ||||
|   // Common state enum for all frame helpers | ||||
|   // Note: Not all states are used by all implementations | ||||
|   // - INITIALIZE: Used by both Noise and Plaintext | ||||
|   // - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol | ||||
|   // - DATA: Used by both Noise and Plaintext | ||||
|   // - CLOSED: Used by both Noise and Plaintext | ||||
|   // - FAILED: Used by both Noise and Plaintext | ||||
|   // - EXPLICIT_REJECT: Only used by Noise protocol | ||||
|   enum class State { | ||||
|     INITIALIZE = 1, | ||||
|     CLIENT_HELLO = 2,  // Noise only | ||||
|     SERVER_HELLO = 3,  // Noise only | ||||
|     HANDSHAKE = 4,     // Noise only | ||||
|     DATA = 5, | ||||
|     CLOSED = 6, | ||||
|     FAILED = 7, | ||||
|     EXPLICIT_REJECT = 8,  // Noise only | ||||
|   }; | ||||
|  | ||||
|   // Current state of the frame helper | ||||
|   State state_{State::INITIALIZE}; | ||||
|  | ||||
|   // Helper name for logging | ||||
|   std::string info_; | ||||
|  | ||||
|   // Socket for communication | ||||
|   socket::Socket *socket_{nullptr}; | ||||
|   std::unique_ptr<socket::Socket> socket_owned_; | ||||
|  | ||||
|   // Common implementation for writing raw data to socket | ||||
|   APIError write_raw_(const struct iovec *iov, int iovcnt); | ||||
|  | ||||
| @@ -169,15 +137,41 @@ class APIFrameHelper { | ||||
|   APIError write_raw_(const struct iovec *iov, int iovcnt, socket::Socket *socket, std::vector<uint8_t> &tx_buf, | ||||
|                       const std::string &info, StateEnum &state, StateEnum failed_state); | ||||
|  | ||||
|   // Pointers first (4 bytes each) | ||||
|   socket::Socket *socket_{nullptr}; | ||||
|   std::unique_ptr<socket::Socket> socket_owned_; | ||||
|  | ||||
|   // Common state enum for all frame helpers | ||||
|   // Note: Not all states are used by all implementations | ||||
|   // - INITIALIZE: Used by both Noise and Plaintext | ||||
|   // - CLIENT_HELLO, SERVER_HELLO, HANDSHAKE: Only used by Noise protocol | ||||
|   // - DATA: Used by both Noise and Plaintext | ||||
|   // - CLOSED: Used by both Noise and Plaintext | ||||
|   // - FAILED: Used by both Noise and Plaintext | ||||
|   // - EXPLICIT_REJECT: Only used by Noise protocol | ||||
|   enum class State : uint8_t { | ||||
|     INITIALIZE = 1, | ||||
|     CLIENT_HELLO = 2,  // Noise only | ||||
|     SERVER_HELLO = 3,  // Noise only | ||||
|     HANDSHAKE = 4,     // Noise only | ||||
|     DATA = 5, | ||||
|     CLOSED = 6, | ||||
|     FAILED = 7, | ||||
|     EXPLICIT_REJECT = 8,  // Noise only | ||||
|   }; | ||||
|  | ||||
|   // Containers (size varies, but typically 12+ bytes on 32-bit) | ||||
|   std::deque<SendBuffer> tx_buf_; | ||||
|   std::string info_; | ||||
|   std::vector<struct iovec> reusable_iovs_; | ||||
|   std::vector<uint8_t> rx_buf_; | ||||
|  | ||||
|   // Group smaller types together | ||||
|   uint16_t rx_buf_len_ = 0; | ||||
|   State state_{State::INITIALIZE}; | ||||
|   uint8_t frame_header_padding_{0}; | ||||
|   uint8_t frame_footer_size_{0}; | ||||
|  | ||||
|   // Reusable IOV array for write_protobuf_packets to avoid repeated allocations | ||||
|   std::vector<struct iovec> reusable_iovs_; | ||||
|  | ||||
|   // Receive buffer for reading frame data | ||||
|   std::vector<uint8_t> rx_buf_; | ||||
|   uint16_t rx_buf_len_ = 0; | ||||
|   // 5 bytes total, 3 bytes padding | ||||
|  | ||||
|   // Common initialization for both plaintext and noise protocols | ||||
|   APIError init_common_(); | ||||
| @@ -213,19 +207,28 @@ class APINoiseFrameHelper : public APIFrameHelper { | ||||
|   APIError init_handshake_(); | ||||
|   APIError check_handshake_finished_(); | ||||
|   void send_explicit_handshake_reject_(const std::string &reason); | ||||
|  | ||||
|   // Pointers first (4 bytes each) | ||||
|   NoiseHandshakeState *handshake_{nullptr}; | ||||
|   NoiseCipherState *send_cipher_{nullptr}; | ||||
|   NoiseCipherState *recv_cipher_{nullptr}; | ||||
|  | ||||
|   // Shared pointer (8 bytes on 32-bit = 4 bytes control block pointer + 4 bytes object pointer) | ||||
|   std::shared_ptr<APINoiseContext> ctx_; | ||||
|  | ||||
|   // Vector (12 bytes on 32-bit) | ||||
|   std::vector<uint8_t> prologue_; | ||||
|  | ||||
|   // NoiseProtocolId (size depends on implementation) | ||||
|   NoiseProtocolId nid_; | ||||
|  | ||||
|   // Group small types together | ||||
|   // Fixed-size header buffer for noise protocol: | ||||
|   // 1 byte for indicator + 2 bytes for message size (16-bit value, not varint) | ||||
|   // Note: Maximum message size is UINT16_MAX (65535), with a limit of 128 bytes during handshake phase | ||||
|   uint8_t rx_header_buf_[3]; | ||||
|   uint8_t rx_header_buf_len_ = 0; | ||||
|  | ||||
|   std::vector<uint8_t> prologue_; | ||||
|  | ||||
|   std::shared_ptr<APINoiseContext> ctx_; | ||||
|   NoiseHandshakeState *handshake_{nullptr}; | ||||
|   NoiseCipherState *send_cipher_{nullptr}; | ||||
|   NoiseCipherState *recv_cipher_{nullptr}; | ||||
|   NoiseProtocolId nid_; | ||||
|   // 4 bytes total, no padding | ||||
| }; | ||||
| #endif  // USE_API_NOISE | ||||
|  | ||||
| @@ -252,6 +255,12 @@ class APIPlaintextFrameHelper : public APIFrameHelper { | ||||
|  | ||||
|  protected: | ||||
|   APIError try_read_frame_(ParsedFrame *frame); | ||||
|  | ||||
|   // Group 2-byte aligned types | ||||
|   uint16_t rx_header_parsed_type_ = 0; | ||||
|   uint16_t rx_header_parsed_len_ = 0; | ||||
|  | ||||
|   // Group 1-byte types together | ||||
|   // Fixed-size header buffer for plaintext protocol: | ||||
|   // We now store the indicator byte + the two varints. | ||||
|   // To match noise protocol's maximum message size (UINT16_MAX = 65535), we need: | ||||
| @@ -263,8 +272,7 @@ class APIPlaintextFrameHelper : public APIFrameHelper { | ||||
|   uint8_t rx_header_buf_[6];  // 1 byte indicator + 5 bytes for varints (3 for size + 2 for type) | ||||
|   uint8_t rx_header_buf_pos_ = 0; | ||||
|   bool rx_header_parsed_ = false; | ||||
|   uint16_t rx_header_parsed_type_ = 0; | ||||
|   uint16_t rx_header_parsed_len_ = 0; | ||||
|   // 8 bytes total, no padding needed | ||||
| }; | ||||
| #endif | ||||
|  | ||||
|   | ||||
| @@ -21,4 +21,5 @@ extend google.protobuf.MessageOptions { | ||||
|     optional string ifdef = 1038; | ||||
|     optional bool log = 1039 [default=true]; | ||||
|     optional bool no_delay = 1040 [default=false]; | ||||
|     optional string base_class = 1041; | ||||
| } | ||||
|   | ||||
| @@ -516,6 +516,8 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantEvent>(enums::V | ||||
|       return "VOICE_ASSISTANT_TTS_STREAM_START"; | ||||
|     case enums::VOICE_ASSISTANT_TTS_STREAM_END: | ||||
|       return "VOICE_ASSISTANT_TTS_STREAM_END"; | ||||
|     case enums::VOICE_ASSISTANT_INTENT_PROGRESS: | ||||
|       return "VOICE_ASSISTANT_INTENT_PROGRESS"; | ||||
|     default: | ||||
|       return "UNKNOWN"; | ||||
|   } | ||||
| @@ -628,6 +630,7 @@ template<> const char *proto_enum_to_string<enums::UpdateCommand>(enums::UpdateC | ||||
|   } | ||||
| } | ||||
| #endif | ||||
|  | ||||
| bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||
|   switch (field_id) { | ||||
|     case 2: { | ||||
| @@ -794,28 +797,18 @@ void ConnectResponse::dump_to(std::string &out) const { | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| void DisconnectRequest::encode(ProtoWriteBuffer buffer) const {} | ||||
| void DisconnectRequest::calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void DisconnectRequest::dump_to(std::string &out) const { out.append("DisconnectRequest {}"); } | ||||
| #endif | ||||
| void DisconnectResponse::encode(ProtoWriteBuffer buffer) const {} | ||||
| void DisconnectResponse::calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void DisconnectResponse::dump_to(std::string &out) const { out.append("DisconnectResponse {}"); } | ||||
| #endif | ||||
| void PingRequest::encode(ProtoWriteBuffer buffer) const {} | ||||
| void PingRequest::calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void PingRequest::dump_to(std::string &out) const { out.append("PingRequest {}"); } | ||||
| #endif | ||||
| void PingResponse::encode(ProtoWriteBuffer buffer) const {} | ||||
| void PingResponse::calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void PingResponse::dump_to(std::string &out) const { out.append("PingResponse {}"); } | ||||
| #endif | ||||
| void DeviceInfoRequest::encode(ProtoWriteBuffer buffer) const {} | ||||
| void DeviceInfoRequest::calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void DeviceInfoRequest::dump_to(std::string &out) const { out.append("DeviceInfoRequest {}"); } | ||||
| #endif | ||||
| @@ -1036,18 +1029,12 @@ void DeviceInfoResponse::dump_to(std::string &out) const { | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| void ListEntitiesRequest::encode(ProtoWriteBuffer buffer) const {} | ||||
| void ListEntitiesRequest::calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesRequest::dump_to(std::string &out) const { out.append("ListEntitiesRequest {}"); } | ||||
| #endif | ||||
| void ListEntitiesDoneResponse::encode(ProtoWriteBuffer buffer) const {} | ||||
| void ListEntitiesDoneResponse::calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void ListEntitiesDoneResponse::dump_to(std::string &out) const { out.append("ListEntitiesDoneResponse {}"); } | ||||
| #endif | ||||
| void SubscribeStatesRequest::encode(ProtoWriteBuffer buffer) const {} | ||||
| void SubscribeStatesRequest::calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void SubscribeStatesRequest::dump_to(std::string &out) const { out.append("SubscribeStatesRequest {}"); } | ||||
| #endif | ||||
| @@ -3368,8 +3355,6 @@ void NoiseEncryptionSetKeyResponse::dump_to(std::string &out) const { | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| void SubscribeHomeassistantServicesRequest::encode(ProtoWriteBuffer buffer) const {} | ||||
| void SubscribeHomeassistantServicesRequest::calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void SubscribeHomeassistantServicesRequest::dump_to(std::string &out) const { | ||||
|   out.append("SubscribeHomeassistantServicesRequest {}"); | ||||
| @@ -3495,8 +3480,6 @@ void HomeassistantServiceResponse::dump_to(std::string &out) const { | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| void SubscribeHomeAssistantStatesRequest::encode(ProtoWriteBuffer buffer) const {} | ||||
| void SubscribeHomeAssistantStatesRequest::calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void SubscribeHomeAssistantStatesRequest::dump_to(std::string &out) const { | ||||
|   out.append("SubscribeHomeAssistantStatesRequest {}"); | ||||
| @@ -3600,8 +3583,6 @@ void HomeAssistantStateResponse::dump_to(std::string &out) const { | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| void GetTimeRequest::encode(ProtoWriteBuffer buffer) const {} | ||||
| void GetTimeRequest::calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void GetTimeRequest::dump_to(std::string &out) const { out.append("GetTimeRequest {}"); } | ||||
| #endif | ||||
| @@ -7496,8 +7477,6 @@ void BluetoothGATTNotifyDataResponse::dump_to(std::string &out) const { | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| void SubscribeBluetoothConnectionsFreeRequest::encode(ProtoWriteBuffer buffer) const {} | ||||
| void SubscribeBluetoothConnectionsFreeRequest::calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void SubscribeBluetoothConnectionsFreeRequest::dump_to(std::string &out) const { | ||||
|   out.append("SubscribeBluetoothConnectionsFreeRequest {}"); | ||||
| @@ -7781,8 +7760,6 @@ void BluetoothDeviceUnpairingResponse::dump_to(std::string &out) const { | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| void UnsubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const {} | ||||
| void UnsubscribeBluetoothLEAdvertisementsRequest::calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void UnsubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { | ||||
|   out.append("UnsubscribeBluetoothLEAdvertisementsRequest {}"); | ||||
| @@ -8448,8 +8425,6 @@ void VoiceAssistantWakeWord::dump_to(std::string &out) const { | ||||
|   out.append("}"); | ||||
| } | ||||
| #endif | ||||
| void VoiceAssistantConfigurationRequest::encode(ProtoWriteBuffer buffer) const {} | ||||
| void VoiceAssistantConfigurationRequest::calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| void VoiceAssistantConfigurationRequest::dump_to(std::string &out) const { | ||||
|   out.append("VoiceAssistantConfigurationRequest {}"); | ||||
|   | ||||
| @@ -208,6 +208,7 @@ enum VoiceAssistantEvent : uint32_t { | ||||
|   VOICE_ASSISTANT_STT_VAD_END = 12, | ||||
|   VOICE_ASSISTANT_TTS_STREAM_START = 98, | ||||
|   VOICE_ASSISTANT_TTS_STREAM_END = 99, | ||||
|   VOICE_ASSISTANT_INTENT_PROGRESS = 100, | ||||
| }; | ||||
| enum VoiceAssistantTimerEvent : uint32_t { | ||||
|   VOICE_ASSISTANT_TIMER_STARTED = 0, | ||||
| @@ -253,6 +254,27 @@ enum UpdateCommand : uint32_t { | ||||
|  | ||||
| }  // namespace enums | ||||
|  | ||||
| class InfoResponseProtoMessage : public ProtoMessage { | ||||
|  public: | ||||
|   ~InfoResponseProtoMessage() override = default; | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   std::string icon{}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|  | ||||
|  protected: | ||||
| }; | ||||
|  | ||||
| class StateResponseProtoMessage : public ProtoMessage { | ||||
|  public: | ||||
|   ~StateResponseProtoMessage() override = default; | ||||
|   uint32_t key{0}; | ||||
|  | ||||
|  protected: | ||||
| }; | ||||
| class HelloRequest : public ProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 1; | ||||
| @@ -335,8 +357,6 @@ class DisconnectRequest : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "disconnect_request"; } | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -350,8 +370,6 @@ class DisconnectResponse : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "disconnect_response"; } | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -365,8 +383,6 @@ class PingRequest : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "ping_request"; } | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -380,8 +396,6 @@ class PingResponse : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "ping_response"; } | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -395,8 +409,6 @@ class DeviceInfoRequest : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "device_info_request"; } | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -446,8 +458,6 @@ class ListEntitiesRequest : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_request"; } | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -461,8 +471,6 @@ class ListEntitiesDoneResponse : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_done_response"; } | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -476,30 +484,21 @@ class SubscribeStatesRequest : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "subscribe_states_request"; } | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
|  | ||||
|  protected: | ||||
| }; | ||||
| class ListEntitiesBinarySensorResponse : public ProtoMessage { | ||||
| class ListEntitiesBinarySensorResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 12; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 56; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_binary_sensor_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string device_class{}; | ||||
|   bool is_status_binary_sensor{false}; | ||||
|   bool disabled_by_default{false}; | ||||
|   std::string icon{}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -511,14 +510,13 @@ class ListEntitiesBinarySensorResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class BinarySensorStateResponse : public ProtoMessage { | ||||
| class BinarySensorStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 21; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 9; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "binary_sensor_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   bool state{false}; | ||||
|   bool missing_state{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| @@ -531,24 +529,17 @@ class BinarySensorStateResponse : public ProtoMessage { | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesCoverResponse : public ProtoMessage { | ||||
| class ListEntitiesCoverResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 13; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 62; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_cover_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   bool assumed_state{false}; | ||||
|   bool supports_position{false}; | ||||
|   bool supports_tilt{false}; | ||||
|   std::string device_class{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   std::string icon{}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   bool supports_stop{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -561,14 +552,13 @@ class ListEntitiesCoverResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class CoverStateResponse : public ProtoMessage { | ||||
| class CoverStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 22; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 19; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "cover_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   enums::LegacyCoverState legacy_state{}; | ||||
|   float position{0.0f}; | ||||
|   float tilt{0.0f}; | ||||
| @@ -608,24 +598,17 @@ class CoverCommandRequest : public ProtoMessage { | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesFanResponse : public ProtoMessage { | ||||
| class ListEntitiesFanResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 14; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 73; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_fan_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   bool supports_oscillation{false}; | ||||
|   bool supports_speed{false}; | ||||
|   bool supports_direction{false}; | ||||
|   int32_t supported_speed_count{0}; | ||||
|   bool disabled_by_default{false}; | ||||
|   std::string icon{}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   std::vector<std::string> supported_preset_modes{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -638,14 +621,13 @@ class ListEntitiesFanResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class FanStateResponse : public ProtoMessage { | ||||
| class FanStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 23; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 26; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "fan_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   bool state{false}; | ||||
|   bool oscillating{false}; | ||||
|   enums::FanSpeed speed{}; | ||||
| @@ -694,17 +676,13 @@ class FanCommandRequest : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesLightResponse : public ProtoMessage { | ||||
| class ListEntitiesLightResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 15; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 85; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_light_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::vector<enums::ColorMode> supported_color_modes{}; | ||||
|   bool legacy_supports_brightness{false}; | ||||
|   bool legacy_supports_rgb{false}; | ||||
| @@ -713,9 +691,6 @@ class ListEntitiesLightResponse : public ProtoMessage { | ||||
|   float min_mireds{0.0f}; | ||||
|   float max_mireds{0.0f}; | ||||
|   std::vector<std::string> effects{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   std::string icon{}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -727,14 +702,13 @@ class ListEntitiesLightResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class LightStateResponse : public ProtoMessage { | ||||
| class LightStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 24; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 63; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "light_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   bool state{false}; | ||||
|   float brightness{0.0f}; | ||||
|   enums::ColorMode color_mode{}; | ||||
| @@ -803,26 +777,19 @@ class LightCommandRequest : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesSensorResponse : public ProtoMessage { | ||||
| class ListEntitiesSensorResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 16; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 73; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_sensor_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   std::string unit_of_measurement{}; | ||||
|   int32_t accuracy_decimals{0}; | ||||
|   bool force_update{false}; | ||||
|   std::string device_class{}; | ||||
|   enums::SensorStateClass state_class{}; | ||||
|   enums::SensorLastResetType legacy_last_reset_type{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -834,14 +801,13 @@ class ListEntitiesSensorResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class SensorStateResponse : public ProtoMessage { | ||||
| class SensorStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 25; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 12; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "sensor_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   float state{0.0f}; | ||||
|   bool missing_state{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| @@ -854,21 +820,14 @@ class SensorStateResponse : public ProtoMessage { | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesSwitchResponse : public ProtoMessage { | ||||
| class ListEntitiesSwitchResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 17; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 56; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_switch_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool assumed_state{false}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   std::string device_class{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -881,14 +840,13 @@ class ListEntitiesSwitchResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class SwitchStateResponse : public ProtoMessage { | ||||
| class SwitchStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 26; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 7; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "switch_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   bool state{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -919,20 +877,13 @@ class SwitchCommandRequest : public ProtoMessage { | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesTextSensorResponse : public ProtoMessage { | ||||
| class ListEntitiesTextSensorResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 18; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 54; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_text_sensor_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   std::string device_class{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -945,14 +896,13 @@ class ListEntitiesTextSensorResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class TextSensorStateResponse : public ProtoMessage { | ||||
| class TextSensorStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 27; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 16; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "text_sensor_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   std::string state{}; | ||||
|   bool missing_state{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| @@ -1045,8 +995,6 @@ class SubscribeHomeassistantServicesRequest : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "subscribe_homeassistant_services_request"; } | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -1095,8 +1043,6 @@ class SubscribeHomeAssistantStatesRequest : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "subscribe_home_assistant_states_request"; } | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -1149,8 +1095,6 @@ class GetTimeRequest : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "get_time_request"; } | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -1249,20 +1193,13 @@ class ExecuteServiceRequest : public ProtoMessage { | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
| }; | ||||
| class ListEntitiesCameraResponse : public ProtoMessage { | ||||
| class ListEntitiesCameraResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 43; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 45; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_camera_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   std::string icon{}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1313,17 +1250,13 @@ class CameraImageRequest : public ProtoMessage { | ||||
|  protected: | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesClimateResponse : public ProtoMessage { | ||||
| class ListEntitiesClimateResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 46; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 151; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_climate_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   bool supports_current_temperature{false}; | ||||
|   bool supports_two_point_target_temperature{false}; | ||||
|   std::vector<enums::ClimateMode> supported_modes{}; | ||||
| @@ -1337,9 +1270,6 @@ class ListEntitiesClimateResponse : public ProtoMessage { | ||||
|   std::vector<std::string> supported_custom_fan_modes{}; | ||||
|   std::vector<enums::ClimatePreset> supported_presets{}; | ||||
|   std::vector<std::string> supported_custom_presets{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   std::string icon{}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   float visual_current_temperature_step{0.0f}; | ||||
|   bool supports_current_humidity{false}; | ||||
|   bool supports_target_humidity{false}; | ||||
| @@ -1356,14 +1286,13 @@ class ListEntitiesClimateResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ClimateStateResponse : public ProtoMessage { | ||||
| class ClimateStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 47; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 65; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "climate_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   enums::ClimateMode mode{}; | ||||
|   float current_temperature{0.0f}; | ||||
|   float target_temperature{0.0f}; | ||||
| @@ -1430,23 +1359,16 @@ class ClimateCommandRequest : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesNumberResponse : public ProtoMessage { | ||||
| class ListEntitiesNumberResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 49; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 80; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_number_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   float min_value{0.0f}; | ||||
|   float max_value{0.0f}; | ||||
|   float step{0.0f}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   std::string unit_of_measurement{}; | ||||
|   enums::NumberMode mode{}; | ||||
|   std::string device_class{}; | ||||
| @@ -1461,14 +1383,13 @@ class ListEntitiesNumberResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class NumberStateResponse : public ProtoMessage { | ||||
| class NumberStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 50; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 12; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "number_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   float state{0.0f}; | ||||
|   bool missing_state{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| @@ -1499,21 +1420,14 @@ class NumberCommandRequest : public ProtoMessage { | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
| }; | ||||
| class ListEntitiesSelectResponse : public ProtoMessage { | ||||
| class ListEntitiesSelectResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 52; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 63; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_select_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   std::vector<std::string> options{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1525,14 +1439,13 @@ class ListEntitiesSelectResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class SelectStateResponse : public ProtoMessage { | ||||
| class SelectStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 53; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 16; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "select_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   std::string state{}; | ||||
|   bool missing_state{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| @@ -1565,23 +1478,16 @@ class SelectCommandRequest : public ProtoMessage { | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
| }; | ||||
| class ListEntitiesSirenResponse : public ProtoMessage { | ||||
| class ListEntitiesSirenResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 55; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 67; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_siren_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   std::vector<std::string> tones{}; | ||||
|   bool supports_duration{false}; | ||||
|   bool supports_volume{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1593,14 +1499,13 @@ class ListEntitiesSirenResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class SirenStateResponse : public ProtoMessage { | ||||
| class SirenStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 56; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 7; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "siren_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   bool state{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -1639,20 +1544,13 @@ class SirenCommandRequest : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesLockResponse : public ProtoMessage { | ||||
| class ListEntitiesLockResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 58; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 60; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_lock_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   bool assumed_state{false}; | ||||
|   bool supports_open{false}; | ||||
|   bool requires_code{false}; | ||||
| @@ -1668,14 +1566,13 @@ class ListEntitiesLockResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class LockStateResponse : public ProtoMessage { | ||||
| class LockStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 59; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 7; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "lock_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   enums::LockState state{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -1709,20 +1606,13 @@ class LockCommandRequest : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesButtonResponse : public ProtoMessage { | ||||
| class ListEntitiesButtonResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 61; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 54; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_button_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   std::string device_class{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -1769,20 +1659,13 @@ class MediaPlayerSupportedFormat : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesMediaPlayerResponse : public ProtoMessage { | ||||
| class ListEntitiesMediaPlayerResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 63; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 81; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_media_player_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   bool supports_pause{false}; | ||||
|   std::vector<MediaPlayerSupportedFormat> supported_formats{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| @@ -1796,14 +1679,13 @@ class ListEntitiesMediaPlayerResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class MediaPlayerStateResponse : public ProtoMessage { | ||||
| class MediaPlayerStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 64; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 14; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "media_player_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   enums::MediaPlayerState state{}; | ||||
|   float volume{0.0f}; | ||||
|   bool muted{false}; | ||||
| @@ -2213,8 +2095,6 @@ class SubscribeBluetoothConnectionsFreeRequest : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "subscribe_bluetooth_connections_free_request"; } | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -2340,8 +2220,6 @@ class UnsubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "unsubscribe_bluetooth_le_advertisements_request"; } | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -2608,8 +2486,6 @@ class VoiceAssistantConfigurationRequest : public ProtoMessage { | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "voice_assistant_configuration_request"; } | ||||
| #endif | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   void dump_to(std::string &out) const override; | ||||
| #endif | ||||
| @@ -2653,20 +2529,13 @@ class VoiceAssistantSetConfiguration : public ProtoMessage { | ||||
|  protected: | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
| }; | ||||
| class ListEntitiesAlarmControlPanelResponse : public ProtoMessage { | ||||
| class ListEntitiesAlarmControlPanelResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 94; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 53; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_alarm_control_panel_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   uint32_t supported_features{0}; | ||||
|   bool requires_code{false}; | ||||
|   bool requires_code_to_arm{false}; | ||||
| @@ -2681,14 +2550,13 @@ class ListEntitiesAlarmControlPanelResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class AlarmControlPanelStateResponse : public ProtoMessage { | ||||
| class AlarmControlPanelStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 95; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 7; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "alarm_control_panel_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   enums::AlarmControlPanelState state{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -2721,20 +2589,13 @@ class AlarmControlPanelCommandRequest : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesTextResponse : public ProtoMessage { | ||||
| class ListEntitiesTextResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 97; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 64; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_text_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   uint32_t min_length{0}; | ||||
|   uint32_t max_length{0}; | ||||
|   std::string pattern{}; | ||||
| @@ -2750,14 +2611,13 @@ class ListEntitiesTextResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class TextStateResponse : public ProtoMessage { | ||||
| class TextStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 98; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 16; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "text_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   std::string state{}; | ||||
|   bool missing_state{false}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| @@ -2790,20 +2650,13 @@ class TextCommandRequest : public ProtoMessage { | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
| }; | ||||
| class ListEntitiesDateResponse : public ProtoMessage { | ||||
| class ListEntitiesDateResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 100; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 45; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_date_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -2815,14 +2668,13 @@ class ListEntitiesDateResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class DateStateResponse : public ProtoMessage { | ||||
| class DateStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 101; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 19; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "date_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   bool missing_state{false}; | ||||
|   uint32_t year{0}; | ||||
|   uint32_t month{0}; | ||||
| @@ -2858,20 +2710,13 @@ class DateCommandRequest : public ProtoMessage { | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesTimeResponse : public ProtoMessage { | ||||
| class ListEntitiesTimeResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 103; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 45; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_time_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -2883,14 +2728,13 @@ class ListEntitiesTimeResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class TimeStateResponse : public ProtoMessage { | ||||
| class TimeStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 104; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 19; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "time_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   bool missing_state{false}; | ||||
|   uint32_t hour{0}; | ||||
|   uint32_t minute{0}; | ||||
| @@ -2926,20 +2770,13 @@ class TimeCommandRequest : public ProtoMessage { | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesEventResponse : public ProtoMessage { | ||||
| class ListEntitiesEventResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 107; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 72; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_event_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   std::string device_class{}; | ||||
|   std::vector<std::string> event_types{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| @@ -2953,14 +2790,13 @@ class ListEntitiesEventResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class EventResponse : public ProtoMessage { | ||||
| class EventResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 108; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 14; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "event_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   std::string event_type{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -2972,20 +2808,13 @@ class EventResponse : public ProtoMessage { | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
| }; | ||||
| class ListEntitiesValveResponse : public ProtoMessage { | ||||
| class ListEntitiesValveResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 109; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 60; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_valve_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   std::string device_class{}; | ||||
|   bool assumed_state{false}; | ||||
|   bool supports_position{false}; | ||||
| @@ -3001,14 +2830,13 @@ class ListEntitiesValveResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ValveStateResponse : public ProtoMessage { | ||||
| class ValveStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 110; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 12; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "valve_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   float position{0.0f}; | ||||
|   enums::ValveOperation current_operation{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| @@ -3042,20 +2870,13 @@ class ValveCommandRequest : public ProtoMessage { | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class ListEntitiesDateTimeResponse : public ProtoMessage { | ||||
| class ListEntitiesDateTimeResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 112; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 45; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_date_time_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -3067,14 +2888,13 @@ class ListEntitiesDateTimeResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class DateTimeStateResponse : public ProtoMessage { | ||||
| class DateTimeStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 113; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 12; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "date_time_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   bool missing_state{false}; | ||||
|   uint32_t epoch_seconds{0}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
| @@ -3105,20 +2925,13 @@ class DateTimeCommandRequest : public ProtoMessage { | ||||
|  protected: | ||||
|   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||
| }; | ||||
| class ListEntitiesUpdateResponse : public ProtoMessage { | ||||
| class ListEntitiesUpdateResponse : public InfoResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 116; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 54; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "list_entities_update_response"; } | ||||
| #endif | ||||
|   std::string object_id{}; | ||||
|   uint32_t key{0}; | ||||
|   std::string name{}; | ||||
|   std::string unique_id{}; | ||||
|   std::string icon{}; | ||||
|   bool disabled_by_default{false}; | ||||
|   enums::EntityCategory entity_category{}; | ||||
|   std::string device_class{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(uint32_t &total_size) const override; | ||||
| @@ -3131,14 +2944,13 @@ class ListEntitiesUpdateResponse : public ProtoMessage { | ||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||
| }; | ||||
| class UpdateStateResponse : public ProtoMessage { | ||||
| class UpdateStateResponse : public StateResponseProtoMessage { | ||||
|  public: | ||||
|   static constexpr uint16_t MESSAGE_TYPE = 117; | ||||
|   static constexpr uint16_t ESTIMATED_SIZE = 61; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   static constexpr const char *message_name() { return "update_state_response"; } | ||||
| #endif | ||||
|   uint32_t key{0}; | ||||
|   bool missing_state{false}; | ||||
|   bool in_progress{false}; | ||||
|   bool has_progress{false}; | ||||
|   | ||||
| @@ -620,544 +620,300 @@ void APIServerConnection::on_ping_request(const PingRequest &msg) { | ||||
|   } | ||||
| } | ||||
| void APIServerConnection::on_device_info_request(const DeviceInfoRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   } | ||||
|   DeviceInfoResponse ret = this->device_info(msg); | ||||
|   if (!this->send_message(ret)) { | ||||
|     this->on_fatal_error(); | ||||
|   if (this->check_connection_setup_()) { | ||||
|     DeviceInfoResponse ret = this->device_info(msg); | ||||
|     if (!this->send_message(ret)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| void APIServerConnection::on_list_entities_request(const ListEntitiesRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->list_entities(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->list_entities(msg); | ||||
| } | ||||
| void APIServerConnection::on_subscribe_states_request(const SubscribeStatesRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->subscribe_states(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->subscribe_states(msg); | ||||
| } | ||||
| void APIServerConnection::on_subscribe_logs_request(const SubscribeLogsRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->subscribe_logs(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->subscribe_logs(msg); | ||||
| } | ||||
| void APIServerConnection::on_subscribe_homeassistant_services_request( | ||||
|     const SubscribeHomeassistantServicesRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->subscribe_homeassistant_services(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->subscribe_homeassistant_services(msg); | ||||
| } | ||||
| void APIServerConnection::on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->subscribe_home_assistant_states(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->subscribe_home_assistant_states(msg); | ||||
| } | ||||
| void APIServerConnection::on_get_time_request(const GetTimeRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   } | ||||
|   GetTimeResponse ret = this->get_time(msg); | ||||
|   if (!this->send_message(ret)) { | ||||
|     this->on_fatal_error(); | ||||
|   if (this->check_connection_setup_()) { | ||||
|     GetTimeResponse ret = this->get_time(msg); | ||||
|     if (!this->send_message(ret)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| void APIServerConnection::on_execute_service_request(const ExecuteServiceRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->execute_service(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->execute_service(msg); | ||||
| } | ||||
| #ifdef USE_API_NOISE | ||||
| void APIServerConnection::on_noise_encryption_set_key_request(const NoiseEncryptionSetKeyRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); | ||||
|   if (!this->send_message(ret)) { | ||||
|     this->on_fatal_error(); | ||||
|   if (this->check_authenticated_()) { | ||||
|     NoiseEncryptionSetKeyResponse ret = this->noise_encryption_set_key(msg); | ||||
|     if (!this->send_message(ret)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_BUTTON | ||||
| void APIServerConnection::on_button_command_request(const ButtonCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->button_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->button_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_ESP32_CAMERA | ||||
| void APIServerConnection::on_camera_image_request(const CameraImageRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->camera_image(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->camera_image(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_CLIMATE | ||||
| void APIServerConnection::on_climate_command_request(const ClimateCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->climate_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->climate_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_COVER | ||||
| void APIServerConnection::on_cover_command_request(const CoverCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->cover_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->cover_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_DATETIME_DATE | ||||
| void APIServerConnection::on_date_command_request(const DateCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->date_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->date_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_DATETIME_DATETIME | ||||
| void APIServerConnection::on_date_time_command_request(const DateTimeCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->datetime_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->datetime_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_FAN | ||||
| void APIServerConnection::on_fan_command_request(const FanCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->fan_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->fan_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_LIGHT | ||||
| void APIServerConnection::on_light_command_request(const LightCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->light_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->light_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_LOCK | ||||
| void APIServerConnection::on_lock_command_request(const LockCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->lock_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->lock_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_MEDIA_PLAYER | ||||
| void APIServerConnection::on_media_player_command_request(const MediaPlayerCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->media_player_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->media_player_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_NUMBER | ||||
| void APIServerConnection::on_number_command_request(const NumberCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->number_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->number_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
| void APIServerConnection::on_select_command_request(const SelectCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->select_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->select_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_SIREN | ||||
| void APIServerConnection::on_siren_command_request(const SirenCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->siren_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->siren_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_SWITCH | ||||
| void APIServerConnection::on_switch_command_request(const SwitchCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->switch_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->switch_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_TEXT | ||||
| void APIServerConnection::on_text_command_request(const TextCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->text_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->text_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_DATETIME_TIME | ||||
| void APIServerConnection::on_time_command_request(const TimeCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->time_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->time_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_UPDATE | ||||
| void APIServerConnection::on_update_command_request(const UpdateCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->update_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->update_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_VALVE | ||||
| void APIServerConnection::on_valve_command_request(const ValveCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->valve_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->valve_command(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| void APIServerConnection::on_subscribe_bluetooth_le_advertisements_request( | ||||
|     const SubscribeBluetoothLEAdvertisementsRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->subscribe_bluetooth_le_advertisements(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->subscribe_bluetooth_le_advertisements(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| void APIServerConnection::on_bluetooth_device_request(const BluetoothDeviceRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->bluetooth_device_request(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->bluetooth_device_request(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| void APIServerConnection::on_bluetooth_gatt_get_services_request(const BluetoothGATTGetServicesRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->bluetooth_gatt_get_services(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->bluetooth_gatt_get_services(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| void APIServerConnection::on_bluetooth_gatt_read_request(const BluetoothGATTReadRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->bluetooth_gatt_read(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->bluetooth_gatt_read(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| void APIServerConnection::on_bluetooth_gatt_write_request(const BluetoothGATTWriteRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->bluetooth_gatt_write(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->bluetooth_gatt_write(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| void APIServerConnection::on_bluetooth_gatt_read_descriptor_request(const BluetoothGATTReadDescriptorRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->bluetooth_gatt_read_descriptor(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->bluetooth_gatt_read_descriptor(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| void APIServerConnection::on_bluetooth_gatt_write_descriptor_request(const BluetoothGATTWriteDescriptorRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->bluetooth_gatt_write_descriptor(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->bluetooth_gatt_write_descriptor(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| void APIServerConnection::on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->bluetooth_gatt_notify(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->bluetooth_gatt_notify(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| void APIServerConnection::on_subscribe_bluetooth_connections_free_request( | ||||
|     const SubscribeBluetoothConnectionsFreeRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); | ||||
|   if (!this->send_message(ret)) { | ||||
|     this->on_fatal_error(); | ||||
|   if (this->check_authenticated_()) { | ||||
|     BluetoothConnectionsFreeResponse ret = this->subscribe_bluetooth_connections_free(msg); | ||||
|     if (!this->send_message(ret)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| void APIServerConnection::on_unsubscribe_bluetooth_le_advertisements_request( | ||||
|     const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->unsubscribe_bluetooth_le_advertisements(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->unsubscribe_bluetooth_le_advertisements(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_BLUETOOTH_PROXY | ||||
| void APIServerConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->bluetooth_scanner_set_mode(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->bluetooth_scanner_set_mode(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
| void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->subscribe_voice_assistant(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->subscribe_voice_assistant(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
| void APIServerConnection::on_voice_assistant_configuration_request(const VoiceAssistantConfigurationRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); | ||||
|   if (!this->send_message(ret)) { | ||||
|     this->on_fatal_error(); | ||||
|   if (this->check_authenticated_()) { | ||||
|     VoiceAssistantConfigurationResponse ret = this->voice_assistant_get_configuration(msg); | ||||
|     if (!this->send_message(ret)) { | ||||
|       this->on_fatal_error(); | ||||
|     } | ||||
|   } | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_VOICE_ASSISTANT | ||||
| void APIServerConnection::on_voice_assistant_set_configuration(const VoiceAssistantSetConfiguration &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->voice_assistant_set_configuration(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->voice_assistant_set_configuration(msg); | ||||
| } | ||||
| #endif | ||||
| #ifdef USE_ALARM_CONTROL_PANEL | ||||
| void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { | ||||
|   if (!this->is_connection_setup()) { | ||||
|     this->on_no_setup_connection(); | ||||
|     return; | ||||
|   if (this->check_authenticated_()) { | ||||
|     this->alarm_control_panel_command(msg); | ||||
|   } | ||||
|   if (!this->is_authenticated()) { | ||||
|     this->on_unauthenticated_access(); | ||||
|     return; | ||||
|   } | ||||
|   this->alarm_control_panel_command(msg); | ||||
| } | ||||
| #endif | ||||
|  | ||||
|   | ||||
| @@ -106,7 +106,7 @@ void APIServer::setup() { | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   this->last_connected_ = millis(); | ||||
|   this->last_connected_ = App.get_loop_component_start_time(); | ||||
|  | ||||
| #ifdef USE_ESP32_CAMERA | ||||
|   if (esp32_camera::global_esp32_camera != nullptr && !esp32_camera::global_esp32_camera->is_internal()) { | ||||
| @@ -164,7 +164,7 @@ void APIServer::loop() { | ||||
|   } | ||||
|  | ||||
|   if (this->reboot_timeout_ != 0) { | ||||
|     const uint32_t now = millis(); | ||||
|     const uint32_t now = App.get_loop_component_start_time(); | ||||
|     if (!this->is_connected()) { | ||||
|       if (now - this->last_connected_ > this->reboot_timeout_) { | ||||
|         ESP_LOGE(TAG, "No client connected; rebooting"); | ||||
|   | ||||
| @@ -142,19 +142,27 @@ class APIServer : public Component, public Controller { | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   bool shutting_down_ = false; | ||||
|   // Pointers and pointer-like types first (4 bytes each) | ||||
|   std::unique_ptr<socket::Socket> socket_ = nullptr; | ||||
|   uint16_t port_{6053}; | ||||
|   Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>(); | ||||
|   Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>(); | ||||
|  | ||||
|   // 4-byte aligned types | ||||
|   uint32_t reboot_timeout_{300000}; | ||||
|   uint32_t batch_delay_{100}; | ||||
|   uint32_t last_connected_{0}; | ||||
|  | ||||
|   // Vectors and strings (12 bytes each on 32-bit) | ||||
|   std::vector<std::unique_ptr<APIConnection>> clients_; | ||||
|   std::string password_; | ||||
|   std::vector<uint8_t> shared_write_buffer_;  // Shared proto write buffer for all connections | ||||
|   std::vector<HomeAssistantStateSubscription> state_subs_; | ||||
|   std::vector<UserServiceDescriptor *> user_services_; | ||||
|   Trigger<std::string, std::string> *client_connected_trigger_ = new Trigger<std::string, std::string>(); | ||||
|   Trigger<std::string, std::string> *client_disconnected_trigger_ = new Trigger<std::string, std::string>(); | ||||
|  | ||||
|   // Group smaller types together | ||||
|   uint16_t port_{6053}; | ||||
|   bool shutting_down_ = false; | ||||
|   // 3 bytes used, 1 byte padding | ||||
|  | ||||
| #ifdef USE_API_NOISE | ||||
|   std::shared_ptr<APINoiseContext> noise_ctx_ = std::make_shared<APINoiseContext>(); | ||||
|   | ||||
| @@ -216,7 +216,7 @@ class ProtoWriteBuffer { | ||||
|     this->buffer_->insert(this->buffer_->end(), data, data + len); | ||||
|   } | ||||
|   void encode_string(uint32_t field_id, const std::string &value, bool force = false) { | ||||
|     this->encode_string(field_id, value.data(), value.size()); | ||||
|     this->encode_string(field_id, value.data(), value.size(), force); | ||||
|   } | ||||
|   void encode_bytes(uint32_t field_id, const uint8_t *data, size_t len, bool force = false) { | ||||
|     this->encode_string(field_id, reinterpret_cast<const char *>(data), len, force); | ||||
| @@ -327,9 +327,11 @@ class ProtoWriteBuffer { | ||||
| class ProtoMessage { | ||||
|  public: | ||||
|   virtual ~ProtoMessage() = default; | ||||
|   virtual void encode(ProtoWriteBuffer buffer) const = 0; | ||||
|   // Default implementation for messages with no fields | ||||
|   virtual void encode(ProtoWriteBuffer buffer) const {} | ||||
|   void decode(const uint8_t *buffer, size_t length); | ||||
|   virtual void calculate_size(uint32_t &total_size) const = 0; | ||||
|   // Default implementation for messages with no fields | ||||
|   virtual void calculate_size(uint32_t &total_size) const {} | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
|   std::string dump() const; | ||||
|   virtual void dump_to(std::string &out) const = 0; | ||||
| @@ -377,6 +379,26 @@ class ProtoService { | ||||
|     // Send the buffer | ||||
|     return this->send_buffer(buffer, message_type); | ||||
|   } | ||||
|  | ||||
|   // Authentication helper methods | ||||
|   bool check_connection_setup_() { | ||||
|     if (!this->is_connection_setup()) { | ||||
|       this->on_no_setup_connection(); | ||||
|       return false; | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   bool check_authenticated_() { | ||||
|     if (!this->check_connection_setup_()) { | ||||
|       return false; | ||||
|     } | ||||
|     if (!this->is_authenticated()) { | ||||
|       this->on_unauthenticated_access(); | ||||
|       return false; | ||||
|     } | ||||
|     return true; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| }  // namespace api | ||||
|   | ||||
| @@ -21,8 +21,8 @@ CONFIG_SCHEMA = cv.All( | ||||
| @coroutine_with_priority(200.0) | ||||
| async def to_code(config): | ||||
|     if CORE.is_esp32 or CORE.is_libretiny: | ||||
|         # https://github.com/esphome/AsyncTCP/blob/master/library.json | ||||
|         cg.add_library("esphome/AsyncTCP-esphome", "2.1.4") | ||||
|         # https://github.com/ESP32Async/AsyncTCP | ||||
|         cg.add_library("ESP32Async/AsyncTCP", "3.4.4") | ||||
|     elif CORE.is_esp8266: | ||||
|         # https://github.com/esphome/ESPAsyncTCP | ||||
|         cg.add_library("esphome/ESPAsyncTCP-esphome", "2.0.0") | ||||
|         # https://github.com/ESP32Async/ESPAsyncTCP | ||||
|         cg.add_library("ESP32Async/ESPAsyncTCP", "2.0.0") | ||||
|   | ||||
| @@ -86,7 +86,7 @@ bool AudioTransferBuffer::reallocate(size_t new_buffer_size) { | ||||
| bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) { | ||||
|   this->buffer_size_ = buffer_size; | ||||
|  | ||||
|   RAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||
|   RAMAllocator<uint8_t> allocator; | ||||
|  | ||||
|   this->buffer_ = allocator.allocate(this->buffer_size_); | ||||
|   if (this->buffer_ == nullptr) { | ||||
| @@ -101,7 +101,7 @@ bool AudioTransferBuffer::allocate_buffer_(size_t buffer_size) { | ||||
|  | ||||
| void AudioTransferBuffer::deallocate_buffer_() { | ||||
|   if (this->buffer_ != nullptr) { | ||||
|     RAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||
|     RAMAllocator<uint8_t> allocator; | ||||
|     allocator.deallocate(this->buffer_, this->buffer_size_); | ||||
|     this->buffer_ = nullptr; | ||||
|     this->data_start_ = nullptr; | ||||
|   | ||||
| @@ -480,7 +480,11 @@ void BedJetHub::set_clock(uint8_t hour, uint8_t minute) { | ||||
|  | ||||
| /* Internal */ | ||||
|  | ||||
| void BedJetHub::loop() {} | ||||
| void BedJetHub::loop() { | ||||
|   // Parent BLEClientNode has a loop() method, but this component uses | ||||
|   // polling via update() and BLE callbacks so loop isn't needed | ||||
|   this->disable_loop(); | ||||
| } | ||||
| void BedJetHub::update() { this->dispatch_status_(); } | ||||
|  | ||||
| void BedJetHub::dump_config() { | ||||
|   | ||||
| @@ -83,7 +83,11 @@ void BedJetClimate::reset_state_() { | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| void BedJetClimate::loop() {} | ||||
| void BedJetClimate::loop() { | ||||
|   // This component is controlled via the parent BedJetHub | ||||
|   // Empty loop not needed, disable to save CPU cycles | ||||
|   this->disable_loop(); | ||||
| } | ||||
|  | ||||
| void BedJetClimate::control(const ClimateCall &call) { | ||||
|   ESP_LOGD(TAG, "Received BedJetClimate::control"); | ||||
|   | ||||
| @@ -11,7 +11,11 @@ namespace ble_client { | ||||
|  | ||||
| static const char *const TAG = "ble_rssi_sensor"; | ||||
|  | ||||
| void BLEClientRSSISensor::loop() {} | ||||
| void BLEClientRSSISensor::loop() { | ||||
|   // Parent BLEClientNode has a loop() method, but this component uses | ||||
|   // polling via update() and BLE GAP callbacks so loop isn't needed | ||||
|   this->disable_loop(); | ||||
| } | ||||
|  | ||||
| void BLEClientRSSISensor::dump_config() { | ||||
|   LOG_SENSOR("", "BLE Client RSSI Sensor", this); | ||||
|   | ||||
| @@ -11,7 +11,11 @@ namespace ble_client { | ||||
|  | ||||
| static const char *const TAG = "ble_sensor"; | ||||
|  | ||||
| void BLESensor::loop() {} | ||||
| void BLESensor::loop() { | ||||
|   // Parent BLEClientNode has a loop() method, but this component uses | ||||
|   // polling via update() and BLE callbacks so loop isn't needed | ||||
|   this->disable_loop(); | ||||
| } | ||||
|  | ||||
| void BLESensor::dump_config() { | ||||
|   LOG_SENSOR("", "BLE Sensor", this); | ||||
|   | ||||
| @@ -14,7 +14,11 @@ static const char *const TAG = "ble_text_sensor"; | ||||
|  | ||||
| static const std::string EMPTY = ""; | ||||
|  | ||||
| void BLETextSensor::loop() {} | ||||
| void BLETextSensor::loop() { | ||||
|   // Parent BLEClientNode has a loop() method, but this component uses | ||||
|   // polling via update() and BLE callbacks so loop isn't needed | ||||
|   this->disable_loop(); | ||||
| } | ||||
|  | ||||
| void BLETextSensor::dump_config() { | ||||
|   LOG_TEXT_SENSOR("", "BLE Text Sensor", this); | ||||
|   | ||||
| @@ -26,10 +26,17 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase { | ||||
|  | ||||
|  protected: | ||||
|   friend class BluetoothProxy; | ||||
|   bool seen_mtu_or_services_{false}; | ||||
|  | ||||
|   int16_t send_service_{-2}; | ||||
|   // Memory optimized layout for 32-bit systems | ||||
|   // Group 1: Pointers (4 bytes each, naturally aligned) | ||||
|   BluetoothProxy *proxy_; | ||||
|  | ||||
|   // Group 2: 2-byte types | ||||
|   int16_t send_service_{-2};  // Needs to handle negative values and service count | ||||
|  | ||||
|   // Group 3: 1-byte types | ||||
|   bool seen_mtu_or_services_{false}; | ||||
|   // 1 byte used, 1 byte padding | ||||
| }; | ||||
|  | ||||
| }  // namespace bluetooth_proxy | ||||
|   | ||||
| @@ -58,7 +58,7 @@ static std::vector<api::BluetoothLERawAdvertisement> &get_batch_buffer() { | ||||
|   return batch_buffer; | ||||
| } | ||||
|  | ||||
| bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { | ||||
| bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) { | ||||
|   if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) | ||||
|     return false; | ||||
|  | ||||
| @@ -73,7 +73,7 @@ bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_p | ||||
|  | ||||
|   // Add new advertisements to the batch buffer | ||||
|   for (size_t i = 0; i < count; i++) { | ||||
|     auto &result = advertisements[i]; | ||||
|     auto &result = scan_results[i]; | ||||
|     uint8_t length = result.adv_data_len + result.scan_rsp_len; | ||||
|  | ||||
|     batch_buffer.emplace_back(); | ||||
|   | ||||
| @@ -52,7 +52,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | ||||
|  public: | ||||
|   BluetoothProxy(); | ||||
|   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; | ||||
|   bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override; | ||||
|   bool parse_devices(const esp32_ble::BLEScanResult *scan_results, size_t count) override; | ||||
|   void dump_config() override; | ||||
|   void setup() override; | ||||
|   void loop() override; | ||||
| @@ -134,11 +134,17 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | ||||
|  | ||||
|   BluetoothConnection *get_connection_(uint64_t address, bool reserve); | ||||
|  | ||||
|   bool active_; | ||||
|  | ||||
|   std::vector<BluetoothConnection *> connections_{}; | ||||
|   // Memory optimized layout for 32-bit systems | ||||
|   // Group 1: Pointers (4 bytes each, naturally aligned) | ||||
|   api::APIConnection *api_connection_{nullptr}; | ||||
|  | ||||
|   // Group 2: Container types (typically 12 bytes on 32-bit) | ||||
|   std::vector<BluetoothConnection *> connections_{}; | ||||
|  | ||||
|   // Group 3: 1-byte types grouped together | ||||
|   bool active_; | ||||
|   bool raw_advertisements_{false}; | ||||
|   // 2 bytes used, 2 bytes padding | ||||
| }; | ||||
|  | ||||
| extern BluetoothProxy *global_bluetooth_proxy;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   | ||||
| @@ -93,9 +93,8 @@ void BME280Component::setup() { | ||||
|  | ||||
|   // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries | ||||
|   // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. | ||||
|   if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { | ||||
|     this->component_state_ &= ~COMPONENT_STATE_MASK; | ||||
|     this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; | ||||
|   if (this->is_failed()) { | ||||
|     this->reset_to_construction_state(); | ||||
|   } | ||||
|  | ||||
|   if (!this->read_byte(BME280_REGISTER_CHIPID, &chip_id)) { | ||||
|   | ||||
| @@ -12,8 +12,8 @@ from esphome.const import ( | ||||
|     CONF_OVERSAMPLING, | ||||
|     CONF_PRESSURE, | ||||
|     CONF_TEMPERATURE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_ATMOSPHERIC_PRESSURE, | ||||
|     DEVICE_CLASS_HUMIDITY, | ||||
|     DEVICE_CLASS_TEMPERATURE, | ||||
|     ICON_GAS_CYLINDER, | ||||
|     STATE_CLASS_MEASUREMENT, | ||||
|   | ||||
| @@ -1,4 +1,5 @@ | ||||
| import re | ||||
|  | ||||
| from esphome import automation | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
|   | ||||
| @@ -41,6 +41,7 @@ async def to_code(config): | ||||
|  | ||||
|     if CORE.using_arduino: | ||||
|         if CORE.is_esp32: | ||||
|             cg.add_library("ESP32 Async UDP", None) | ||||
|             cg.add_library("DNSServer", None) | ||||
|             cg.add_library("WiFi", None) | ||||
|         if CORE.is_esp8266: | ||||
|   | ||||
| @@ -37,7 +37,12 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) { | ||||
|   request->redirect("/?save"); | ||||
| } | ||||
|  | ||||
| void CaptivePortal::setup() {} | ||||
| void CaptivePortal::setup() { | ||||
| #ifndef USE_ARDUINO | ||||
|   // No DNS server needed for non-Arduino frameworks | ||||
|   this->disable_loop(); | ||||
| #endif | ||||
| } | ||||
| void CaptivePortal::start() { | ||||
|   this->base_->init(); | ||||
|   if (!this->initialized_) { | ||||
| @@ -50,6 +55,8 @@ void CaptivePortal::start() { | ||||
|   this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError); | ||||
|   network::IPAddress ip = wifi::global_wifi_component->wifi_soft_ap_ip(); | ||||
|   this->dns_server_->start(53, "*", ip); | ||||
|   // Re-enable loop() when DNS server is started | ||||
|   this->enable_loop(); | ||||
| #endif | ||||
|  | ||||
|   this->base_->get_server()->onNotFound([this](AsyncWebServerRequest *req) { | ||||
| @@ -68,7 +75,11 @@ void CaptivePortal::start() { | ||||
|  | ||||
| void CaptivePortal::handleRequest(AsyncWebServerRequest *req) { | ||||
|   if (req->url() == "/") { | ||||
| #ifndef USE_ESP8266 | ||||
|     auto *response = req->beginResponse(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); | ||||
| #else | ||||
|     auto *response = req->beginResponse_P(200, "text/html", INDEX_GZ, sizeof(INDEX_GZ)); | ||||
| #endif | ||||
|     response->addHeader("Content-Encoding", "gzip"); | ||||
|     req->send(response); | ||||
|     return; | ||||
|   | ||||
| @@ -21,8 +21,11 @@ class CaptivePortal : public AsyncWebHandler, public Component { | ||||
|   void dump_config() override; | ||||
| #ifdef USE_ARDUINO | ||||
|   void loop() override { | ||||
|     if (this->dns_server_ != nullptr) | ||||
|     if (this->dns_server_ != nullptr) { | ||||
|       this->dns_server_->processNextRequest(); | ||||
|     } else { | ||||
|       this->disable_loop(); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|   float get_setup_priority() const override; | ||||
| @@ -37,7 +40,7 @@ class CaptivePortal : public AsyncWebHandler, public Component { | ||||
| #endif | ||||
|   } | ||||
|  | ||||
|   bool canHandle(AsyncWebServerRequest *request) override { | ||||
|   bool canHandle(AsyncWebServerRequest *request) const override { | ||||
|     if (!this->active_) | ||||
|       return false; | ||||
|  | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| """CM1106 Sensor component for ESPHome.""" | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| import esphome.config_validation as cv | ||||
| from esphome import automation | ||||
| from esphome.automation import maybe_simple_id | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import sensor, uart | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_CO2, | ||||
|     CONF_ID, | ||||
|   | ||||
| @@ -11,25 +11,25 @@ static const char *const TAG = "datetime.date_entity"; | ||||
|  | ||||
| void DateEntity::publish_state() { | ||||
|   if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     return; | ||||
|   } | ||||
|   if (this->year_ < 1970 || this->year_ > 3000) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Year must be between 1970 and 3000"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->month_ < 1 || this->month_ > 12) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Month must be between 1 and 12"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->day_ > days_in_month(this->month_, this->year_)) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); | ||||
|     return; | ||||
|   } | ||||
|   this->has_state_ = true; | ||||
|   this->set_has_state(true); | ||||
|   ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); | ||||
|   this->state_callback_.call(); | ||||
| } | ||||
|   | ||||
| @@ -13,9 +13,6 @@ namespace datetime { | ||||
|  | ||||
| class DateTimeBase : public EntityBase { | ||||
|  public: | ||||
|   /// Return whether this Datetime has gotten a full state yet. | ||||
|   bool has_state() const { return this->has_state_; } | ||||
|  | ||||
|   virtual ESPTime state_as_esptime() const = 0; | ||||
|  | ||||
|   void add_on_state_callback(std::function<void()> &&callback) { this->state_callback_.add(std::move(callback)); } | ||||
| @@ -31,8 +28,6 @@ class DateTimeBase : public EntityBase { | ||||
| #ifdef USE_TIME | ||||
|   time::RealTimeClock *rtc_; | ||||
| #endif | ||||
|  | ||||
|   bool has_state_{false}; | ||||
| }; | ||||
|  | ||||
| #ifdef USE_TIME | ||||
|   | ||||
| @@ -11,40 +11,40 @@ static const char *const TAG = "datetime.datetime_entity"; | ||||
|  | ||||
| void DateTimeEntity::publish_state() { | ||||
|   if (this->year_ == 0 || this->month_ == 0 || this->day_ == 0) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     return; | ||||
|   } | ||||
|   if (this->year_ < 1970 || this->year_ > 3000) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Year must be between 1970 and 3000"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->month_ < 1 || this->month_ > 12) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Month must be between 1 and 12"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->day_ > days_in_month(this->month_, this->year_)) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Day must be between 1 and %d for month %d", days_in_month(this->month_, this->year_), this->month_); | ||||
|     return; | ||||
|   } | ||||
|   if (this->hour_ > 23) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Hour must be between 0 and 23"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->minute_ > 59) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Minute must be between 0 and 59"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->second_ > 59) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Second must be between 0 and 59"); | ||||
|     return; | ||||
|   } | ||||
|   this->has_state_ = true; | ||||
|   this->set_has_state(true); | ||||
|   ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, | ||||
|            this->month_, this->day_, this->hour_, this->minute_, this->second_); | ||||
|   this->state_callback_.call(); | ||||
|   | ||||
| @@ -11,21 +11,21 @@ static const char *const TAG = "datetime.time_entity"; | ||||
|  | ||||
| void TimeEntity::publish_state() { | ||||
|   if (this->hour_ > 23) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Hour must be between 0 and 23"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->minute_ > 59) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Minute must be between 0 and 59"); | ||||
|     return; | ||||
|   } | ||||
|   if (this->second_ > 59) { | ||||
|     this->has_state_ = false; | ||||
|     this->set_has_state(false); | ||||
|     ESP_LOGE(TAG, "Second must be between 0 and 59"); | ||||
|     return; | ||||
|   } | ||||
|   this->has_state_ = true; | ||||
|   this->set_has_state(true); | ||||
|   ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, | ||||
|            this->second_); | ||||
|   this->state_callback_.call(); | ||||
|   | ||||
| @@ -11,7 +11,7 @@ namespace display { | ||||
| static const char *const TAG = "display"; | ||||
|  | ||||
| void DisplayBuffer::init_internal_(uint32_t buffer_length) { | ||||
|   ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||
|   RAMAllocator<uint8_t> allocator; | ||||
|   this->buffer_ = allocator.allocate(buffer_length); | ||||
|   if (this->buffer_ == nullptr) { | ||||
|     ESP_LOGE(TAG, "Could not allocate buffer for display!"); | ||||
|   | ||||
| @@ -94,6 +94,13 @@ COMPILER_OPTIMIZATIONS = { | ||||
|     "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE", | ||||
| } | ||||
|  | ||||
| ARDUINO_ALLOWED_VARIANTS = [ | ||||
|     VARIANT_ESP32, | ||||
|     VARIANT_ESP32C3, | ||||
|     VARIANT_ESP32S2, | ||||
|     VARIANT_ESP32S3, | ||||
| ] | ||||
|  | ||||
|  | ||||
| def get_cpu_frequencies(*frequencies): | ||||
|     return [str(x) + "MHZ" for x in frequencies] | ||||
| @@ -125,6 +132,8 @@ def set_core_data(config): | ||||
|         choices = CPU_FREQUENCIES[variant] | ||||
|         if "160MHZ" in choices: | ||||
|             cpu_frequency = "160MHZ" | ||||
|         elif "360MHZ" in choices: | ||||
|             cpu_frequency = "360MHZ" | ||||
|         else: | ||||
|             cpu_frequency = choices[-1] | ||||
|         config[CONF_CPU_FREQUENCY] = cpu_frequency | ||||
| @@ -143,12 +152,17 @@ def set_core_data(config): | ||||
|         CORE.data[KEY_ESP32][KEY_COMPONENTS] = {} | ||||
|     elif conf[CONF_TYPE] == FRAMEWORK_ARDUINO: | ||||
|         CORE.data[KEY_CORE][KEY_TARGET_FRAMEWORK] = "arduino" | ||||
|         if variant not in ARDUINO_ALLOWED_VARIANTS: | ||||
|             raise cv.Invalid( | ||||
|                 f"ESPHome does not support using the Arduino framework for the {variant}. Please use the ESP-IDF framework instead.", | ||||
|                 path=[CONF_FRAMEWORK, CONF_TYPE], | ||||
|             ) | ||||
|     CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] = cv.Version.parse( | ||||
|         config[CONF_FRAMEWORK][CONF_VERSION] | ||||
|     ) | ||||
|  | ||||
|     CORE.data[KEY_ESP32][KEY_BOARD] = config[CONF_BOARD] | ||||
|     CORE.data[KEY_ESP32][KEY_VARIANT] = config[CONF_VARIANT] | ||||
|     CORE.data[KEY_ESP32][KEY_VARIANT] = variant | ||||
|     CORE.data[KEY_ESP32][KEY_EXTRA_BUILD_FILES] = {} | ||||
|  | ||||
|     return config | ||||
| @@ -277,11 +291,8 @@ def add_extra_build_file(filename: str, path: str) -> bool: | ||||
|  | ||||
| def _format_framework_arduino_version(ver: cv.Version) -> str: | ||||
|     # format the given arduino (https://github.com/espressif/arduino-esp32/releases) version to | ||||
|     # a PIO platformio/framework-arduinoespressif32 value | ||||
|     # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32 | ||||
|     if ver <= cv.Version(1, 0, 3): | ||||
|         return f"~2.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" | ||||
|     return f"~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0" | ||||
|     # a PIO pioarduino/framework-arduinoespressif32 value | ||||
|     return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip" | ||||
|  | ||||
|  | ||||
| def _format_framework_espidf_version( | ||||
| @@ -305,12 +316,10 @@ def _format_framework_espidf_version( | ||||
|  | ||||
| # The default/recommended arduino framework version | ||||
| #  - https://github.com/espressif/arduino-esp32/releases | ||||
| #  - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-arduinoespressif32 | ||||
| RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(2, 0, 5) | ||||
| # The platformio/espressif32 version to use for arduino frameworks | ||||
| #  - https://github.com/platformio/platform-espressif32/releases | ||||
| #  - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32 | ||||
| ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0) | ||||
| RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 1, 3) | ||||
| # The platform-espressif32 version to use for arduino frameworks | ||||
| #  - https://github.com/pioarduino/platform-espressif32/releases | ||||
| ARDUINO_PLATFORM_VERSION = cv.Version(53, 3, 13) | ||||
|  | ||||
| # The default/recommended esp-idf framework version | ||||
| #  - https://github.com/espressif/esp-idf/releases | ||||
| @@ -353,8 +362,8 @@ SUPPORTED_PIOARDUINO_ESP_IDF_5X = [ | ||||
| def _arduino_check_versions(value): | ||||
|     value = value.copy() | ||||
|     lookups = { | ||||
|         "dev": (cv.Version(2, 1, 0), "https://github.com/espressif/arduino-esp32.git"), | ||||
|         "latest": (cv.Version(2, 0, 9), None), | ||||
|         "dev": (cv.Version(3, 1, 3), "https://github.com/espressif/arduino-esp32.git"), | ||||
|         "latest": (cv.Version(3, 1, 3), None), | ||||
|         "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None), | ||||
|     } | ||||
|  | ||||
| @@ -376,6 +385,10 @@ def _arduino_check_versions(value): | ||||
|         CONF_PLATFORM_VERSION, _parse_platform_version(str(ARDUINO_PLATFORM_VERSION)) | ||||
|     ) | ||||
|  | ||||
|     if value[CONF_SOURCE].startswith("http"): | ||||
|         # prefix is necessary or platformio will complain with a cryptic error | ||||
|         value[CONF_SOURCE] = f"framework-arduinoespressif32@{value[CONF_SOURCE]}" | ||||
|  | ||||
|     if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION: | ||||
|         _LOGGER.warning( | ||||
|             "The selected Arduino framework version is not the recommended one. " | ||||
| @@ -618,6 +631,21 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All( | ||||
| ) | ||||
|  | ||||
|  | ||||
| def _set_default_framework(config): | ||||
|     if CONF_FRAMEWORK not in config: | ||||
|         config = config.copy() | ||||
|  | ||||
|         variant = config[CONF_VARIANT] | ||||
|         if variant in ARDUINO_ALLOWED_VARIANTS: | ||||
|             config[CONF_FRAMEWORK] = ARDUINO_FRAMEWORK_SCHEMA({}) | ||||
|             config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ARDUINO | ||||
|         else: | ||||
|             config[CONF_FRAMEWORK] = ESP_IDF_FRAMEWORK_SCHEMA({}) | ||||
|             config[CONF_FRAMEWORK][CONF_TYPE] = FRAMEWORK_ESP_IDF | ||||
|  | ||||
|     return config | ||||
|  | ||||
|  | ||||
| FRAMEWORK_ESP_IDF = "esp-idf" | ||||
| FRAMEWORK_ARDUINO = "arduino" | ||||
| FRAMEWORK_SCHEMA = cv.typed_schema( | ||||
| @@ -627,7 +655,6 @@ FRAMEWORK_SCHEMA = cv.typed_schema( | ||||
|     }, | ||||
|     lower=True, | ||||
|     space="-", | ||||
|     default_type=FRAMEWORK_ARDUINO, | ||||
| ) | ||||
|  | ||||
|  | ||||
| @@ -654,10 +681,11 @@ CONFIG_SCHEMA = cv.All( | ||||
|             ), | ||||
|             cv.Optional(CONF_PARTITIONS): cv.file_, | ||||
|             cv.Optional(CONF_VARIANT): cv.one_of(*VARIANTS, upper=True), | ||||
|             cv.Optional(CONF_FRAMEWORK, default={}): FRAMEWORK_SCHEMA, | ||||
|             cv.Optional(CONF_FRAMEWORK): FRAMEWORK_SCHEMA, | ||||
|         } | ||||
|     ), | ||||
|     _detect_variant, | ||||
|     _set_default_framework, | ||||
|     set_core_data, | ||||
| ) | ||||
|  | ||||
| @@ -668,6 +696,7 @@ FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate) | ||||
| async def to_code(config): | ||||
|     cg.add_platformio_option("board", config[CONF_BOARD]) | ||||
|     cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE]) | ||||
|     cg.set_cpp_standard("gnu++17") | ||||
|     cg.add_build_flag("-DUSE_ESP32") | ||||
|     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) | ||||
|     cg.add_build_flag(f"-DUSE_ESP32_VARIANT_{config[CONF_VARIANT]}") | ||||
| @@ -801,10 +830,7 @@ async def to_code(config): | ||||
|         cg.add_platformio_option("framework", "arduino") | ||||
|         cg.add_build_flag("-DUSE_ARDUINO") | ||||
|         cg.add_build_flag("-DUSE_ESP32_FRAMEWORK_ARDUINO") | ||||
|         cg.add_platformio_option( | ||||
|             "platform_packages", | ||||
|             [f"platformio/framework-arduinoespressif32@{conf[CONF_SOURCE]}"], | ||||
|         ) | ||||
|         cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]]) | ||||
|  | ||||
|         if CONF_PARTITIONS in config: | ||||
|             cg.add_platformio_option("board_build.partitions", config[CONF_PARTITIONS]) | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "ble.h" | ||||
| #include "ble_event_pool.h" | ||||
|  | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/log.h" | ||||
| @@ -23,9 +24,6 @@ namespace esp32_ble { | ||||
|  | ||||
| static const char *const TAG = "esp32_ble"; | ||||
|  | ||||
| static RAMAllocator<BLEEvent> EVENT_ALLOCATOR(  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|     RAMAllocator<BLEEvent>::ALLOW_FAILURE | RAMAllocator<BLEEvent>::ALLOC_INTERNAL); | ||||
|  | ||||
| void ESP32BLE::setup() { | ||||
|   global_ble = this; | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
| @@ -304,82 +302,191 @@ void ESP32BLE::loop() { | ||||
|   BLEEvent *ble_event = this->ble_events_.pop(); | ||||
|   while (ble_event != nullptr) { | ||||
|     switch (ble_event->type_) { | ||||
|       case BLEEvent::GATTS: | ||||
|         this->real_gatts_event_handler_(ble_event->event_.gatts.gatts_event, ble_event->event_.gatts.gatts_if, | ||||
|                                         &ble_event->event_.gatts.gatts_param); | ||||
|       case BLEEvent::GATTS: { | ||||
|         esp_gatts_cb_event_t event = ble_event->event_.gatts.gatts_event; | ||||
|         esp_gatt_if_t gatts_if = ble_event->event_.gatts.gatts_if; | ||||
|         esp_ble_gatts_cb_param_t *param = ble_event->event_.gatts.gatts_param; | ||||
|         ESP_LOGV(TAG, "gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); | ||||
|         for (auto *gatts_handler : this->gatts_event_handlers_) { | ||||
|           gatts_handler->gatts_event_handler(event, gatts_if, param); | ||||
|         } | ||||
|         break; | ||||
|       case BLEEvent::GATTC: | ||||
|         this->real_gattc_event_handler_(ble_event->event_.gattc.gattc_event, ble_event->event_.gattc.gattc_if, | ||||
|                                         &ble_event->event_.gattc.gattc_param); | ||||
|       } | ||||
|       case BLEEvent::GATTC: { | ||||
|         esp_gattc_cb_event_t event = ble_event->event_.gattc.gattc_event; | ||||
|         esp_gatt_if_t gattc_if = ble_event->event_.gattc.gattc_if; | ||||
|         esp_ble_gattc_cb_param_t *param = ble_event->event_.gattc.gattc_param; | ||||
|         ESP_LOGV(TAG, "gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); | ||||
|         for (auto *gattc_handler : this->gattc_event_handlers_) { | ||||
|           gattc_handler->gattc_event_handler(event, gattc_if, param); | ||||
|         } | ||||
|         break; | ||||
|       case BLEEvent::GAP: | ||||
|         this->real_gap_event_handler_(ble_event->event_.gap.gap_event, &ble_event->event_.gap.gap_param); | ||||
|       } | ||||
|       case BLEEvent::GAP: { | ||||
|         esp_gap_ble_cb_event_t gap_event = ble_event->event_.gap.gap_event; | ||||
|         switch (gap_event) { | ||||
|           case ESP_GAP_BLE_SCAN_RESULT_EVT: | ||||
|             // Use the new scan event handler - no memcpy! | ||||
|             for (auto *scan_handler : this->gap_scan_event_handlers_) { | ||||
|               scan_handler->gap_scan_event_handler(ble_event->scan_result()); | ||||
|             } | ||||
|             break; | ||||
|  | ||||
|           // Scan complete events | ||||
|           case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: | ||||
|           case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: | ||||
|           case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: | ||||
|             // All three scan complete events have the same structure with just status | ||||
|             // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe | ||||
|             // This is verified at compile-time by static_assert checks in ble_event.h | ||||
|             // The struct already contains our copy of the status (copied in BLEEvent constructor) | ||||
|             ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); | ||||
|             for (auto *gap_handler : this->gap_event_handlers_) { | ||||
|               gap_handler->gap_event_handler( | ||||
|                   gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete)); | ||||
|             } | ||||
|             break; | ||||
|  | ||||
|           // Advertising complete events | ||||
|           case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: | ||||
|           case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: | ||||
|           case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: | ||||
|           case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: | ||||
|           case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: | ||||
|             // All advertising complete events have the same structure with just status | ||||
|             ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); | ||||
|             for (auto *gap_handler : this->gap_event_handlers_) { | ||||
|               gap_handler->gap_event_handler( | ||||
|                   gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete)); | ||||
|             } | ||||
|             break; | ||||
|  | ||||
|           // RSSI complete event | ||||
|           case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: | ||||
|             ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); | ||||
|             for (auto *gap_handler : this->gap_event_handlers_) { | ||||
|               gap_handler->gap_event_handler( | ||||
|                   gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete)); | ||||
|             } | ||||
|             break; | ||||
|  | ||||
|           // Security events | ||||
|           case ESP_GAP_BLE_AUTH_CMPL_EVT: | ||||
|           case ESP_GAP_BLE_SEC_REQ_EVT: | ||||
|           case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: | ||||
|           case ESP_GAP_BLE_PASSKEY_REQ_EVT: | ||||
|           case ESP_GAP_BLE_NC_REQ_EVT: | ||||
|             ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); | ||||
|             for (auto *gap_handler : this->gap_event_handlers_) { | ||||
|               gap_handler->gap_event_handler( | ||||
|                   gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security)); | ||||
|             } | ||||
|             break; | ||||
|  | ||||
|           default: | ||||
|             // Unknown/unhandled event | ||||
|             ESP_LOGW(TAG, "Unhandled GAP event type in loop: %d", gap_event); | ||||
|             break; | ||||
|         } | ||||
|         break; | ||||
|       } | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
|     ble_event->~BLEEvent(); | ||||
|     EVENT_ALLOCATOR.deallocate(ble_event, 1); | ||||
|     // Return the event to the pool | ||||
|     this->ble_event_pool_.release(ble_event); | ||||
|     ble_event = this->ble_events_.pop(); | ||||
|   } | ||||
|   if (this->advertising_ != nullptr) { | ||||
|     this->advertising_->loop(); | ||||
|   } | ||||
|  | ||||
|   // Log dropped events periodically | ||||
|   uint16_t dropped = this->ble_events_.get_and_reset_dropped_count(); | ||||
|   if (dropped > 0) { | ||||
|     ESP_LOGW(TAG, "Dropped %u BLE events due to buffer overflow", dropped); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { | ||||
|   BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); | ||||
|   if (new_event == nullptr) { | ||||
|     // Memory too fragmented to allocate new event. Can only drop it until memory comes back | ||||
| // Helper function to load new event data based on type | ||||
| void load_ble_event(BLEEvent *event, esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { | ||||
|   event->load_gap_event(e, p); | ||||
| } | ||||
|  | ||||
| void load_ble_event(BLEEvent *event, esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { | ||||
|   event->load_gattc_event(e, i, p); | ||||
| } | ||||
|  | ||||
| void load_ble_event(BLEEvent *event, esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { | ||||
|   event->load_gatts_event(e, i, p); | ||||
| } | ||||
|  | ||||
| template<typename... Args> void enqueue_ble_event(Args... args) { | ||||
|   // Allocate an event from the pool | ||||
|   BLEEvent *event = global_ble->ble_event_pool_.allocate(); | ||||
|   if (event == nullptr) { | ||||
|     // No events available - queue is full or we're out of memory | ||||
|     global_ble->ble_events_.increment_dropped_count(); | ||||
|     return; | ||||
|   } | ||||
|   new (new_event) BLEEvent(event, param); | ||||
|   global_ble->ble_events_.push(new_event); | ||||
| }  // NOLINT(clang-analyzer-unix.Malloc) | ||||
|  | ||||
| void ESP32BLE::real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { | ||||
|   ESP_LOGV(TAG, "(BLE) gap_event_handler - %d", event); | ||||
|   for (auto *gap_handler : this->gap_event_handlers_) { | ||||
|     gap_handler->gap_event_handler(event, param); | ||||
|   // Load new event data (replaces previous event) | ||||
|   load_ble_event(event, args...); | ||||
|  | ||||
|   // Push the event to the queue | ||||
|   global_ble->ble_events_.push(event); | ||||
|   // Push always succeeds because we're the only producer and the pool ensures we never exceed queue size | ||||
| } | ||||
|  | ||||
| // Explicit template instantiations for the friend function | ||||
| template void enqueue_ble_event(esp_gap_ble_cb_event_t, esp_ble_gap_cb_param_t *); | ||||
| template void enqueue_ble_event(esp_gatts_cb_event_t, esp_gatt_if_t, esp_ble_gatts_cb_param_t *); | ||||
| template void enqueue_ble_event(esp_gattc_cb_event_t, esp_gatt_if_t, esp_ble_gattc_cb_param_t *); | ||||
|  | ||||
| void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { | ||||
|   switch (event) { | ||||
|     // Queue GAP events that components need to handle | ||||
|     // Scanning events - used by esp32_ble_tracker | ||||
|     case ESP_GAP_BLE_SCAN_RESULT_EVT: | ||||
|     case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: | ||||
|     case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: | ||||
|     case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: | ||||
|     // Advertising events - used by esp32_ble_beacon and esp32_ble server | ||||
|     case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: | ||||
|     case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: | ||||
|     case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: | ||||
|     case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: | ||||
|     case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: | ||||
|     // Connection events - used by ble_client | ||||
|     case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: | ||||
|     // Security events - used by ble_client and bluetooth_proxy | ||||
|     case ESP_GAP_BLE_AUTH_CMPL_EVT: | ||||
|     case ESP_GAP_BLE_SEC_REQ_EVT: | ||||
|     case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: | ||||
|     case ESP_GAP_BLE_PASSKEY_REQ_EVT: | ||||
|     case ESP_GAP_BLE_NC_REQ_EVT: | ||||
|       enqueue_ble_event(event, param); | ||||
|       return; | ||||
|  | ||||
|     // Ignore these GAP events as they are not relevant for our use case | ||||
|     case ESP_GAP_BLE_UPDATE_CONN_PARAMS_EVT: | ||||
|     case ESP_GAP_BLE_SET_PKT_LENGTH_COMPLETE_EVT: | ||||
|       return; | ||||
|  | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
|   ESP_LOGW(TAG, "Ignoring unexpected GAP event type: %d", event); | ||||
| } | ||||
|  | ||||
| void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, | ||||
|                                    esp_ble_gatts_cb_param_t *param) { | ||||
|   BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); | ||||
|   if (new_event == nullptr) { | ||||
|     // Memory too fragmented to allocate new event. Can only drop it until memory comes back | ||||
|     return; | ||||
|   } | ||||
|   new (new_event) BLEEvent(event, gatts_if, param); | ||||
|   global_ble->ble_events_.push(new_event); | ||||
| }  // NOLINT(clang-analyzer-unix.Malloc) | ||||
|  | ||||
| void ESP32BLE::real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, | ||||
|                                          esp_ble_gatts_cb_param_t *param) { | ||||
|   ESP_LOGV(TAG, "(BLE) gatts_event [esp_gatt_if: %d] - %d", gatts_if, event); | ||||
|   for (auto *gatts_handler : this->gatts_event_handlers_) { | ||||
|     gatts_handler->gatts_event_handler(event, gatts_if, param); | ||||
|   } | ||||
|   enqueue_ble_event(event, gatts_if, param); | ||||
| } | ||||
|  | ||||
| void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                                    esp_ble_gattc_cb_param_t *param) { | ||||
|   BLEEvent *new_event = EVENT_ALLOCATOR.allocate(1); | ||||
|   if (new_event == nullptr) { | ||||
|     // Memory too fragmented to allocate new event. Can only drop it until memory comes back | ||||
|     return; | ||||
|   } | ||||
|   new (new_event) BLEEvent(event, gattc_if, param); | ||||
|   global_ble->ble_events_.push(new_event); | ||||
| }  // NOLINT(clang-analyzer-unix.Malloc) | ||||
|  | ||||
| void ESP32BLE::real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                                          esp_ble_gattc_cb_param_t *param) { | ||||
|   ESP_LOGV(TAG, "(BLE) gattc_event [esp_gatt_if: %d] - %d", gattc_if, event); | ||||
|   for (auto *gattc_handler : this->gattc_event_handlers_) { | ||||
|     gattc_handler->gattc_event_handler(event, gattc_if, param); | ||||
|   } | ||||
|   enqueue_ble_event(event, gattc_if, param); | ||||
| } | ||||
|  | ||||
| float ESP32BLE::get_setup_priority() const { return setup_priority::BLUETOOTH; } | ||||
|   | ||||
| @@ -2,6 +2,7 @@ | ||||
|  | ||||
| #include "ble_advertising.h" | ||||
| #include "ble_uuid.h" | ||||
| #include "ble_scan_result.h" | ||||
|  | ||||
| #include <functional> | ||||
|  | ||||
| @@ -11,6 +12,7 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #include "ble_event.h" | ||||
| #include "ble_event_pool.h" | ||||
| #include "queue.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
| @@ -22,6 +24,16 @@ | ||||
| namespace esphome { | ||||
| namespace esp32_ble { | ||||
|  | ||||
| // Maximum number of BLE scan results to buffer | ||||
| #ifdef USE_PSRAM | ||||
| static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 32; | ||||
| #else | ||||
| static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 20; | ||||
| #endif | ||||
|  | ||||
| // Maximum size of the BLE event queue - must be power of 2 for lock-free queue | ||||
| static constexpr size_t MAX_BLE_QUEUE_SIZE = 64; | ||||
|  | ||||
| uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); | ||||
|  | ||||
| // NOLINTNEXTLINE(modernize-use-using) | ||||
| @@ -57,6 +69,11 @@ class GAPEventHandler { | ||||
|   virtual void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) = 0; | ||||
| }; | ||||
|  | ||||
| class GAPScanEventHandler { | ||||
|  public: | ||||
|   virtual void gap_scan_event_handler(const BLEScanResult &scan_result) = 0; | ||||
| }; | ||||
|  | ||||
| class GATTcEventHandler { | ||||
|  public: | ||||
|   virtual void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
| @@ -101,6 +118,9 @@ class ESP32BLE : public Component { | ||||
|   void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback); | ||||
|  | ||||
|   void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); } | ||||
|   void register_gap_scan_event_handler(GAPScanEventHandler *handler) { | ||||
|     this->gap_scan_event_handlers_.push_back(handler); | ||||
|   } | ||||
|   void register_gattc_event_handler(GATTcEventHandler *handler) { this->gattc_event_handlers_.push_back(handler); } | ||||
|   void register_gatts_event_handler(GATTsEventHandler *handler) { this->gatts_event_handlers_.push_back(handler); } | ||||
|   void register_ble_status_event_handler(BLEStatusEventHandler *handler) { | ||||
| @@ -113,22 +133,23 @@ class ESP32BLE : public Component { | ||||
|   static void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); | ||||
|   static void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); | ||||
|  | ||||
|   void real_gatts_event_handler_(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param); | ||||
|   void real_gattc_event_handler_(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, esp_ble_gattc_cb_param_t *param); | ||||
|   void real_gap_event_handler_(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param); | ||||
|  | ||||
|   bool ble_setup_(); | ||||
|   bool ble_dismantle_(); | ||||
|   bool ble_pre_setup_(); | ||||
|   void advertising_init_(); | ||||
|  | ||||
|  private: | ||||
|   template<typename... Args> friend void enqueue_ble_event(Args... args); | ||||
|  | ||||
|   std::vector<GAPEventHandler *> gap_event_handlers_; | ||||
|   std::vector<GAPScanEventHandler *> gap_scan_event_handlers_; | ||||
|   std::vector<GATTcEventHandler *> gattc_event_handlers_; | ||||
|   std::vector<GATTsEventHandler *> gatts_event_handlers_; | ||||
|   std::vector<BLEStatusEventHandler *> ble_status_event_handlers_; | ||||
|   BLEComponentState state_{BLE_COMPONENT_STATE_OFF}; | ||||
|  | ||||
|   Queue<BLEEvent> ble_events_; | ||||
|   LockFreeQueue<BLEEvent, MAX_BLE_QUEUE_SIZE> ble_events_; | ||||
|   BLEEventPool<MAX_BLE_QUEUE_SIZE> ble_event_pool_; | ||||
|   BLEAdvertising *advertising_{}; | ||||
|   esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE}; | ||||
|   uint32_t advertising_cycle_time_{}; | ||||
|   | ||||
| @@ -2,92 +2,399 @@ | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <cstddef>  // for offsetof | ||||
| #include <vector> | ||||
|  | ||||
| #include <esp_gap_ble_api.h> | ||||
| #include <esp_gattc_api.h> | ||||
| #include <esp_gatts_api.h> | ||||
|  | ||||
| #include "ble_scan_result.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_ble { | ||||
|  | ||||
| // Compile-time verification that ESP-IDF scan complete events only contain a status field | ||||
| // This ensures our reinterpret_cast in ble.cpp is safe | ||||
| static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param) == sizeof(esp_bt_status_t), | ||||
|               "ESP-IDF scan_param_cmpl structure has unexpected size"); | ||||
| static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_start_cmpl_evt_param) == sizeof(esp_bt_status_t), | ||||
|               "ESP-IDF scan_start_cmpl structure has unexpected size"); | ||||
| static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_stop_cmpl_evt_param) == sizeof(esp_bt_status_t), | ||||
|               "ESP-IDF scan_stop_cmpl structure has unexpected size"); | ||||
|  | ||||
| // Verify the status field is at offset 0 (first member) | ||||
| static_assert(offsetof(esp_ble_gap_cb_param_t, scan_param_cmpl.status) == 0, | ||||
|               "status must be first member of scan_param_cmpl"); | ||||
| static_assert(offsetof(esp_ble_gap_cb_param_t, scan_start_cmpl.status) == 0, | ||||
|               "status must be first member of scan_start_cmpl"); | ||||
| static_assert(offsetof(esp_ble_gap_cb_param_t, scan_stop_cmpl.status) == 0, | ||||
|               "status must be first member of scan_stop_cmpl"); | ||||
|  | ||||
| // Compile-time verification for advertising complete events | ||||
| static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_cmpl_evt_param) == sizeof(esp_bt_status_t), | ||||
|               "ESP-IDF adv_data_cmpl structure has unexpected size"); | ||||
| static_assert(sizeof(esp_ble_gap_cb_param_t::ble_scan_rsp_data_cmpl_evt_param) == sizeof(esp_bt_status_t), | ||||
|               "ESP-IDF scan_rsp_data_cmpl structure has unexpected size"); | ||||
| static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_data_raw_cmpl_evt_param) == sizeof(esp_bt_status_t), | ||||
|               "ESP-IDF adv_data_raw_cmpl structure has unexpected size"); | ||||
| static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_start_cmpl_evt_param) == sizeof(esp_bt_status_t), | ||||
|               "ESP-IDF adv_start_cmpl structure has unexpected size"); | ||||
| static_assert(sizeof(esp_ble_gap_cb_param_t::ble_adv_stop_cmpl_evt_param) == sizeof(esp_bt_status_t), | ||||
|               "ESP-IDF adv_stop_cmpl structure has unexpected size"); | ||||
|  | ||||
| // Verify the status field is at offset 0 for advertising events | ||||
| static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_cmpl.status) == 0, | ||||
|               "status must be first member of adv_data_cmpl"); | ||||
| static_assert(offsetof(esp_ble_gap_cb_param_t, scan_rsp_data_cmpl.status) == 0, | ||||
|               "status must be first member of scan_rsp_data_cmpl"); | ||||
| static_assert(offsetof(esp_ble_gap_cb_param_t, adv_data_raw_cmpl.status) == 0, | ||||
|               "status must be first member of adv_data_raw_cmpl"); | ||||
| static_assert(offsetof(esp_ble_gap_cb_param_t, adv_start_cmpl.status) == 0, | ||||
|               "status must be first member of adv_start_cmpl"); | ||||
| static_assert(offsetof(esp_ble_gap_cb_param_t, adv_stop_cmpl.status) == 0, | ||||
|               "status must be first member of adv_stop_cmpl"); | ||||
|  | ||||
| // Compile-time verification for RSSI complete event structure | ||||
| static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.status) == 0, | ||||
|               "status must be first member of read_rssi_cmpl"); | ||||
| static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.rssi) == sizeof(esp_bt_status_t), | ||||
|               "rssi must immediately follow status in read_rssi_cmpl"); | ||||
| static_assert(offsetof(esp_ble_gap_cb_param_t, read_rssi_cmpl.remote_addr) == sizeof(esp_bt_status_t) + sizeof(int8_t), | ||||
|               "remote_addr must follow rssi in read_rssi_cmpl"); | ||||
|  | ||||
| // Received GAP, GATTC and GATTS events are only queued, and get processed in the main loop(). | ||||
| // This class stores each event in a single type. | ||||
| // This class stores each event with minimal memory usage. | ||||
| // GAP events (99% of traffic) don't have the vector overhead. | ||||
| // GATTC/GATTS events use heap allocation for their param and data. | ||||
| // | ||||
| // Event flow: | ||||
| // 1. ESP-IDF BLE stack calls our static handlers in the BLE task context | ||||
| // 2. The handlers create a BLEEvent instance, copying only the data we need | ||||
| // 3. The event is pushed to a thread-safe queue | ||||
| // 4. In the main loop(), events are popped from the queue and processed | ||||
| // 5. The event destructor cleans up any external allocations | ||||
| // | ||||
| // Thread safety: | ||||
| // - GAP events: We copy only the fields we need directly into the union | ||||
| // - GATTC/GATTS events: We heap-allocate and copy the entire param struct, ensuring | ||||
| //   the data remains valid even after the BLE callback returns. The original | ||||
| //   param pointer from ESP-IDF is only valid during the callback. | ||||
| // | ||||
| // CRITICAL DESIGN NOTE: | ||||
| // The heap allocations for GATTC/GATTS events are REQUIRED for memory safety. | ||||
| // DO NOT attempt to optimize by removing these allocations or storing pointers | ||||
| // to the original ESP-IDF data. The ESP-IDF callback data has a different lifetime | ||||
| // than our event processing, and accessing it after the callback returns would | ||||
| // result in use-after-free bugs and crashes. | ||||
| class BLEEvent { | ||||
|  public: | ||||
|   BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { | ||||
|     this->event_.gap.gap_event = e; | ||||
|     memcpy(&this->event_.gap.gap_param, p, sizeof(esp_ble_gap_cb_param_t)); | ||||
|     this->type_ = GAP; | ||||
|   }; | ||||
|  | ||||
|   BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { | ||||
|     this->event_.gattc.gattc_event = e; | ||||
|     this->event_.gattc.gattc_if = i; | ||||
|     memcpy(&this->event_.gattc.gattc_param, p, sizeof(esp_ble_gattc_cb_param_t)); | ||||
|     // Need to also make a copy of relevant event data. | ||||
|     switch (e) { | ||||
|       case ESP_GATTC_NOTIFY_EVT: | ||||
|         this->data.assign(p->notify.value, p->notify.value + p->notify.value_len); | ||||
|         this->event_.gattc.gattc_param.notify.value = this->data.data(); | ||||
|         break; | ||||
|       case ESP_GATTC_READ_CHAR_EVT: | ||||
|       case ESP_GATTC_READ_DESCR_EVT: | ||||
|         this->data.assign(p->read.value, p->read.value + p->read.value_len); | ||||
|         this->event_.gattc.gattc_param.read.value = this->data.data(); | ||||
|         break; | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
|     this->type_ = GATTC; | ||||
|   }; | ||||
|  | ||||
|   BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { | ||||
|     this->event_.gatts.gatts_event = e; | ||||
|     this->event_.gatts.gatts_if = i; | ||||
|     memcpy(&this->event_.gatts.gatts_param, p, sizeof(esp_ble_gatts_cb_param_t)); | ||||
|     // Need to also make a copy of relevant event data. | ||||
|     switch (e) { | ||||
|       case ESP_GATTS_WRITE_EVT: | ||||
|         this->data.assign(p->write.value, p->write.value + p->write.len); | ||||
|         this->event_.gatts.gatts_param.write.value = this->data.data(); | ||||
|         break; | ||||
|       default: | ||||
|         break; | ||||
|     } | ||||
|     this->type_ = GATTS; | ||||
|   }; | ||||
|  | ||||
|   union { | ||||
|     // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|     struct gap_event { | ||||
|       esp_gap_ble_cb_event_t gap_event; | ||||
|       esp_ble_gap_cb_param_t gap_param; | ||||
|     } gap; | ||||
|  | ||||
|     // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|     struct gattc_event { | ||||
|       esp_gattc_cb_event_t gattc_event; | ||||
|       esp_gatt_if_t gattc_if; | ||||
|       esp_ble_gattc_cb_param_t gattc_param; | ||||
|     } gattc; | ||||
|  | ||||
|     // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|     struct gatts_event { | ||||
|       esp_gatts_cb_event_t gatts_event; | ||||
|       esp_gatt_if_t gatts_if; | ||||
|       esp_ble_gatts_cb_param_t gatts_param; | ||||
|     } gatts; | ||||
|   } event_; | ||||
|  | ||||
|   std::vector<uint8_t> data{}; | ||||
|   // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|   enum ble_event_t : uint8_t { | ||||
|     GAP, | ||||
|     GATTC, | ||||
|     GATTS, | ||||
|   } type_; | ||||
|   }; | ||||
|  | ||||
|   // Type definitions for cleaner method signatures | ||||
|   struct StatusOnlyData { | ||||
|     esp_bt_status_t status; | ||||
|   }; | ||||
|  | ||||
|   struct RSSICompleteData { | ||||
|     esp_bt_status_t status; | ||||
|     int8_t rssi; | ||||
|     esp_bd_addr_t remote_addr; | ||||
|   }; | ||||
|  | ||||
|   // Constructor for GAP events - no external allocations needed | ||||
|   BLEEvent(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { | ||||
|     this->type_ = GAP; | ||||
|     this->init_gap_data_(e, p); | ||||
|   } | ||||
|  | ||||
|   // Constructor for GATTC events - uses heap allocation | ||||
|   // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization. | ||||
|   // The param pointer from ESP-IDF is only valid during the callback execution. | ||||
|   // Since BLE events are processed asynchronously in the main loop, we must create | ||||
|   // our own copy to ensure the data remains valid until the event is processed. | ||||
|   BLEEvent(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { | ||||
|     this->type_ = GATTC; | ||||
|     this->init_gattc_data_(e, i, p); | ||||
|   } | ||||
|  | ||||
|   // Constructor for GATTS events - uses heap allocation | ||||
|   // IMPORTANT: The heap allocation is REQUIRED and must not be removed as an optimization. | ||||
|   // The param pointer from ESP-IDF is only valid during the callback execution. | ||||
|   // Since BLE events are processed asynchronously in the main loop, we must create | ||||
|   // our own copy to ensure the data remains valid until the event is processed. | ||||
|   BLEEvent(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { | ||||
|     this->type_ = GATTS; | ||||
|     this->init_gatts_data_(e, i, p); | ||||
|   } | ||||
|  | ||||
|   // Destructor to clean up heap allocations | ||||
|   ~BLEEvent() { this->cleanup_heap_data(); } | ||||
|  | ||||
|   // Default constructor for pre-allocation in pool | ||||
|   BLEEvent() : type_(GAP) {} | ||||
|  | ||||
|   // Clean up any heap-allocated data | ||||
|   void cleanup_heap_data() { | ||||
|     if (this->type_ == GAP) { | ||||
|       return; | ||||
|     } | ||||
|     if (this->type_ == GATTC) { | ||||
|       delete this->event_.gattc.gattc_param; | ||||
|       delete this->event_.gattc.data; | ||||
|       this->event_.gattc.gattc_param = nullptr; | ||||
|       this->event_.gattc.data = nullptr; | ||||
|       return; | ||||
|     } | ||||
|     if (this->type_ == GATTS) { | ||||
|       delete this->event_.gatts.gatts_param; | ||||
|       delete this->event_.gatts.data; | ||||
|       this->event_.gatts.gatts_param = nullptr; | ||||
|       this->event_.gatts.data = nullptr; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Load new event data for reuse (replaces previous event data) | ||||
|   void load_gap_event(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { | ||||
|     this->cleanup_heap_data(); | ||||
|     this->type_ = GAP; | ||||
|     this->init_gap_data_(e, p); | ||||
|   } | ||||
|  | ||||
|   void load_gattc_event(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { | ||||
|     this->cleanup_heap_data(); | ||||
|     this->type_ = GATTC; | ||||
|     this->init_gattc_data_(e, i, p); | ||||
|   } | ||||
|  | ||||
|   void load_gatts_event(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { | ||||
|     this->cleanup_heap_data(); | ||||
|     this->type_ = GATTS; | ||||
|     this->init_gatts_data_(e, i, p); | ||||
|   } | ||||
|  | ||||
|   // Disable copy to prevent double-delete | ||||
|   BLEEvent(const BLEEvent &) = delete; | ||||
|   BLEEvent &operator=(const BLEEvent &) = delete; | ||||
|  | ||||
|   union { | ||||
|     // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|     struct gap_event { | ||||
|       esp_gap_ble_cb_event_t gap_event; | ||||
|       union { | ||||
|         BLEScanResult scan_result;  // 73 bytes - Used by: esp32_ble_tracker | ||||
|         // This matches ESP-IDF's scan complete event structures | ||||
|         // All three (scan_param_cmpl, scan_start_cmpl, scan_stop_cmpl) have identical layout | ||||
|         // Used by: esp32_ble_tracker | ||||
|         StatusOnlyData scan_complete;  // 1 byte | ||||
|         // Advertising complete events all have same structure | ||||
|         // Used by: esp32_ble_beacon, esp32_ble server components | ||||
|         // ADV_DATA_SET, SCAN_RSP_DATA_SET, ADV_DATA_RAW_SET, ADV_START, ADV_STOP | ||||
|         StatusOnlyData adv_complete;  // 1 byte | ||||
|         // RSSI complete event | ||||
|         // Used by: ble_client (ble_rssi_sensor component) | ||||
|         RSSICompleteData read_rssi_complete;  // 8 bytes | ||||
|         // Security events - we store the full security union | ||||
|         // Used by: ble_client (automation), bluetooth_proxy, esp32_ble_client | ||||
|         esp_ble_sec_t security;  // Variable size, but fits within scan_result size | ||||
|       }; | ||||
|     } gap;  // 80 bytes total | ||||
|  | ||||
|     // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|     struct gattc_event { | ||||
|       esp_gattc_cb_event_t gattc_event; | ||||
|       esp_gatt_if_t gattc_if; | ||||
|       esp_ble_gattc_cb_param_t *gattc_param;  // Heap-allocated | ||||
|       std::vector<uint8_t> *data;             // Heap-allocated | ||||
|     } gattc;                                  // 16 bytes (pointers only) | ||||
|  | ||||
|     // NOLINTNEXTLINE(readability-identifier-naming) | ||||
|     struct gatts_event { | ||||
|       esp_gatts_cb_event_t gatts_event; | ||||
|       esp_gatt_if_t gatts_if; | ||||
|       esp_ble_gatts_cb_param_t *gatts_param;  // Heap-allocated | ||||
|       std::vector<uint8_t> *data;             // Heap-allocated | ||||
|     } gatts;                                  // 16 bytes (pointers only) | ||||
|   } event_;                                   // 80 bytes | ||||
|  | ||||
|   ble_event_t type_; | ||||
|  | ||||
|   // Helper methods to access event data | ||||
|   ble_event_t type() const { return type_; } | ||||
|   esp_gap_ble_cb_event_t gap_event_type() const { return event_.gap.gap_event; } | ||||
|   const BLEScanResult &scan_result() const { return event_.gap.scan_result; } | ||||
|   esp_bt_status_t scan_complete_status() const { return event_.gap.scan_complete.status; } | ||||
|   esp_bt_status_t adv_complete_status() const { return event_.gap.adv_complete.status; } | ||||
|   const RSSICompleteData &read_rssi_complete() const { return event_.gap.read_rssi_complete; } | ||||
|   const esp_ble_sec_t &security() const { return event_.gap.security; } | ||||
|  | ||||
|  private: | ||||
|   // Initialize GAP event data | ||||
|   void init_gap_data_(esp_gap_ble_cb_event_t e, esp_ble_gap_cb_param_t *p) { | ||||
|     this->event_.gap.gap_event = e; | ||||
|  | ||||
|     if (p == nullptr) { | ||||
|       return;  // Invalid event, but we can't log in header file | ||||
|     } | ||||
|  | ||||
|     // Copy data based on event type | ||||
|     switch (e) { | ||||
|       case ESP_GAP_BLE_SCAN_RESULT_EVT: | ||||
|         memcpy(this->event_.gap.scan_result.bda, p->scan_rst.bda, sizeof(esp_bd_addr_t)); | ||||
|         this->event_.gap.scan_result.ble_addr_type = p->scan_rst.ble_addr_type; | ||||
|         this->event_.gap.scan_result.rssi = p->scan_rst.rssi; | ||||
|         this->event_.gap.scan_result.adv_data_len = p->scan_rst.adv_data_len; | ||||
|         this->event_.gap.scan_result.scan_rsp_len = p->scan_rst.scan_rsp_len; | ||||
|         this->event_.gap.scan_result.search_evt = p->scan_rst.search_evt; | ||||
|         memcpy(this->event_.gap.scan_result.ble_adv, p->scan_rst.ble_adv, | ||||
|                ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX); | ||||
|         break; | ||||
|  | ||||
|       case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: | ||||
|         this->event_.gap.scan_complete.status = p->scan_param_cmpl.status; | ||||
|         break; | ||||
|  | ||||
|       case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: | ||||
|         this->event_.gap.scan_complete.status = p->scan_start_cmpl.status; | ||||
|         break; | ||||
|  | ||||
|       case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: | ||||
|         this->event_.gap.scan_complete.status = p->scan_stop_cmpl.status; | ||||
|         break; | ||||
|  | ||||
|       // Advertising complete events - all have same structure with just status | ||||
|       // Used by: esp32_ble_beacon, esp32_ble server components | ||||
|       case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: | ||||
|         this->event_.gap.adv_complete.status = p->adv_data_cmpl.status; | ||||
|         break; | ||||
|       case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: | ||||
|         this->event_.gap.adv_complete.status = p->scan_rsp_data_cmpl.status; | ||||
|         break; | ||||
|       case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:  // Used by: esp32_ble_beacon | ||||
|         this->event_.gap.adv_complete.status = p->adv_data_raw_cmpl.status; | ||||
|         break; | ||||
|       case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:  // Used by: esp32_ble_beacon | ||||
|         this->event_.gap.adv_complete.status = p->adv_start_cmpl.status; | ||||
|         break; | ||||
|       case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:  // Used by: esp32_ble_beacon | ||||
|         this->event_.gap.adv_complete.status = p->adv_stop_cmpl.status; | ||||
|         break; | ||||
|  | ||||
|       // RSSI complete event | ||||
|       // Used by: ble_client (ble_rssi_sensor) | ||||
|       case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: | ||||
|         this->event_.gap.read_rssi_complete.status = p->read_rssi_cmpl.status; | ||||
|         this->event_.gap.read_rssi_complete.rssi = p->read_rssi_cmpl.rssi; | ||||
|         memcpy(this->event_.gap.read_rssi_complete.remote_addr, p->read_rssi_cmpl.remote_addr, sizeof(esp_bd_addr_t)); | ||||
|         break; | ||||
|  | ||||
|       // Security events - copy the entire security union | ||||
|       // Used by: ble_client, bluetooth_proxy, esp32_ble_client | ||||
|       case ESP_GAP_BLE_AUTH_CMPL_EVT:      // Used by: bluetooth_proxy, esp32_ble_client | ||||
|       case ESP_GAP_BLE_SEC_REQ_EVT:        // Used by: esp32_ble_client | ||||
|       case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:  // Used by: ble_client automation | ||||
|       case ESP_GAP_BLE_PASSKEY_REQ_EVT:    // Used by: ble_client automation | ||||
|       case ESP_GAP_BLE_NC_REQ_EVT:         // Used by: ble_client automation | ||||
|         memcpy(&this->event_.gap.security, &p->ble_security, sizeof(esp_ble_sec_t)); | ||||
|         break; | ||||
|  | ||||
|       default: | ||||
|         // We only store data for GAP events that components currently use | ||||
|         // Unknown events still get queued and logged in ble.cpp:375 as | ||||
|         // "Unhandled GAP event type in loop" - this helps identify new events | ||||
|         // that components might need in the future | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Initialize GATTC event data | ||||
|   void init_gattc_data_(esp_gattc_cb_event_t e, esp_gatt_if_t i, esp_ble_gattc_cb_param_t *p) { | ||||
|     this->event_.gattc.gattc_event = e; | ||||
|     this->event_.gattc.gattc_if = i; | ||||
|  | ||||
|     if (p == nullptr) { | ||||
|       this->event_.gattc.gattc_param = nullptr; | ||||
|       this->event_.gattc.data = nullptr; | ||||
|       return;  // Invalid event, but we can't log in header file | ||||
|     } | ||||
|  | ||||
|     // Heap-allocate param and data | ||||
|     // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) | ||||
|     // while GAP events (99%) are stored inline to minimize memory usage | ||||
|     // IMPORTANT: This heap allocation provides clear ownership semantics: | ||||
|     // - The BLEEvent owns the allocated memory for its lifetime | ||||
|     // - The data remains valid from the BLE callback context until processed in the main loop | ||||
|     // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory | ||||
|     this->event_.gattc.gattc_param = new esp_ble_gattc_cb_param_t(*p); | ||||
|  | ||||
|     // Copy data for events that need it | ||||
|     // The param struct contains pointers (e.g., notify.value) that point to temporary buffers. | ||||
|     // We must copy this data to ensure it remains valid when the event is processed later. | ||||
|     switch (e) { | ||||
|       case ESP_GATTC_NOTIFY_EVT: | ||||
|         this->event_.gattc.data = new std::vector<uint8_t>(p->notify.value, p->notify.value + p->notify.value_len); | ||||
|         this->event_.gattc.gattc_param->notify.value = this->event_.gattc.data->data(); | ||||
|         break; | ||||
|       case ESP_GATTC_READ_CHAR_EVT: | ||||
|       case ESP_GATTC_READ_DESCR_EVT: | ||||
|         this->event_.gattc.data = new std::vector<uint8_t>(p->read.value, p->read.value + p->read.value_len); | ||||
|         this->event_.gattc.gattc_param->read.value = this->event_.gattc.data->data(); | ||||
|         break; | ||||
|       default: | ||||
|         this->event_.gattc.data = nullptr; | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Initialize GATTS event data | ||||
|   void init_gatts_data_(esp_gatts_cb_event_t e, esp_gatt_if_t i, esp_ble_gatts_cb_param_t *p) { | ||||
|     this->event_.gatts.gatts_event = e; | ||||
|     this->event_.gatts.gatts_if = i; | ||||
|  | ||||
|     if (p == nullptr) { | ||||
|       this->event_.gatts.gatts_param = nullptr; | ||||
|       this->event_.gatts.data = nullptr; | ||||
|       return;  // Invalid event, but we can't log in header file | ||||
|     } | ||||
|  | ||||
|     // Heap-allocate param and data | ||||
|     // Heap allocation is used because GATTC/GATTS events are rare (<1% of events) | ||||
|     // while GAP events (99%) are stored inline to minimize memory usage | ||||
|     // IMPORTANT: This heap allocation provides clear ownership semantics: | ||||
|     // - The BLEEvent owns the allocated memory for its lifetime | ||||
|     // - The data remains valid from the BLE callback context until processed in the main loop | ||||
|     // - Without this copy, we'd have use-after-free bugs as ESP-IDF reuses the callback memory | ||||
|     this->event_.gatts.gatts_param = new esp_ble_gatts_cb_param_t(*p); | ||||
|  | ||||
|     // Copy data for events that need it | ||||
|     // The param struct contains pointers (e.g., write.value) that point to temporary buffers. | ||||
|     // We must copy this data to ensure it remains valid when the event is processed later. | ||||
|     switch (e) { | ||||
|       case ESP_GATTS_WRITE_EVT: | ||||
|         this->event_.gatts.data = new std::vector<uint8_t>(p->write.value, p->write.value + p->write.len); | ||||
|         this->event_.gatts.gatts_param->write.value = this->event_.gatts.data->data(); | ||||
|         break; | ||||
|       default: | ||||
|         this->event_.gatts.data = nullptr; | ||||
|         break; | ||||
|     } | ||||
|   } | ||||
| }; | ||||
|  | ||||
| // Verify the gap_event struct hasn't grown beyond expected size | ||||
| // The gap member in the union should be 80 bytes (including the gap_event enum) | ||||
| static_assert(sizeof(decltype(((BLEEvent *) nullptr)->event_.gap)) <= 80, "gap_event struct has grown beyond 80 bytes"); | ||||
|  | ||||
| // Verify esp_ble_sec_t fits within our union | ||||
| static_assert(sizeof(esp_ble_sec_t) <= 73, "esp_ble_sec_t is larger than BLEScanResult"); | ||||
|  | ||||
| // BLEEvent total size: 84 bytes (80 byte union + 1 byte type + 3 bytes padding) | ||||
|  | ||||
| }  // namespace esp32_ble | ||||
| }  // namespace esphome | ||||
|  | ||||
|   | ||||
							
								
								
									
										72
									
								
								esphome/components/esp32_ble/ble_event_pool.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										72
									
								
								esphome/components/esp32_ble/ble_event_pool.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,72 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <atomic> | ||||
| #include <cstddef> | ||||
| #include "ble_event.h" | ||||
| #include "queue.h" | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_ble { | ||||
|  | ||||
| // BLE Event Pool - On-demand pool of BLEEvent objects to avoid heap fragmentation | ||||
| // Events are allocated on first use and reused thereafter, growing to peak usage | ||||
| template<uint8_t SIZE> class BLEEventPool { | ||||
|  public: | ||||
|   BLEEventPool() : total_created_(0) {} | ||||
|  | ||||
|   ~BLEEventPool() { | ||||
|     // Clean up any remaining events in the free list | ||||
|     BLEEvent *event; | ||||
|     while ((event = this->free_list_.pop()) != nullptr) { | ||||
|       delete event; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Allocate an event from the pool | ||||
|   // Returns nullptr if pool is full | ||||
|   BLEEvent *allocate() { | ||||
|     // Try to get from free list first | ||||
|     BLEEvent *event = this->free_list_.pop(); | ||||
|     if (event != nullptr) | ||||
|       return event; | ||||
|  | ||||
|     // Need to create a new event | ||||
|     if (this->total_created_ >= SIZE) { | ||||
|       // Pool is at capacity | ||||
|       return nullptr; | ||||
|     } | ||||
|  | ||||
|     // Use internal RAM for better performance | ||||
|     RAMAllocator<BLEEvent> allocator(RAMAllocator<BLEEvent>::ALLOC_INTERNAL); | ||||
|     event = allocator.allocate(1); | ||||
|  | ||||
|     if (event == nullptr) { | ||||
|       // Memory allocation failed | ||||
|       return nullptr; | ||||
|     } | ||||
|  | ||||
|     // Placement new to construct the object | ||||
|     new (event) BLEEvent(); | ||||
|     this->total_created_++; | ||||
|     return event; | ||||
|   } | ||||
|  | ||||
|   // Return an event to the pool for reuse | ||||
|   void release(BLEEvent *event) { | ||||
|     if (event != nullptr) { | ||||
|       this->free_list_.push(event); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  private: | ||||
|   LockFreeQueue<BLEEvent, SIZE> free_list_;  // Free events ready for reuse | ||||
|   uint8_t total_created_;                    // Total events created (high water mark) | ||||
| }; | ||||
|  | ||||
| }  // namespace esp32_ble | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
							
								
								
									
										24
									
								
								esphome/components/esp32_ble/ble_scan_result.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								esphome/components/esp32_ble/ble_scan_result.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,24 @@ | ||||
| #pragma once | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <esp_gap_ble_api.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_ble { | ||||
|  | ||||
| // Structure for BLE scan results - only fields we actually use | ||||
| struct __attribute__((packed)) BLEScanResult { | ||||
|   esp_bd_addr_t bda; | ||||
|   uint8_t ble_addr_type; | ||||
|   int8_t rssi; | ||||
|   uint8_t ble_adv[ESP_BLE_ADV_DATA_LEN_MAX + ESP_BLE_SCAN_RSP_DATA_LEN_MAX]; | ||||
|   uint8_t adv_data_len; | ||||
|   uint8_t scan_rsp_len; | ||||
|   uint8_t search_evt; | ||||
| };  // ~73 bytes vs ~400 bytes for full esp_ble_gap_cb_param_t | ||||
|  | ||||
| }  // namespace esp32_ble | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -2,52 +2,81 @@ | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <mutex> | ||||
| #include <queue> | ||||
|  | ||||
| #include <freertos/FreeRTOS.h> | ||||
| #include <freertos/semphr.h> | ||||
| #include <atomic> | ||||
| #include <cstddef> | ||||
|  | ||||
| /* | ||||
|  * BLE events come in from a separate Task (thread) in the ESP32 stack. Rather | ||||
|  * than trying to deal with various locking strategies, all incoming GAP and GATT | ||||
|  * events will simply be placed on a semaphore guarded queue. The next time the | ||||
|  * component runs loop(), these events are popped off the queue and handed at | ||||
|  * this safer time. | ||||
|  * than using mutex-based locking, this lock-free queue allows the BLE | ||||
|  * task to enqueue events without blocking. The main loop() then processes | ||||
|  * these events at a safer time. | ||||
|  * | ||||
|  * This is a Single-Producer Single-Consumer (SPSC) lock-free ring buffer. | ||||
|  * The BLE task is the only producer, and the main loop() is the only consumer. | ||||
|  */ | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_ble { | ||||
|  | ||||
| template<class T> class Queue { | ||||
| template<class T, uint8_t SIZE> class LockFreeQueue { | ||||
|  public: | ||||
|   Queue() { m_ = xSemaphoreCreateMutex(); } | ||||
|   LockFreeQueue() : head_(0), tail_(0), dropped_count_(0) {} | ||||
|  | ||||
|   void push(T *element) { | ||||
|   bool push(T *element) { | ||||
|     if (element == nullptr) | ||||
|       return; | ||||
|     // It is not called from main loop. Thus it won't block main thread. | ||||
|     xSemaphoreTake(m_, portMAX_DELAY); | ||||
|     q_.push(element); | ||||
|     xSemaphoreGive(m_); | ||||
|       return false; | ||||
|  | ||||
|     uint8_t current_tail = tail_.load(std::memory_order_relaxed); | ||||
|     uint8_t next_tail = (current_tail + 1) % SIZE; | ||||
|  | ||||
|     if (next_tail == head_.load(std::memory_order_acquire)) { | ||||
|       // Buffer full | ||||
|       dropped_count_.fetch_add(1, std::memory_order_relaxed); | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
|     buffer_[current_tail] = element; | ||||
|     tail_.store(next_tail, std::memory_order_release); | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   T *pop() { | ||||
|     T *element = nullptr; | ||||
|     uint8_t current_head = head_.load(std::memory_order_relaxed); | ||||
|  | ||||
|     if (xSemaphoreTake(m_, 5L / portTICK_PERIOD_MS)) { | ||||
|       if (!q_.empty()) { | ||||
|         element = q_.front(); | ||||
|         q_.pop(); | ||||
|       } | ||||
|       xSemaphoreGive(m_); | ||||
|     if (current_head == tail_.load(std::memory_order_acquire)) { | ||||
|       return nullptr;  // Empty | ||||
|     } | ||||
|  | ||||
|     T *element = buffer_[current_head]; | ||||
|     head_.store((current_head + 1) % SIZE, std::memory_order_release); | ||||
|     return element; | ||||
|   } | ||||
|  | ||||
|   size_t size() const { | ||||
|     uint8_t tail = tail_.load(std::memory_order_acquire); | ||||
|     uint8_t head = head_.load(std::memory_order_acquire); | ||||
|     return (tail - head + SIZE) % SIZE; | ||||
|   } | ||||
|  | ||||
|   uint16_t get_and_reset_dropped_count() { return dropped_count_.exchange(0, std::memory_order_relaxed); } | ||||
|  | ||||
|   void increment_dropped_count() { dropped_count_.fetch_add(1, std::memory_order_relaxed); } | ||||
|  | ||||
|   bool empty() const { return head_.load(std::memory_order_acquire) == tail_.load(std::memory_order_acquire); } | ||||
|  | ||||
|   bool full() const { | ||||
|     uint8_t next_tail = (tail_.load(std::memory_order_relaxed) + 1) % SIZE; | ||||
|     return next_tail == head_.load(std::memory_order_acquire); | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   std::queue<T *> q_; | ||||
|   SemaphoreHandle_t m_; | ||||
|   T *buffer_[SIZE]; | ||||
|   // Atomic: written by producer (push/increment), read+reset by consumer (get_and_reset) | ||||
|   std::atomic<uint16_t> dropped_count_;  // 65535 max - more than enough for drop tracking | ||||
|   // Atomic: written by consumer (pop), read by producer (push) to check if full | ||||
|   std::atomic<uint8_t> head_; | ||||
|   // Atomic: written by producer (push), read by consumer (pop) to check if empty | ||||
|   std::atomic<uint8_t> tail_; | ||||
| }; | ||||
|  | ||||
| }  // namespace esp32_ble | ||||
|   | ||||
| @@ -22,6 +22,16 @@ void BLEClientBase::setup() { | ||||
|   this->connection_index_ = connection_index++; | ||||
| } | ||||
|  | ||||
| void BLEClientBase::set_state(espbt::ClientState st) { | ||||
|   ESP_LOGV(TAG, "[%d] [%s] Set state %d", this->connection_index_, this->address_str_.c_str(), (int) st); | ||||
|   ESPBTClient::set_state(st); | ||||
|  | ||||
|   if (st == espbt::ClientState::READY_TO_CONNECT) { | ||||
|     // Enable loop when we need to connect | ||||
|     this->enable_loop(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void BLEClientBase::loop() { | ||||
|   if (!esp32_ble::global_ble->is_active()) { | ||||
|     this->set_state(espbt::ClientState::INIT); | ||||
| @@ -37,9 +47,14 @@ void BLEClientBase::loop() { | ||||
|   } | ||||
|   // READY_TO_CONNECT means we have discovered the device | ||||
|   // and the scanner has been stopped by the tracker. | ||||
|   if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { | ||||
|   else if (this->state_ == espbt::ClientState::READY_TO_CONNECT) { | ||||
|     this->connect(); | ||||
|   } | ||||
|   // If its idle, we can disable the loop as set_state | ||||
|   // will enable it again when we need to connect. | ||||
|   else if (this->state_ == espbt::ClientState::IDLE) { | ||||
|     this->disable_loop(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } | ||||
|   | ||||
| @@ -93,22 +93,37 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { | ||||
|  | ||||
|   bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; } | ||||
|  | ||||
|   void set_state(espbt::ClientState st) override; | ||||
|  | ||||
|  protected: | ||||
|   int gattc_if_; | ||||
|   esp_bd_addr_t remote_bda_; | ||||
|   esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC}; | ||||
|   uint16_t conn_id_{UNSET_CONN_ID}; | ||||
|   // Memory optimized layout for 32-bit systems | ||||
|   // Group 1: 8-byte types | ||||
|   uint64_t address_{0}; | ||||
|   bool auto_connect_{false}; | ||||
|  | ||||
|   // Group 2: Container types (grouped for memory optimization) | ||||
|   std::string address_str_{}; | ||||
|   uint8_t connection_index_; | ||||
|   int16_t service_count_{0}; | ||||
|   uint16_t mtu_{23}; | ||||
|   bool paired_{false}; | ||||
|   espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; | ||||
|   std::vector<BLEService *> services_; | ||||
|  | ||||
|   // Group 3: 4-byte types | ||||
|   int gattc_if_; | ||||
|   esp_gatt_status_t status_{ESP_GATT_OK}; | ||||
|  | ||||
|   // Group 4: Arrays (6 bytes) | ||||
|   esp_bd_addr_t remote_bda_; | ||||
|  | ||||
|   // Group 5: 2-byte types | ||||
|   uint16_t conn_id_{UNSET_CONN_ID}; | ||||
|   uint16_t mtu_{23}; | ||||
|  | ||||
|   // Group 6: 1-byte types and small enums | ||||
|   esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC}; | ||||
|   espbt::ConnectionType connection_type_{espbt::ConnectionType::V1}; | ||||
|   uint8_t connection_index_; | ||||
|   uint8_t service_count_{0};  // ESP32 has max handles < 255, typical devices have < 50 services | ||||
|   bool auto_connect_{false}; | ||||
|   bool paired_{false}; | ||||
|   // 6 bytes used, 2 bytes padding | ||||
|  | ||||
|   void log_event_(const char *name); | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -268,6 +268,7 @@ async def to_code(config): | ||||
|  | ||||
|     parent = await cg.get_variable(config[esp32_ble.CONF_BLE_ID]) | ||||
|     cg.add(parent.register_gap_event_handler(var)) | ||||
|     cg.add(parent.register_gap_scan_event_handler(var)) | ||||
|     cg.add(parent.register_gattc_event_handler(var)) | ||||
|     cg.add(parent.register_ble_status_event_handler(var)) | ||||
|     cg.add(var.set_parent(parent)) | ||||
|   | ||||
| @@ -50,17 +50,15 @@ void ESP32BLETracker::setup() { | ||||
|     ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE"); | ||||
|     return; | ||||
|   } | ||||
|   ExternalRAMAllocator<esp_ble_gap_cb_param_t::ble_scan_result_evt_param> allocator( | ||||
|       ExternalRAMAllocator<esp_ble_gap_cb_param_t::ble_scan_result_evt_param>::ALLOW_FAILURE); | ||||
|   this->scan_result_buffer_ = allocator.allocate(ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE); | ||||
|   RAMAllocator<BLEScanResult> allocator; | ||||
|   this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE); | ||||
|  | ||||
|   if (this->scan_result_buffer_ == nullptr) { | ||||
|     ESP_LOGE(TAG, "Could not allocate buffer for BLE Tracker!"); | ||||
|   if (this->scan_ring_buffer_ == nullptr) { | ||||
|     ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!"); | ||||
|     this->mark_failed(); | ||||
|   } | ||||
|  | ||||
|   global_esp32_ble_tracker = this; | ||||
|   this->scan_result_lock_ = xSemaphoreCreateMutex(); | ||||
|  | ||||
| #ifdef USE_OTA | ||||
|   ota::get_global_ota_callback()->add_on_state_callback( | ||||
| @@ -120,27 +118,31 @@ void ESP32BLETracker::loop() { | ||||
|   } | ||||
|   bool promote_to_connecting = discovered && !searching && !connecting; | ||||
|  | ||||
|   if (this->scanner_state_ == ScannerState::RUNNING && | ||||
|       this->scan_result_index_ &&  // if it looks like we have a scan result we will take the lock | ||||
|       xSemaphoreTake(this->scan_result_lock_, 0)) { | ||||
|     uint32_t index = this->scan_result_index_; | ||||
|     if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { | ||||
|       ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); | ||||
|     } | ||||
|   // Process scan results from lock-free SPSC ring buffer | ||||
|   // Consumer side: This runs in the main loop thread | ||||
|   if (this->scanner_state_ == ScannerState::RUNNING) { | ||||
|     // Load our own index with relaxed ordering (we're the only writer) | ||||
|     uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed); | ||||
|  | ||||
|     if (this->raw_advertisements_) { | ||||
|       for (auto *listener : this->listeners_) { | ||||
|         listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_); | ||||
|       } | ||||
|       for (auto *client : this->clients_) { | ||||
|         client->parse_devices(this->scan_result_buffer_, this->scan_result_index_); | ||||
|       } | ||||
|     } | ||||
|     // Load producer's index with acquire to see their latest writes | ||||
|     uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire); | ||||
|  | ||||
|     if (this->parse_advertisements_) { | ||||
|       for (size_t i = 0; i < index; i++) { | ||||
|     while (read_idx != write_idx) { | ||||
|       // Process one result at a time directly from ring buffer | ||||
|       BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx]; | ||||
|  | ||||
|       if (this->raw_advertisements_) { | ||||
|         for (auto *listener : this->listeners_) { | ||||
|           listener->parse_devices(&scan_result, 1); | ||||
|         } | ||||
|         for (auto *client : this->clients_) { | ||||
|           client->parse_devices(&scan_result, 1); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (this->parse_advertisements_) { | ||||
|         ESPBTDevice device; | ||||
|         device.parse_scan_rst(this->scan_result_buffer_[i]); | ||||
|         device.parse_scan_rst(scan_result); | ||||
|  | ||||
|         bool found = false; | ||||
|         for (auto *listener : this->listeners_) { | ||||
| @@ -161,9 +163,19 @@ void ESP32BLETracker::loop() { | ||||
|           this->print_bt_device_info(device); | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       // Move to next entry in ring buffer | ||||
|       read_idx = (read_idx + 1) % SCAN_RESULT_BUFFER_SIZE; | ||||
|  | ||||
|       // Store with release to ensure reads complete before index update | ||||
|       this->ring_read_index_.store(read_idx, std::memory_order_release); | ||||
|     } | ||||
|  | ||||
|     // Log dropped results periodically | ||||
|     size_t dropped = this->scan_results_dropped_.exchange(0, std::memory_order_relaxed); | ||||
|     if (dropped > 0) { | ||||
|       ESP_LOGW(TAG, "Dropped %zu BLE scan results due to buffer overflow", dropped); | ||||
|     } | ||||
|     this->scan_result_index_ = 0; | ||||
|     xSemaphoreGive(this->scan_result_lock_); | ||||
|   } | ||||
|   if (this->scanner_state_ == ScannerState::STOPPED) { | ||||
|     this->end_of_scan_();  // Change state to IDLE | ||||
| @@ -370,9 +382,6 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() { | ||||
|  | ||||
| void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) { | ||||
|   switch (event) { | ||||
|     case ESP_GAP_BLE_SCAN_RESULT_EVT: | ||||
|       this->gap_scan_result_(param->scan_rst); | ||||
|       break; | ||||
|     case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: | ||||
|       this->gap_scan_set_param_complete_(param->scan_param_cmpl); | ||||
|       break; | ||||
| @@ -385,11 +394,57 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga | ||||
|     default: | ||||
|       break; | ||||
|   } | ||||
|   // Forward all events to clients (scan results are handled separately via gap_scan_event_handler) | ||||
|   for (auto *client : this->clients_) { | ||||
|     client->gap_event_handler(event, param); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) { | ||||
|   ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt); | ||||
|  | ||||
|   if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { | ||||
|     // Lock-free SPSC ring buffer write (Producer side) | ||||
|     // This runs in the ESP-IDF Bluetooth stack callback thread | ||||
|     // IMPORTANT: Only this thread writes to ring_write_index_ | ||||
|  | ||||
|     // Load our own index with relaxed ordering (we're the only writer) | ||||
|     uint8_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed); | ||||
|     uint8_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE; | ||||
|  | ||||
|     // Load consumer's index with acquire to see their latest updates | ||||
|     uint8_t read_idx = this->ring_read_index_.load(std::memory_order_acquire); | ||||
|  | ||||
|     // Check if buffer is full | ||||
|     if (next_write_idx != read_idx) { | ||||
|       // Write to ring buffer | ||||
|       this->scan_ring_buffer_[write_idx] = scan_result; | ||||
|  | ||||
|       // Store with release to ensure the write is visible before index update | ||||
|       this->ring_write_index_.store(next_write_idx, std::memory_order_release); | ||||
|     } else { | ||||
|       // Buffer full, track dropped results | ||||
|       this->scan_results_dropped_.fetch_add(1, std::memory_order_relaxed); | ||||
|     } | ||||
|   } else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { | ||||
|     // Scan finished on its own | ||||
|     if (this->scanner_state_ != ScannerState::RUNNING) { | ||||
|       if (this->scanner_state_ == ScannerState::STOPPING) { | ||||
|         ESP_LOGE(TAG, "Scan was not running when scan completed."); | ||||
|       } else if (this->scanner_state_ == ScannerState::STARTING) { | ||||
|         ESP_LOGE(TAG, "Scan was not started when scan completed."); | ||||
|       } else if (this->scanner_state_ == ScannerState::FAILED) { | ||||
|         ESP_LOGE(TAG, "Scan was in failed state when scan completed."); | ||||
|       } else if (this->scanner_state_ == ScannerState::IDLE) { | ||||
|         ESP_LOGE(TAG, "Scan was idle when scan completed."); | ||||
|       } else if (this->scanner_state_ == ScannerState::STOPPED) { | ||||
|         ESP_LOGE(TAG, "Scan was stopped when scan completed."); | ||||
|       } | ||||
|     } | ||||
|     this->set_scanner_state_(ScannerState::STOPPED); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ESP32BLETracker::gap_scan_set_param_complete_(const esp_ble_gap_cb_param_t::ble_scan_param_cmpl_evt_param ¶m) { | ||||
|   ESP_LOGV(TAG, "gap_scan_set_param_complete - status %d", param.status); | ||||
|   if (param.status == ESP_BT_STATUS_DONE) { | ||||
| @@ -444,34 +499,6 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_ | ||||
|   this->set_scanner_state_(ScannerState::STOPPED); | ||||
| } | ||||
|  | ||||
| void ESP32BLETracker::gap_scan_result_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { | ||||
|   ESP_LOGV(TAG, "gap_scan_result - event %d", param.search_evt); | ||||
|   if (param.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) { | ||||
|     if (xSemaphoreTake(this->scan_result_lock_, 0)) { | ||||
|       if (this->scan_result_index_ < ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { | ||||
|         this->scan_result_buffer_[this->scan_result_index_++] = param; | ||||
|       } | ||||
|       xSemaphoreGive(this->scan_result_lock_); | ||||
|     } | ||||
|   } else if (param.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) { | ||||
|     // Scan finished on its own | ||||
|     if (this->scanner_state_ != ScannerState::RUNNING) { | ||||
|       if (this->scanner_state_ == ScannerState::STOPPING) { | ||||
|         ESP_LOGE(TAG, "Scan was not running when scan completed."); | ||||
|       } else if (this->scanner_state_ == ScannerState::STARTING) { | ||||
|         ESP_LOGE(TAG, "Scan was not started when scan completed."); | ||||
|       } else if (this->scanner_state_ == ScannerState::FAILED) { | ||||
|         ESP_LOGE(TAG, "Scan was in failed state when scan completed."); | ||||
|       } else if (this->scanner_state_ == ScannerState::IDLE) { | ||||
|         ESP_LOGE(TAG, "Scan was idle when scan completed."); | ||||
|       } else if (this->scanner_state_ == ScannerState::STOPPED) { | ||||
|         ESP_LOGE(TAG, "Scan was stopped when scan completed."); | ||||
|       } | ||||
|     } | ||||
|     this->set_scanner_state_(ScannerState::STOPPED); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                                           esp_ble_gattc_cb_param_t *param) { | ||||
|   for (auto *client : this->clients_) { | ||||
| @@ -494,13 +521,15 @@ optional<ESPBLEiBeacon> ESPBLEiBeacon::from_manufacturer_data(const ServiceData | ||||
|   return ESPBLEiBeacon(data.data.data()); | ||||
| } | ||||
|  | ||||
| void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { | ||||
|   this->scan_result_ = param; | ||||
| void ESPBTDevice::parse_scan_rst(const BLEScanResult &scan_result) { | ||||
|   for (uint8_t i = 0; i < ESP_BD_ADDR_LEN; i++) | ||||
|     this->address_[i] = param.bda[i]; | ||||
|   this->address_type_ = param.ble_addr_type; | ||||
|   this->rssi_ = param.rssi; | ||||
|   this->parse_adv_(param); | ||||
|     this->address_[i] = scan_result.bda[i]; | ||||
|   this->address_type_ = static_cast<esp_ble_addr_type_t>(scan_result.ble_addr_type); | ||||
|   this->rssi_ = scan_result.rssi; | ||||
|  | ||||
|   // Parse advertisement data directly | ||||
|   uint8_t total_len = scan_result.adv_data_len + scan_result.scan_rsp_len; | ||||
|   this->parse_adv_(scan_result.ble_adv, total_len); | ||||
|  | ||||
| #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE | ||||
|   ESP_LOGVV(TAG, "Parse Result:"); | ||||
| @@ -558,13 +587,13 @@ void ESPBTDevice::parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_e | ||||
|     ESP_LOGVV(TAG, "    Data: %s", format_hex_pretty(data.data).c_str()); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGVV(TAG, "  Adv data: %s", format_hex_pretty(param.ble_adv, param.adv_data_len + param.scan_rsp_len).c_str()); | ||||
|   ESP_LOGVV(TAG, "  Adv data: %s", | ||||
|             format_hex_pretty(scan_result.ble_adv, scan_result.adv_data_len + scan_result.scan_rsp_len).c_str()); | ||||
| #endif | ||||
| } | ||||
| void ESPBTDevice::parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m) { | ||||
|  | ||||
| void ESPBTDevice::parse_adv_(const uint8_t *payload, uint8_t len) { | ||||
|   size_t offset = 0; | ||||
|   const uint8_t *payload = param.ble_adv; | ||||
|   uint8_t len = param.adv_data_len + param.scan_rsp_len; | ||||
|  | ||||
|   while (offset + 2 < len) { | ||||
|     const uint8_t field_length = payload[offset++];  // First byte is length of adv record | ||||
|   | ||||
| @@ -6,6 +6,7 @@ | ||||
| #include "esphome/core/helpers.h" | ||||
|  | ||||
| #include <array> | ||||
| #include <atomic> | ||||
| #include <string> | ||||
| #include <vector> | ||||
|  | ||||
| @@ -62,7 +63,7 @@ class ESPBLEiBeacon { | ||||
|  | ||||
| class ESPBTDevice { | ||||
|  public: | ||||
|   void parse_scan_rst(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); | ||||
|   void parse_scan_rst(const BLEScanResult &scan_result); | ||||
|  | ||||
|   std::string address_str() const; | ||||
|  | ||||
| @@ -84,8 +85,6 @@ class ESPBTDevice { | ||||
|  | ||||
|   const std::vector<ServiceData> &get_service_datas() const { return service_datas_; } | ||||
|  | ||||
|   const esp_ble_gap_cb_param_t::ble_scan_result_evt_param &get_scan_result() const { return scan_result_; } | ||||
|  | ||||
|   bool resolve_irk(const uint8_t *irk) const; | ||||
|  | ||||
|   optional<ESPBLEiBeacon> get_ibeacon() const { | ||||
| @@ -98,7 +97,7 @@ class ESPBTDevice { | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   void parse_adv_(const esp_ble_gap_cb_param_t::ble_scan_result_evt_param ¶m); | ||||
|   void parse_adv_(const uint8_t *payload, uint8_t len); | ||||
|  | ||||
|   esp_bd_addr_t address_{ | ||||
|       0, | ||||
| @@ -112,7 +111,6 @@ class ESPBTDevice { | ||||
|   std::vector<ESPBTUUID> service_uuids_{}; | ||||
|   std::vector<ServiceData> manufacturer_datas_{}; | ||||
|   std::vector<ServiceData> service_datas_{}; | ||||
|   esp_ble_gap_cb_param_t::ble_scan_result_evt_param scan_result_{}; | ||||
| }; | ||||
|  | ||||
| class ESP32BLETracker; | ||||
| @@ -121,9 +119,7 @@ class ESPBTDeviceListener { | ||||
|  public: | ||||
|   virtual void on_scan_end() {} | ||||
|   virtual bool parse_device(const ESPBTDevice &device) = 0; | ||||
|   virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { | ||||
|     return false; | ||||
|   }; | ||||
|   virtual bool parse_devices(const BLEScanResult *scan_results, size_t count) { return false; }; | ||||
|   virtual AdvertisementParserType get_advertisement_parser_type() { | ||||
|     return AdvertisementParserType::PARSED_ADVERTISEMENTS; | ||||
|   }; | ||||
| @@ -133,7 +129,7 @@ class ESPBTDeviceListener { | ||||
|   ESP32BLETracker *parent_{nullptr}; | ||||
| }; | ||||
|  | ||||
| enum class ClientState { | ||||
| enum class ClientState : uint8_t { | ||||
|   // Connection is allocated | ||||
|   INIT, | ||||
|   // Client is disconnecting | ||||
| @@ -169,7 +165,7 @@ enum class ScannerState { | ||||
|   STOPPED, | ||||
| }; | ||||
|  | ||||
| enum class ConnectionType { | ||||
| enum class ConnectionType : uint8_t { | ||||
|   // The default connection type, we hold all the services in ram | ||||
|   // for the duration of the connection. | ||||
|   V1, | ||||
| @@ -197,19 +193,24 @@ class ESPBTClient : public ESPBTDeviceListener { | ||||
|     } | ||||
|   } | ||||
|   ClientState state() const { return state_; } | ||||
|   int app_id; | ||||
|  | ||||
|   // Memory optimized layout | ||||
|   uint8_t app_id;  // App IDs are small integers assigned sequentially | ||||
|  | ||||
|  protected: | ||||
|   // Group 1: 1-byte types | ||||
|   ClientState state_{ClientState::INIT}; | ||||
|   // want_disconnect_ is set to true when a disconnect is requested | ||||
|   // while the client is connecting. This is used to disconnect the | ||||
|   // client as soon as we get the connection id (conn_id_) from the | ||||
|   // ESP_GATTC_OPEN_EVT event. | ||||
|   bool want_disconnect_{false}; | ||||
|   // 2 bytes used, 2 bytes padding | ||||
| }; | ||||
|  | ||||
| class ESP32BLETracker : public Component, | ||||
|                         public GAPEventHandler, | ||||
|                         public GAPScanEventHandler, | ||||
|                         public GATTcEventHandler, | ||||
|                         public BLEStatusEventHandler, | ||||
|                         public Parented<ESP32BLE> { | ||||
| @@ -240,6 +241,7 @@ class ESP32BLETracker : public Component, | ||||
|   void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | ||||
|                            esp_ble_gattc_cb_param_t *param) override; | ||||
|   void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override; | ||||
|   void gap_scan_event_handler(const BLEScanResult &scan_result) override; | ||||
|   void ble_before_disabled_event_handler() override; | ||||
|  | ||||
|   void add_scanner_state_callback(std::function<void(ScannerState)> &&callback) { | ||||
| @@ -264,7 +266,7 @@ class ESP32BLETracker : public Component, | ||||
|   /// Called to set the scanner state. Will also call callbacks to let listeners know when state is changed. | ||||
|   void set_scanner_state_(ScannerState state); | ||||
|  | ||||
|   int app_id_{0}; | ||||
|   uint8_t app_id_{0}; | ||||
|  | ||||
|   /// Vector of addresses that have already been printed in print_bt_device_info | ||||
|   std::vector<uint64_t> already_discovered_; | ||||
| @@ -285,14 +287,16 @@ class ESP32BLETracker : public Component, | ||||
|   bool ble_was_disabled_{true}; | ||||
|   bool raw_advertisements_{false}; | ||||
|   bool parse_advertisements_{false}; | ||||
|   SemaphoreHandle_t scan_result_lock_; | ||||
|   size_t scan_result_index_{0}; | ||||
| #ifdef USE_PSRAM | ||||
|   const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 32; | ||||
| #else | ||||
|   const static u_int8_t SCAN_RESULT_BUFFER_SIZE = 20; | ||||
| #endif  // USE_PSRAM | ||||
|   esp_ble_gap_cb_param_t::ble_scan_result_evt_param *scan_result_buffer_; | ||||
|  | ||||
|   // Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results | ||||
|   // Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler) | ||||
|   // Consumer: ESPHome main loop (loop() method) | ||||
|   // This design ensures zero blocking in the BT callback and prevents scan result loss | ||||
|   BLEScanResult *scan_ring_buffer_; | ||||
|   std::atomic<uint8_t> ring_write_index_{0};       // Written only by BT callback (producer) | ||||
|   std::atomic<uint8_t> ring_read_index_{0};        // Written only by main loop (consumer) | ||||
|   std::atomic<uint16_t> scan_results_dropped_{0};  // Tracks buffer overflow events | ||||
|  | ||||
|   esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS}; | ||||
|   esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS}; | ||||
|   int connecting_{0}; | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| from esphome import automation, pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import i2c | ||||
| from esphome.components.esp32 import add_idf_component | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
| @@ -7,6 +8,7 @@ from esphome.const import ( | ||||
|     CONF_CONTRAST, | ||||
|     CONF_DATA_PINS, | ||||
|     CONF_FREQUENCY, | ||||
|     CONF_I2C_ID, | ||||
|     CONF_ID, | ||||
|     CONF_PIN, | ||||
|     CONF_RESET_PIN, | ||||
| @@ -149,93 +151,104 @@ CONF_ON_IMAGE = "on_image" | ||||
|  | ||||
| camera_range_param = cv.int_range(min=-2, max=2) | ||||
|  | ||||
| CONFIG_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( | ||||
|     { | ||||
|         cv.GenerateID(): cv.declare_id(ESP32Camera), | ||||
|         # pin assignment | ||||
|         cv.Required(CONF_DATA_PINS): cv.All( | ||||
|             [pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8) | ||||
|         ), | ||||
|         cv.Required(CONF_VSYNC_PIN): pins.internal_gpio_input_pin_number, | ||||
|         cv.Required(CONF_HREF_PIN): pins.internal_gpio_input_pin_number, | ||||
|         cv.Required(CONF_PIXEL_CLOCK_PIN): pins.internal_gpio_input_pin_number, | ||||
|         cv.Required(CONF_EXTERNAL_CLOCK): cv.Schema( | ||||
|             { | ||||
|                 cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, | ||||
|                 cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( | ||||
|                     cv.frequency, cv.Range(min=8e6, max=20e6) | ||||
|                 ), | ||||
|             } | ||||
|         ), | ||||
|         cv.Required(CONF_I2C_PINS): cv.Schema( | ||||
|             { | ||||
|                 cv.Required(CONF_SDA): pins.internal_gpio_output_pin_number, | ||||
|                 cv.Required(CONF_SCL): pins.internal_gpio_output_pin_number, | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, | ||||
|         cv.Optional(CONF_POWER_DOWN_PIN): pins.internal_gpio_output_pin_number, | ||||
|         # image | ||||
|         cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( | ||||
|             FRAME_SIZES, upper=True | ||||
|         ), | ||||
|         cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), | ||||
|         cv.Optional(CONF_CONTRAST, default=0): camera_range_param, | ||||
|         cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, | ||||
|         cv.Optional(CONF_SATURATION, default=0): camera_range_param, | ||||
|         cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean, | ||||
|         cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean, | ||||
|         cv.Optional(CONF_SPECIAL_EFFECT, default="NONE"): cv.enum( | ||||
|             ENUM_SPECIAL_EFFECT, upper=True | ||||
|         ), | ||||
|         # exposure | ||||
|         cv.Optional(CONF_AGC_MODE, default="AUTO"): cv.enum( | ||||
|             ENUM_GAIN_CONTROL_MODE, upper=True | ||||
|         ), | ||||
|         cv.Optional(CONF_AEC2, default=False): cv.boolean, | ||||
|         cv.Optional(CONF_AE_LEVEL, default=0): camera_range_param, | ||||
|         cv.Optional(CONF_AEC_VALUE, default=300): cv.int_range(min=0, max=1200), | ||||
|         # gains | ||||
|         cv.Optional(CONF_AEC_MODE, default="AUTO"): cv.enum( | ||||
|             ENUM_GAIN_CONTROL_MODE, upper=True | ||||
|         ), | ||||
|         cv.Optional(CONF_AGC_VALUE, default=0): cv.int_range(min=0, max=30), | ||||
|         cv.Optional(CONF_AGC_GAIN_CEILING, default="2X"): cv.enum( | ||||
|             ENUM_GAIN_CEILING, upper=True | ||||
|         ), | ||||
|         # white balance | ||||
|         cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum(ENUM_WB_MODE, upper=True), | ||||
|         # test pattern | ||||
|         cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean, | ||||
|         # framerates | ||||
|         cv.Optional(CONF_MAX_FRAMERATE, default="10 fps"): cv.All( | ||||
|             cv.framerate, cv.Range(min=0, min_included=False, max=60) | ||||
|         ), | ||||
|         cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( | ||||
|             cv.framerate, cv.Range(min=0, max=1) | ||||
|         ), | ||||
|         cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), | ||||
|         cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
|                     ESP32CameraStreamStartTrigger | ||||
|                 ), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
|                     ESP32CameraStreamStopTrigger | ||||
|                 ), | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_IMAGE): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32CameraImageTrigger), | ||||
|             } | ||||
|         ), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     cv.ENTITY_BASE_SCHEMA.extend( | ||||
|         { | ||||
|             cv.GenerateID(): cv.declare_id(ESP32Camera), | ||||
|             # pin assignment | ||||
|             cv.Required(CONF_DATA_PINS): cv.All( | ||||
|                 [pins.internal_gpio_input_pin_number], cv.Length(min=8, max=8) | ||||
|             ), | ||||
|             cv.Required(CONF_VSYNC_PIN): pins.internal_gpio_input_pin_number, | ||||
|             cv.Required(CONF_HREF_PIN): pins.internal_gpio_input_pin_number, | ||||
|             cv.Required(CONF_PIXEL_CLOCK_PIN): pins.internal_gpio_input_pin_number, | ||||
|             cv.Required(CONF_EXTERNAL_CLOCK): cv.Schema( | ||||
|                 { | ||||
|                     cv.Required(CONF_PIN): pins.internal_gpio_input_pin_number, | ||||
|                     cv.Optional(CONF_FREQUENCY, default="20MHz"): cv.All( | ||||
|                         cv.frequency, cv.Range(min=8e6, max=20e6) | ||||
|                     ), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_I2C_PINS): cv.Schema( | ||||
|                 { | ||||
|                     cv.Required(CONF_SDA): pins.internal_gpio_output_pin_number, | ||||
|                     cv.Required(CONF_SCL): pins.internal_gpio_output_pin_number, | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_I2C_ID): cv.Any( | ||||
|                 cv.use_id(i2c.InternalI2CBus), | ||||
|                 msg="I2C bus must be an internal ESP32 I2C bus", | ||||
|             ), | ||||
|             cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number, | ||||
|             cv.Optional(CONF_POWER_DOWN_PIN): pins.internal_gpio_output_pin_number, | ||||
|             # image | ||||
|             cv.Optional(CONF_RESOLUTION, default="640X480"): cv.enum( | ||||
|                 FRAME_SIZES, upper=True | ||||
|             ), | ||||
|             cv.Optional(CONF_JPEG_QUALITY, default=10): cv.int_range(min=6, max=63), | ||||
|             cv.Optional(CONF_CONTRAST, default=0): camera_range_param, | ||||
|             cv.Optional(CONF_BRIGHTNESS, default=0): camera_range_param, | ||||
|             cv.Optional(CONF_SATURATION, default=0): camera_range_param, | ||||
|             cv.Optional(CONF_VERTICAL_FLIP, default=True): cv.boolean, | ||||
|             cv.Optional(CONF_HORIZONTAL_MIRROR, default=True): cv.boolean, | ||||
|             cv.Optional(CONF_SPECIAL_EFFECT, default="NONE"): cv.enum( | ||||
|                 ENUM_SPECIAL_EFFECT, upper=True | ||||
|             ), | ||||
|             # exposure | ||||
|             cv.Optional(CONF_AGC_MODE, default="AUTO"): cv.enum( | ||||
|                 ENUM_GAIN_CONTROL_MODE, upper=True | ||||
|             ), | ||||
|             cv.Optional(CONF_AEC2, default=False): cv.boolean, | ||||
|             cv.Optional(CONF_AE_LEVEL, default=0): camera_range_param, | ||||
|             cv.Optional(CONF_AEC_VALUE, default=300): cv.int_range(min=0, max=1200), | ||||
|             # gains | ||||
|             cv.Optional(CONF_AEC_MODE, default="AUTO"): cv.enum( | ||||
|                 ENUM_GAIN_CONTROL_MODE, upper=True | ||||
|             ), | ||||
|             cv.Optional(CONF_AGC_VALUE, default=0): cv.int_range(min=0, max=30), | ||||
|             cv.Optional(CONF_AGC_GAIN_CEILING, default="2X"): cv.enum( | ||||
|                 ENUM_GAIN_CEILING, upper=True | ||||
|             ), | ||||
|             # white balance | ||||
|             cv.Optional(CONF_WB_MODE, default="AUTO"): cv.enum( | ||||
|                 ENUM_WB_MODE, upper=True | ||||
|             ), | ||||
|             # test pattern | ||||
|             cv.Optional(CONF_TEST_PATTERN, default=False): cv.boolean, | ||||
|             # framerates | ||||
|             cv.Optional(CONF_MAX_FRAMERATE, default="10 fps"): cv.All( | ||||
|                 cv.framerate, cv.Range(min=0, min_included=False, max=60) | ||||
|             ), | ||||
|             cv.Optional(CONF_IDLE_FRAMERATE, default="0.1 fps"): cv.All( | ||||
|                 cv.framerate, cv.Range(min=0, max=1) | ||||
|             ), | ||||
|             cv.Optional(CONF_FRAME_BUFFER_COUNT, default=1): cv.int_range(min=1, max=2), | ||||
|             cv.Optional(CONF_ON_STREAM_START): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
|                         ESP32CameraStreamStartTrigger | ||||
|                     ), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_STREAM_STOP): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
|                         ESP32CameraStreamStopTrigger | ||||
|                     ), | ||||
|                 } | ||||
|             ), | ||||
|             cv.Optional(CONF_ON_IMAGE): automation.validate_automation( | ||||
|                 { | ||||
|                     cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
|                         ESP32CameraImageTrigger | ||||
|                     ), | ||||
|                 } | ||||
|             ), | ||||
|         } | ||||
|     ).extend(cv.COMPONENT_SCHEMA), | ||||
|     cv.has_exactly_one_key(CONF_I2C_PINS, CONF_I2C_ID), | ||||
| ) | ||||
|  | ||||
| SETTERS = { | ||||
|     # pin assignment | ||||
| @@ -280,8 +293,12 @@ async def to_code(config): | ||||
|  | ||||
|     extclk = config[CONF_EXTERNAL_CLOCK] | ||||
|     cg.add(var.set_external_clock(extclk[CONF_PIN], extclk[CONF_FREQUENCY])) | ||||
|     i2c_pins = config[CONF_I2C_PINS] | ||||
|     cg.add(var.set_i2c_pins(i2c_pins[CONF_SDA], i2c_pins[CONF_SCL])) | ||||
|     if i2c_id := config.get(CONF_I2C_ID): | ||||
|         i2c_hub = await cg.get_variable(i2c_id) | ||||
|         cg.add(var.set_i2c_id(i2c_hub)) | ||||
|     else: | ||||
|         i2c_pins = config[CONF_I2C_PINS] | ||||
|         cg.add(var.set_i2c_pins(i2c_pins[CONF_SDA], i2c_pins[CONF_SCL])) | ||||
|     cg.add(var.set_max_update_interval(1000 / config[CONF_MAX_FRAMERATE])) | ||||
|     if config[CONF_IDLE_FRAMERATE] == 0: | ||||
|         cg.add(var.set_idle_update_interval(0)) | ||||
|   | ||||
| @@ -1,9 +1,9 @@ | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include "esp32_camera.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include "esphome/core/log.h" | ||||
|  | ||||
| #include <freertos/task.h> | ||||
|  | ||||
| @@ -16,6 +16,12 @@ static const char *const TAG = "esp32_camera"; | ||||
| void ESP32Camera::setup() { | ||||
|   global_esp32_camera = this; | ||||
|  | ||||
| #ifdef USE_I2C | ||||
|   if (this->i2c_bus_ != nullptr) { | ||||
|     this->config_.sccb_i2c_port = this->i2c_bus_->get_port(); | ||||
|   } | ||||
| #endif | ||||
|  | ||||
|   /* initialize time to now */ | ||||
|   this->last_update_ = millis(); | ||||
|  | ||||
| @@ -57,7 +63,7 @@ void ESP32Camera::dump_config() { | ||||
|                 "  External Clock: Pin:%d Frequency:%u\n" | ||||
|                 "  I2C Pins: SDA:%d SCL:%d\n" | ||||
|                 "  Reset Pin: %d", | ||||
|                 this->name_.c_str(), YESNO(this->internal_), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, | ||||
|                 this->name_.c_str(), YESNO(this->is_internal()), conf.pin_d0, conf.pin_d1, conf.pin_d2, conf.pin_d3, | ||||
|                 conf.pin_d4, conf.pin_d5, conf.pin_d6, conf.pin_d7, conf.pin_vsync, conf.pin_href, conf.pin_pclk, | ||||
|                 conf.pin_xclk, conf.xclk_freq_hz, conf.pin_sccb_sda, conf.pin_sccb_scl, conf.pin_reset); | ||||
|   switch (this->config_.frame_size) { | ||||
| @@ -246,6 +252,13 @@ void ESP32Camera::set_i2c_pins(uint8_t sda, uint8_t scl) { | ||||
|   this->config_.pin_sccb_sda = sda; | ||||
|   this->config_.pin_sccb_scl = scl; | ||||
| } | ||||
| #ifdef USE_I2C | ||||
| void ESP32Camera::set_i2c_id(i2c::InternalI2CBus *i2c_bus) { | ||||
|   this->i2c_bus_ = i2c_bus; | ||||
|   this->config_.pin_sccb_sda = -1; | ||||
|   this->config_.pin_sccb_scl = -1; | ||||
| } | ||||
| #endif  // USE_I2C | ||||
| void ESP32Camera::set_reset_pin(uint8_t pin) { this->config_.pin_reset = pin; } | ||||
| void ESP32Camera::set_power_down_pin(uint8_t pin) { this->config_.pin_pwdn = pin; } | ||||
|  | ||||
|   | ||||
| @@ -2,13 +2,17 @@ | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #include <esp_camera.h> | ||||
| #include <freertos/FreeRTOS.h> | ||||
| #include <freertos/queue.h> | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/core/entity_base.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include <esp_camera.h> | ||||
| #include <freertos/FreeRTOS.h> | ||||
| #include <freertos/queue.h> | ||||
|  | ||||
| #ifdef USE_I2C | ||||
| #include "esphome/components/i2c/i2c_bus.h" | ||||
| #endif  // USE_I2C | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_camera { | ||||
| @@ -118,6 +122,9 @@ class ESP32Camera : public EntityBase, public Component { | ||||
|   void set_pixel_clock_pin(uint8_t pin); | ||||
|   void set_external_clock(uint8_t pin, uint32_t frequency); | ||||
|   void set_i2c_pins(uint8_t sda, uint8_t scl); | ||||
| #ifdef USE_I2C | ||||
|   void set_i2c_id(i2c::InternalI2CBus *i2c_bus); | ||||
| #endif  // USE_I2C | ||||
|   void set_reset_pin(uint8_t pin); | ||||
|   void set_power_down_pin(uint8_t pin); | ||||
|   /* -- image */ | ||||
| @@ -210,6 +217,9 @@ class ESP32Camera : public EntityBase, public Component { | ||||
|  | ||||
|   uint32_t last_idle_request_{0}; | ||||
|   uint32_t last_update_{0}; | ||||
| #ifdef USE_I2C | ||||
|   i2c::InternalI2CBus *i2c_bus_{nullptr}; | ||||
| #endif  // USE_I2C | ||||
| }; | ||||
|  | ||||
| // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, CONF_MODE, CONF_PORT | ||||
|  | ||||
| CODEOWNERS = ["@ayufan"] | ||||
| DEPENDENCIES = ["esp32_camera"] | ||||
| DEPENDENCIES = ["esp32_camera", "network"] | ||||
| MULTI_CONF = True | ||||
|  | ||||
| esp32_camera_web_server_ns = cg.esphome_ns.namespace("esp32_camera_web_server") | ||||
|   | ||||
| @@ -1,25 +0,0 @@ | ||||
| #ifdef USE_ESP32 | ||||
| #include "esp32_hall.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include "esphome/core/hal.h" | ||||
| #include <driver/adc.h> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_hall { | ||||
|  | ||||
| static const char *const TAG = "esp32_hall"; | ||||
|  | ||||
| void ESP32HallSensor::update() { | ||||
|   adc1_config_width(ADC_WIDTH_BIT_12); | ||||
|   int value_int = hall_sensor_read(); | ||||
|   float value = (value_int / 4095.0f) * 10000.0f; | ||||
|   ESP_LOGD(TAG, "'%s': Got reading %.0f µT", this->name_.c_str(), value); | ||||
|   this->publish_state(value); | ||||
| } | ||||
| std::string ESP32HallSensor::unique_id() { return get_mac_address() + "-hall"; } | ||||
| void ESP32HallSensor::dump_config() { LOG_SENSOR("", "ESP32 Hall Sensor", this); } | ||||
|  | ||||
| }  // namespace esp32_hall | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -1,23 +0,0 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/sensor/sensor.h" | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_hall { | ||||
|  | ||||
| class ESP32HallSensor : public sensor::Sensor, public PollingComponent { | ||||
|  public: | ||||
|   void dump_config() override; | ||||
|  | ||||
|   void update() override; | ||||
|  | ||||
|   std::string unique_id() override; | ||||
| }; | ||||
|  | ||||
| }  // namespace esp32_hall | ||||
| }  // namespace esphome | ||||
|  | ||||
| #endif | ||||
| @@ -1,24 +0,0 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ICON_MAGNET, STATE_CLASS_MEASUREMENT, UNIT_MICROTESLA | ||||
|  | ||||
| DEPENDENCIES = ["esp32"] | ||||
|  | ||||
| esp32_hall_ns = cg.esphome_ns.namespace("esp32_hall") | ||||
| ESP32HallSensor = esp32_hall_ns.class_( | ||||
|     "ESP32HallSensor", sensor.Sensor, cg.PollingComponent | ||||
| ) | ||||
|  | ||||
| CONFIG_SCHEMA = sensor.sensor_schema( | ||||
|     ESP32HallSensor, | ||||
|     unit_of_measurement=UNIT_MICROTESLA, | ||||
|     icon=ICON_MAGNET, | ||||
|     accuracy_decimals=1, | ||||
|     state_class=STATE_CLASS_MEASUREMENT, | ||||
| ).extend(cv.polling_component_schema("60s")) | ||||
|  | ||||
|  | ||||
| async def to_code(config): | ||||
|     var = await sensor.new_sensor(config) | ||||
|     await cg.register_component(var, config) | ||||
| @@ -168,6 +168,8 @@ void ESP32ImprovComponent::loop() { | ||||
|     case improv::STATE_PROVISIONED: { | ||||
|       this->incoming_data_.clear(); | ||||
|       this->set_status_indicator_state_(false); | ||||
|       // Provisioning complete, no further loop execution needed | ||||
|       this->disable_loop(); | ||||
|       break; | ||||
|     } | ||||
|   } | ||||
| @@ -254,6 +256,7 @@ void ESP32ImprovComponent::start() { | ||||
|  | ||||
|   ESP_LOGD(TAG, "Setting Improv to start"); | ||||
|   this->should_start_ = true; | ||||
|   this->enable_loop(); | ||||
| } | ||||
|  | ||||
| void ESP32ImprovComponent::stop() { | ||||
|   | ||||
| @@ -1,48 +1,8 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import esp32 | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import KEY_CORE, KEY_FRAMEWORK_VERSION | ||||
| from esphome.core import CORE | ||||
|  | ||||
| CODEOWNERS = ["@jesserockz"] | ||||
|  | ||||
| RMT_TX_CHANNELS = { | ||||
|     esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7], | ||||
|     esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3], | ||||
|     esp32.const.VARIANT_ESP32S3: [0, 1, 2, 3], | ||||
|     esp32.const.VARIANT_ESP32C3: [0, 1], | ||||
|     esp32.const.VARIANT_ESP32C6: [0, 1], | ||||
|     esp32.const.VARIANT_ESP32H2: [0, 1], | ||||
| } | ||||
|  | ||||
| RMT_RX_CHANNELS = { | ||||
|     esp32.const.VARIANT_ESP32: [0, 1, 2, 3, 4, 5, 6, 7], | ||||
|     esp32.const.VARIANT_ESP32S2: [0, 1, 2, 3], | ||||
|     esp32.const.VARIANT_ESP32S3: [4, 5, 6, 7], | ||||
|     esp32.const.VARIANT_ESP32C3: [2, 3], | ||||
|     esp32.const.VARIANT_ESP32C6: [2, 3], | ||||
|     esp32.const.VARIANT_ESP32H2: [2, 3], | ||||
| } | ||||
|  | ||||
| rmt_channel_t = cg.global_ns.enum("rmt_channel_t") | ||||
| RMT_CHANNEL_ENUMS = { | ||||
|     0: rmt_channel_t.RMT_CHANNEL_0, | ||||
|     1: rmt_channel_t.RMT_CHANNEL_1, | ||||
|     2: rmt_channel_t.RMT_CHANNEL_2, | ||||
|     3: rmt_channel_t.RMT_CHANNEL_3, | ||||
|     4: rmt_channel_t.RMT_CHANNEL_4, | ||||
|     5: rmt_channel_t.RMT_CHANNEL_5, | ||||
|     6: rmt_channel_t.RMT_CHANNEL_6, | ||||
|     7: rmt_channel_t.RMT_CHANNEL_7, | ||||
| } | ||||
|  | ||||
|  | ||||
| def use_new_rmt_driver(): | ||||
|     framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] | ||||
|     if CORE.using_esp_idf and framework_version >= cv.Version(5, 0, 0): | ||||
|         return True | ||||
|     return False | ||||
|  | ||||
|  | ||||
| def validate_clock_resolution(): | ||||
|     def _validator(value): | ||||
| @@ -60,21 +20,3 @@ def validate_clock_resolution(): | ||||
|         return value | ||||
|  | ||||
|     return _validator | ||||
|  | ||||
|  | ||||
| def validate_rmt_channel(*, tx: bool): | ||||
|     rmt_channels = RMT_TX_CHANNELS if tx else RMT_RX_CHANNELS | ||||
|  | ||||
|     def _validator(value): | ||||
|         cv.only_on_esp32(value) | ||||
|         value = cv.int_(value) | ||||
|         variant = esp32.get_esp32_variant() | ||||
|         if variant not in rmt_channels: | ||||
|             raise cv.Invalid(f"ESP32 variant {variant} does not support RMT.") | ||||
|         if value not in rmt_channels[variant]: | ||||
|             raise cv.Invalid( | ||||
|                 f"RMT channel {value} does not support {'transmitting' if tx else 'receiving'} for ESP32 variant {variant}." | ||||
|             ) | ||||
|         return cv.enum(RMT_CHANNEL_ENUMS)(value) | ||||
|  | ||||
|     return _validator | ||||
|   | ||||
| @@ -42,7 +42,6 @@ void ESP32RMTLEDStripLightOutput::setup() { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
| #if ESP_IDF_VERSION_MAJOR >= 5 | ||||
|   RAMAllocator<rmt_symbol_word_t> rmt_allocator(this->use_psram_ ? 0 : RAMAllocator<rmt_symbol_word_t>::ALLOC_INTERNAL); | ||||
|  | ||||
|   // 8 bits per byte, 1 rmt_symbol_word_t per bit + 1 rmt_symbol_word_t for reset | ||||
| @@ -79,36 +78,6 @@ void ESP32RMTLEDStripLightOutput::setup() { | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
| #else | ||||
|   RAMAllocator<rmt_item32_t> rmt_allocator(this->use_psram_ ? 0 : RAMAllocator<rmt_item32_t>::ALLOC_INTERNAL); | ||||
|  | ||||
|   // 8 bits per byte, 1 rmt_item32_t per bit + 1 rmt_item32_t for reset | ||||
|   this->rmt_buf_ = rmt_allocator.allocate(buffer_size * 8 + 1); | ||||
|  | ||||
|   rmt_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.channel = this->channel_; | ||||
|   config.rmt_mode = RMT_MODE_TX; | ||||
|   config.gpio_num = gpio_num_t(this->pin_); | ||||
|   config.mem_block_num = 1; | ||||
|   config.clk_div = RMT_CLK_DIV; | ||||
|   config.tx_config.loop_en = false; | ||||
|   config.tx_config.carrier_level = RMT_CARRIER_LEVEL_LOW; | ||||
|   config.tx_config.carrier_en = false; | ||||
|   config.tx_config.idle_level = RMT_IDLE_LEVEL_LOW; | ||||
|   config.tx_config.idle_output_en = true; | ||||
|  | ||||
|   if (rmt_config(&config) != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "Cannot initialize RMT!"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   if (rmt_driver_install(config.channel, 0, 0) != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "Cannot install RMT driver!"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high, | ||||
| @@ -145,11 +114,7 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { | ||||
|  | ||||
|   ESP_LOGVV(TAG, "Writing RGB values to bus"); | ||||
|  | ||||
| #if ESP_IDF_VERSION_MAJOR >= 5 | ||||
|   esp_err_t error = rmt_tx_wait_all_done(this->channel_, 1000); | ||||
| #else | ||||
|   esp_err_t error = rmt_wait_tx_done(this->channel_, pdMS_TO_TICKS(1000)); | ||||
| #endif | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "RMT TX timeout"); | ||||
|     this->status_set_warning(); | ||||
| @@ -162,11 +127,7 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { | ||||
|   size_t size = 0; | ||||
|   size_t len = 0; | ||||
|   uint8_t *psrc = this->buf_; | ||||
| #if ESP_IDF_VERSION_MAJOR >= 5 | ||||
|   rmt_symbol_word_t *pdest = this->rmt_buf_; | ||||
| #else | ||||
|   rmt_item32_t *pdest = this->rmt_buf_; | ||||
| #endif | ||||
|   while (size < buffer_size) { | ||||
|     uint8_t b = *psrc; | ||||
|     for (int i = 0; i < 8; i++) { | ||||
| @@ -184,15 +145,11 @@ void ESP32RMTLEDStripLightOutput::write_state(light::LightState *state) { | ||||
|     len++; | ||||
|   } | ||||
|  | ||||
| #if ESP_IDF_VERSION_MAJOR >= 5 | ||||
|   rmt_transmit_config_t config; | ||||
|   memset(&config, 0, sizeof(config)); | ||||
|   config.loop_count = 0; | ||||
|   config.flags.eot_level = 0; | ||||
|   error = rmt_transmit(this->channel_, this->encoder_, this->rmt_buf_, len * sizeof(rmt_symbol_word_t), &config); | ||||
| #else | ||||
|   error = rmt_write_items(this->channel_, this->rmt_buf_, len, false); | ||||
| #endif | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "RMT TX error"); | ||||
|     this->status_set_warning(); | ||||
| @@ -251,11 +208,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() { | ||||
|                 "ESP32 RMT LED Strip:\n" | ||||
|                 "  Pin: %u", | ||||
|                 this->pin_); | ||||
| #if ESP_IDF_VERSION_MAJOR >= 5 | ||||
|   ESP_LOGCONFIG(TAG, "  RMT Symbols: %" PRIu32, this->rmt_symbols_); | ||||
| #else | ||||
|   ESP_LOGCONFIG(TAG, "  Channel: %u", this->channel_); | ||||
| #endif | ||||
|   const char *rgb_order; | ||||
|   switch (this->rgb_order_) { | ||||
|     case ORDER_RGB: | ||||
|   | ||||
| @@ -11,12 +11,7 @@ | ||||
| #include <driver/gpio.h> | ||||
| #include <esp_err.h> | ||||
| #include <esp_idf_version.h> | ||||
|  | ||||
| #if ESP_IDF_VERSION_MAJOR >= 5 | ||||
| #include <driver/rmt_tx.h> | ||||
| #else | ||||
| #include <driver/rmt.h> | ||||
| #endif | ||||
|  | ||||
| namespace esphome { | ||||
| namespace esp32_rmt_led_strip { | ||||
| @@ -61,11 +56,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { | ||||
|                       uint32_t reset_time_high, uint32_t reset_time_low); | ||||
|  | ||||
|   void set_rgb_order(RGBOrder rgb_order) { this->rgb_order_ = rgb_order; } | ||||
| #if ESP_IDF_VERSION_MAJOR >= 5 | ||||
|   void set_rmt_symbols(uint32_t rmt_symbols) { this->rmt_symbols_ = rmt_symbols; } | ||||
| #else | ||||
|   void set_rmt_channel(rmt_channel_t channel) { this->channel_ = channel; } | ||||
| #endif | ||||
|  | ||||
|   void clear_effect_data() override { | ||||
|     for (int i = 0; i < this->size(); i++) | ||||
| @@ -81,17 +72,11 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight { | ||||
|  | ||||
|   uint8_t *buf_{nullptr}; | ||||
|   uint8_t *effect_data_{nullptr}; | ||||
| #if ESP_IDF_VERSION_MAJOR >= 5 | ||||
|   rmt_channel_handle_t channel_{nullptr}; | ||||
|   rmt_encoder_handle_t encoder_{nullptr}; | ||||
|   rmt_symbol_word_t *rmt_buf_{nullptr}; | ||||
|   rmt_symbol_word_t bit0_, bit1_, reset_; | ||||
|   uint32_t rmt_symbols_{48}; | ||||
| #else | ||||
|   rmt_item32_t *rmt_buf_{nullptr}; | ||||
|   rmt_item32_t bit0_, bit1_, reset_; | ||||
|   rmt_channel_t channel_{RMT_CHANNEL_0}; | ||||
| #endif | ||||
|  | ||||
|   uint8_t pin_; | ||||
|   uint16_t num_leds_; | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import logging | ||||
|  | ||||
| from esphome import pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import esp32, esp32_rmt, light | ||||
| from esphome.components import esp32, light | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_CHIPSET, | ||||
| @@ -13,11 +13,9 @@ from esphome.const import ( | ||||
|     CONF_OUTPUT_ID, | ||||
|     CONF_PIN, | ||||
|     CONF_RGB_ORDER, | ||||
|     CONF_RMT_CHANNEL, | ||||
|     CONF_RMT_SYMBOLS, | ||||
|     CONF_USE_DMA, | ||||
| ) | ||||
| from esphome.core import CORE | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| @@ -69,53 +67,6 @@ CONF_RESET_HIGH = "reset_high" | ||||
| CONF_RESET_LOW = "reset_low" | ||||
|  | ||||
|  | ||||
| class OptionalForIDF5(cv.SplitDefault): | ||||
|     @property | ||||
|     def default(self): | ||||
|         if not esp32_rmt.use_new_rmt_driver(): | ||||
|             return cv.UNDEFINED | ||||
|         return super().default | ||||
|  | ||||
|     @default.setter | ||||
|     def default(self, value): | ||||
|         # Ignore default set from vol.Optional | ||||
|         pass | ||||
|  | ||||
|  | ||||
| def only_with_new_rmt_driver(obj): | ||||
|     if not esp32_rmt.use_new_rmt_driver(): | ||||
|         raise cv.Invalid( | ||||
|             "This feature is only available for the IDF framework version 5." | ||||
|         ) | ||||
|     return obj | ||||
|  | ||||
|  | ||||
| def not_with_new_rmt_driver(obj): | ||||
|     if esp32_rmt.use_new_rmt_driver(): | ||||
|         raise cv.Invalid( | ||||
|             "This feature is not available for the IDF framework version 5." | ||||
|         ) | ||||
|     return obj | ||||
|  | ||||
|  | ||||
| def final_validation(config): | ||||
|     if not esp32_rmt.use_new_rmt_driver(): | ||||
|         if CONF_RMT_CHANNEL not in config: | ||||
|             if CORE.using_esp_idf: | ||||
|                 raise cv.Invalid( | ||||
|                     "rmt_channel is a required option for IDF version < 5." | ||||
|                 ) | ||||
|             raise cv.Invalid( | ||||
|                 "rmt_channel is a required option for the Arduino framework." | ||||
|             ) | ||||
|         _LOGGER.warning( | ||||
|             "RMT_LED_STRIP support for IDF version < 5 is deprecated and will be removed soon." | ||||
|         ) | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = final_validation | ||||
|  | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|     light.ADDRESSABLE_LIGHT_SCHEMA.extend( | ||||
|         { | ||||
| @@ -123,20 +74,17 @@ CONFIG_SCHEMA = cv.All( | ||||
|             cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number, | ||||
|             cv.Required(CONF_NUM_LEDS): cv.positive_not_null_int, | ||||
|             cv.Required(CONF_RGB_ORDER): cv.enum(RGB_ORDERS, upper=True), | ||||
|             cv.Optional(CONF_RMT_CHANNEL): cv.All( | ||||
|                 not_with_new_rmt_driver, esp32_rmt.validate_rmt_channel(tx=True) | ||||
|             ), | ||||
|             OptionalForIDF5( | ||||
|             cv.SplitDefault( | ||||
|                 CONF_RMT_SYMBOLS, | ||||
|                 esp32_idf=192, | ||||
|                 esp32_s2_idf=192, | ||||
|                 esp32_s3_idf=192, | ||||
|                 esp32_p4_idf=192, | ||||
|                 esp32_c3_idf=96, | ||||
|                 esp32_c5_idf=96, | ||||
|                 esp32_c6_idf=96, | ||||
|                 esp32_h2_idf=96, | ||||
|             ): cv.All(only_with_new_rmt_driver, cv.int_range(min=2)), | ||||
|                 esp32=192, | ||||
|                 esp32_s2=192, | ||||
|                 esp32_s3=192, | ||||
|                 esp32_p4=192, | ||||
|                 esp32_c3=96, | ||||
|                 esp32_c5=96, | ||||
|                 esp32_c6=96, | ||||
|                 esp32_h2=96, | ||||
|             ): cv.int_range(min=2), | ||||
|             cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds, | ||||
|             cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True), | ||||
|             cv.Optional(CONF_IS_RGBW, default=False): cv.boolean, | ||||
| @@ -145,7 +93,6 @@ CONFIG_SCHEMA = cv.All( | ||||
|                 esp32.only_on_variant( | ||||
|                     supported=[esp32.const.VARIANT_ESP32S3, esp32.const.VARIANT_ESP32P4] | ||||
|                 ), | ||||
|                 cv.only_with_esp_idf, | ||||
|                 cv.boolean, | ||||
|             ), | ||||
|             cv.Optional(CONF_USE_PSRAM, default=True): cv.boolean, | ||||
| @@ -218,15 +165,6 @@ async def to_code(config): | ||||
|     cg.add(var.set_is_rgbw(config[CONF_IS_RGBW])) | ||||
|     cg.add(var.set_is_wrgb(config[CONF_IS_WRGB])) | ||||
|     cg.add(var.set_use_psram(config[CONF_USE_PSRAM])) | ||||
|  | ||||
|     if esp32_rmt.use_new_rmt_driver(): | ||||
|         cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) | ||||
|         if CONF_USE_DMA in config: | ||||
|             cg.add(var.set_use_dma(config[CONF_USE_DMA])) | ||||
|     else: | ||||
|         rmt_channel_t = cg.global_ns.enum("rmt_channel_t") | ||||
|         cg.add( | ||||
|             var.set_rmt_channel( | ||||
|                 getattr(rmt_channel_t, f"RMT_CHANNEL_{config[CONF_RMT_CHANNEL]}") | ||||
|             ) | ||||
|         ) | ||||
|     cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) | ||||
|     if CONF_USE_DMA in config: | ||||
|         cg.add(var.set_use_dma(config[CONF_USE_DMA])) | ||||
|   | ||||
| @@ -183,6 +183,7 @@ async def to_code(config): | ||||
|  | ||||
|     cg.add_platformio_option("board", config[CONF_BOARD]) | ||||
|     cg.add_build_flag("-DUSE_ESP8266") | ||||
|     cg.set_cpp_standard("gnu++17") | ||||
|     cg.add_define("ESPHOME_BOARD", config[CONF_BOARD]) | ||||
|     cg.add_define("ESPHOME_VARIANT", "ESP8266") | ||||
|  | ||||
|   | ||||
| @@ -26,19 +26,19 @@ void ESPHomeOTAComponent::setup() { | ||||
|   ota::register_ota_platform(this); | ||||
| #endif | ||||
|  | ||||
|   server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0);  // monitored for incoming connections | ||||
|   if (server_ == nullptr) { | ||||
|   this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0);  // monitored for incoming connections | ||||
|   if (this->server_ == nullptr) { | ||||
|     ESP_LOGW(TAG, "Could not create socket"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   int enable = 1; | ||||
|   int err = server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); | ||||
|   int err = this->server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); | ||||
|   if (err != 0) { | ||||
|     ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); | ||||
|     // we can still continue | ||||
|   } | ||||
|   err = server_->setblocking(false); | ||||
|   err = this->server_->setblocking(false); | ||||
|   if (err != 0) { | ||||
|     ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); | ||||
|     this->mark_failed(); | ||||
| @@ -54,14 +54,14 @@ void ESPHomeOTAComponent::setup() { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   err = server_->bind((struct sockaddr *) &server, sizeof(server)); | ||||
|   err = this->server_->bind((struct sockaddr *) &server, sizeof(server)); | ||||
|   if (err != 0) { | ||||
|     ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   err = server_->listen(4); | ||||
|   err = this->server_->listen(4); | ||||
|   if (err != 0) { | ||||
|     ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno); | ||||
|     this->mark_failed(); | ||||
| @@ -82,7 +82,14 @@ void ESPHomeOTAComponent::dump_config() { | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void ESPHomeOTAComponent::loop() { this->handle_(); } | ||||
| void ESPHomeOTAComponent::loop() { | ||||
|   // Skip handle_() call if no client connected and no incoming connections | ||||
|   // This optimization reduces idle loop overhead when OTA is not active | ||||
|   // Note: No need to check server_ for null as the component is marked failed in setup() if server_ creation fails | ||||
|   if (this->client_ != nullptr || this->server_->ready()) { | ||||
|     this->handle_(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01; | ||||
|  | ||||
| @@ -101,23 +108,21 @@ void ESPHomeOTAComponent::handle_() { | ||||
|   size_t size_acknowledged = 0; | ||||
| #endif | ||||
|  | ||||
|   if (client_ == nullptr) { | ||||
|     // Check if the server socket is ready before accepting | ||||
|     if (this->server_->ready()) { | ||||
|       struct sockaddr_storage source_addr; | ||||
|       socklen_t addr_len = sizeof(source_addr); | ||||
|       client_ = server_->accept((struct sockaddr *) &source_addr, &addr_len); | ||||
|     } | ||||
|   if (this->client_ == nullptr) { | ||||
|     // We already checked server_->ready() in loop(), so we can accept directly | ||||
|     struct sockaddr_storage source_addr; | ||||
|     socklen_t addr_len = sizeof(source_addr); | ||||
|     this->client_ = this->server_->accept((struct sockaddr *) &source_addr, &addr_len); | ||||
|     if (this->client_ == nullptr) | ||||
|       return; | ||||
|   } | ||||
|   if (client_ == nullptr) | ||||
|     return; | ||||
|  | ||||
|   int enable = 1; | ||||
|   int err = client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); | ||||
|   int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int)); | ||||
|   if (err != 0) { | ||||
|     ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno); | ||||
|     client_->close(); | ||||
|     client_ = nullptr; | ||||
|     this->client_->close(); | ||||
|     this->client_ = nullptr; | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -106,7 +106,7 @@ void EthernetComponent::setup() { | ||||
|       .post_cb = nullptr, | ||||
|   }; | ||||
|  | ||||
| #if USE_ESP_IDF && (ESP_IDF_VERSION_MAJOR >= 5) | ||||
| #if ESP_IDF_VERSION_MAJOR >= 5 | ||||
|   eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg); | ||||
| #else | ||||
|   spi_device_handle_t spi_handle = nullptr; | ||||
| @@ -274,6 +274,9 @@ void EthernetComponent::loop() { | ||||
|         ESP_LOGW(TAG, "Connection lost; reconnecting"); | ||||
|         this->state_ = EthernetComponentState::CONNECTING; | ||||
|         this->start_connect_(); | ||||
|       } else { | ||||
|         // When connected and stable, disable the loop to save CPU cycles | ||||
|         this->disable_loop(); | ||||
|       } | ||||
|       break; | ||||
|   } | ||||
| @@ -397,11 +400,13 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base | ||||
|     case ETHERNET_EVENT_START: | ||||
|       event_name = "ETH started"; | ||||
|       global_eth_component->started_ = true; | ||||
|       global_eth_component->enable_loop_soon_any_context(); | ||||
|       break; | ||||
|     case ETHERNET_EVENT_STOP: | ||||
|       event_name = "ETH stopped"; | ||||
|       global_eth_component->started_ = false; | ||||
|       global_eth_component->connected_ = false; | ||||
|       global_eth_component->enable_loop_soon_any_context();  // Enable loop when connection state changes | ||||
|       break; | ||||
|     case ETHERNET_EVENT_CONNECTED: | ||||
|       event_name = "ETH connected"; | ||||
| @@ -409,6 +414,7 @@ void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base | ||||
|     case ETHERNET_EVENT_DISCONNECTED: | ||||
|       event_name = "ETH disconnected"; | ||||
|       global_eth_component->connected_ = false; | ||||
|       global_eth_component->enable_loop_soon_any_context();  // Enable loop when connection state changes | ||||
|       break; | ||||
|     default: | ||||
|       return; | ||||
| @@ -425,8 +431,10 @@ void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_b | ||||
|   global_eth_component->got_ipv4_address_ = true; | ||||
| #if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) | ||||
|   global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT; | ||||
|   global_eth_component->enable_loop_soon_any_context();  // Enable loop when connection state changes | ||||
| #else | ||||
|   global_eth_component->connected_ = true; | ||||
|   global_eth_component->enable_loop_soon_any_context();  // Enable loop when connection state changes | ||||
| #endif /* USE_NETWORK_IPV6 */ | ||||
| } | ||||
|  | ||||
| @@ -439,8 +447,10 @@ void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_ | ||||
| #if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0) | ||||
|   global_eth_component->connected_ = | ||||
|       global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT); | ||||
|   global_eth_component->enable_loop_soon_any_context();  // Enable loop when connection state changes | ||||
| #else | ||||
|   global_eth_component->connected_ = global_eth_component->got_ipv4_address_; | ||||
|   global_eth_component->enable_loop_soon_any_context();  // Enable loop when connection state changes | ||||
| #endif | ||||
| } | ||||
| #endif /* USE_NETWORK_IPV6 */ | ||||
| @@ -620,6 +630,7 @@ bool EthernetComponent::powerdown() { | ||||
|   } | ||||
|   this->connected_ = false; | ||||
|   this->started_ = false; | ||||
|   // No need to enable_loop() here as this is only called during shutdown/reboot | ||||
|   if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "Error powering down ethernet PHY"); | ||||
|     return false; | ||||
|   | ||||
| @@ -41,39 +41,48 @@ void FanCall::perform() { | ||||
| void FanCall::validate_() { | ||||
|   auto traits = this->parent_.get_traits(); | ||||
|  | ||||
|   if (this->speed_.has_value()) | ||||
|   if (this->speed_.has_value()) { | ||||
|     this->speed_ = clamp(*this->speed_, 1, traits.supported_speed_count()); | ||||
|  | ||||
|   if (this->binary_state_.has_value() && *this->binary_state_) { | ||||
|     // when turning on, if neither current nor new speed available, set speed to 100% | ||||
|     if (traits.supports_speed() && !this->parent_.state && this->parent_.speed == 0 && !this->speed_.has_value()) { | ||||
|       this->speed_ = traits.supported_speed_count(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (this->oscillating_.has_value() && !traits.supports_oscillation()) { | ||||
|     ESP_LOGW(TAG, "'%s' - This fan does not support oscillation!", this->parent_.get_name().c_str()); | ||||
|     this->oscillating_.reset(); | ||||
|   } | ||||
|  | ||||
|   if (this->speed_.has_value() && !traits.supports_speed()) { | ||||
|     ESP_LOGW(TAG, "'%s' - This fan does not support speeds!", this->parent_.get_name().c_str()); | ||||
|     this->speed_.reset(); | ||||
|   } | ||||
|  | ||||
|   if (this->direction_.has_value() && !traits.supports_direction()) { | ||||
|     ESP_LOGW(TAG, "'%s' - This fan does not support directions!", this->parent_.get_name().c_str()); | ||||
|     this->direction_.reset(); | ||||
|     // https://developers.home-assistant.io/docs/core/entity/fan/#preset-modes | ||||
|     // "Manually setting a speed must disable any set preset mode" | ||||
|     this->preset_mode_.clear(); | ||||
|   } | ||||
|  | ||||
|   if (!this->preset_mode_.empty()) { | ||||
|     const auto &preset_modes = traits.supported_preset_modes(); | ||||
|     if (preset_modes.find(this->preset_mode_) == preset_modes.end()) { | ||||
|       ESP_LOGW(TAG, "'%s' - This fan does not support preset mode '%s'!", this->parent_.get_name().c_str(), | ||||
|                this->preset_mode_.c_str()); | ||||
|       ESP_LOGW(TAG, "%s: Preset mode '%s' not supported", this->parent_.get_name().c_str(), this->preset_mode_.c_str()); | ||||
|       this->preset_mode_.clear(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // when turning on... | ||||
|   if (!this->parent_.state && this->binary_state_.has_value() && | ||||
|       *this->binary_state_ | ||||
|       // ..,and no preset mode will be active... | ||||
|       && this->preset_mode_.empty() && | ||||
|       this->parent_.preset_mode.empty() | ||||
|       // ...and neither current nor new speed is available... | ||||
|       && traits.supports_speed() && this->parent_.speed == 0 && !this->speed_.has_value()) { | ||||
|     // ...set speed to 100% | ||||
|     this->speed_ = traits.supported_speed_count(); | ||||
|   } | ||||
|  | ||||
|   if (this->oscillating_.has_value() && !traits.supports_oscillation()) { | ||||
|     ESP_LOGW(TAG, "%s: Oscillation not supported", this->parent_.get_name().c_str()); | ||||
|     this->oscillating_.reset(); | ||||
|   } | ||||
|  | ||||
|   if (this->speed_.has_value() && !traits.supports_speed()) { | ||||
|     ESP_LOGW(TAG, "%s: Speed control not supported", this->parent_.get_name().c_str()); | ||||
|     this->speed_.reset(); | ||||
|   } | ||||
|  | ||||
|   if (this->direction_.has_value() && !traits.supports_direction()) { | ||||
|     ESP_LOGW(TAG, "%s: Direction control not supported", this->parent_.get_name().c_str()); | ||||
|     this->direction_.reset(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| FanCall FanRestoreState::to_call(Fan &fan) { | ||||
|   | ||||
| @@ -67,10 +67,10 @@ class Font | ||||
|   inline int get_height() { return this->height_; } | ||||
|   inline int get_bpp() { return this->bpp_; } | ||||
|  | ||||
|   const std::vector<Glyph, ExternalRAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; } | ||||
|   const std::vector<Glyph, RAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; } | ||||
|  | ||||
|  protected: | ||||
|   std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_; | ||||
|   std::vector<Glyph, RAMAllocator<Glyph>> glyphs_; | ||||
|   int baseline_; | ||||
|   int height_; | ||||
|   uint8_t bpp_;  // bits per pixel | ||||
|   | ||||
| @@ -125,6 +125,6 @@ async def to_code(config): | ||||
|     cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE])) | ||||
|     cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE])) | ||||
|  | ||||
|     cg.add_library("tonia/HeatpumpIR", "1.0.32") | ||||
|     cg.add_library("tonia/HeatpumpIR", "1.0.35") | ||||
|     if CORE.is_libretiny: | ||||
|         CORE.add_platformio_option("lib_ignore", "IRremoteESP8266") | ||||
|   | ||||
| @@ -175,7 +175,7 @@ async def to_code(config): | ||||
|                 not config.get(CONF_VERIFY_SSL), | ||||
|             ) | ||||
|         else: | ||||
|             cg.add_library("WiFiClientSecure", None) | ||||
|             cg.add_library("NetworkClientSecure", None) | ||||
|             cg.add_library("HTTPClient", None) | ||||
|     if CORE.is_esp8266: | ||||
|         cg.add_library("ESP8266HTTPClient", None) | ||||
|   | ||||
| @@ -239,7 +239,7 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { | ||||
|  | ||||
|     std::string response_body; | ||||
|     if (this->capture_response_.value(x...)) { | ||||
|       ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||
|       RAMAllocator<uint8_t> allocator; | ||||
|       uint8_t *buf = allocator.allocate(max_length); | ||||
|       if (buf != nullptr) { | ||||
|         size_t read_index = 0; | ||||
|   | ||||
| @@ -6,6 +6,7 @@ | ||||
|  | ||||
| #if defined(USE_ESP32) || defined(USE_RP2040) | ||||
| #include <HTTPClient.h> | ||||
| #include <WiFiClient.h> | ||||
| #endif | ||||
| #ifdef USE_ESP8266 | ||||
| #include <ESP8266HTTPClient.h> | ||||
|   | ||||
| @@ -54,7 +54,7 @@ void HttpRequestUpdate::update_task(void *params) { | ||||
|     UPDATE_RETURN; | ||||
|   } | ||||
|  | ||||
|   ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||
|   RAMAllocator<uint8_t> allocator; | ||||
|   uint8_t *data = allocator.allocate(container->content_length); | ||||
|   if (data == nullptr) { | ||||
|     std::string msg = str_sprintf("Failed to allocate %d bytes for manifest", container->content_length); | ||||
|   | ||||
| @@ -22,8 +22,9 @@ import esphome.final_validate as fv | ||||
| CODEOWNERS = ["@esphome/core"] | ||||
| i2c_ns = cg.esphome_ns.namespace("i2c") | ||||
| I2CBus = i2c_ns.class_("I2CBus") | ||||
| ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", I2CBus, cg.Component) | ||||
| IDFI2CBus = i2c_ns.class_("IDFI2CBus", I2CBus, cg.Component) | ||||
| InternalI2CBus = i2c_ns.class_("InternalI2CBus", I2CBus) | ||||
| ArduinoI2CBus = i2c_ns.class_("ArduinoI2CBus", InternalI2CBus, cg.Component) | ||||
| IDFI2CBus = i2c_ns.class_("IDFI2CBus", InternalI2CBus, cg.Component) | ||||
| I2CDevice = i2c_ns.class_("I2CDevice") | ||||
|  | ||||
|  | ||||
| @@ -71,6 +72,7 @@ CONFIG_SCHEMA = cv.All( | ||||
| @coroutine_with_priority(1.0) | ||||
| async def to_code(config): | ||||
|     cg.add_global(i2c_ns.using) | ||||
|     cg.add_define("USE_I2C") | ||||
|     var = cg.new_Pvariable(config[CONF_ID]) | ||||
|     await cg.register_component(var, config) | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| #pragma once | ||||
| #include <cstdint> | ||||
| #include <cstddef> | ||||
| #include <cstdint> | ||||
| #include <utility> | ||||
| #include <vector> | ||||
|  | ||||
| @@ -108,5 +108,12 @@ class I2CBus { | ||||
|   bool scan_{false};                                    ///< Should we scan ? Can be set in the yaml | ||||
| }; | ||||
|  | ||||
| class InternalI2CBus : public I2CBus { | ||||
|  public: | ||||
|   /// @brief Returns the I2C port number. | ||||
|   /// @return the port number of the internal I2C bus | ||||
|   virtual int get_port() const = 0; | ||||
| }; | ||||
|  | ||||
| }  // namespace i2c | ||||
| }  // namespace esphome | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| #ifdef USE_ARDUINO | ||||
|  | ||||
| #include "i2c_bus_arduino.h" | ||||
| #include <Arduino.h> | ||||
| #include <cstring> | ||||
| #include "esphome/core/application.h" | ||||
| #include "esphome/core/helpers.h" | ||||
| #include "esphome/core/log.h" | ||||
| #include <Arduino.h> | ||||
| #include <cstring> | ||||
|  | ||||
| namespace esphome { | ||||
| namespace i2c { | ||||
| @@ -23,6 +23,7 @@ void ArduinoI2CBus::setup() { | ||||
|   } else { | ||||
|     wire_ = new TwoWire(next_bus_num);  // NOLINT(cppcoreguidelines-owning-memory) | ||||
|   } | ||||
|   this->port_ = next_bus_num; | ||||
|   next_bus_num++; | ||||
| #elif defined(USE_ESP8266) | ||||
|   wire_ = new TwoWire();  // NOLINT(cppcoreguidelines-owning-memory) | ||||
| @@ -125,7 +126,7 @@ ErrorCode ArduinoI2CBus::readv(uint8_t address, ReadBuffer *buffers, size_t cnt) | ||||
|   size_t to_request = 0; | ||||
|   for (size_t i = 0; i < cnt; i++) | ||||
|     to_request += buffers[i].len; | ||||
|   size_t ret = wire_->requestFrom((int) address, (int) to_request, 1); | ||||
|   size_t ret = wire_->requestFrom(address, to_request, true); | ||||
|   if (ret != to_request) { | ||||
|     ESP_LOGVV(TAG, "RX %u from %02X failed with error %u", to_request, address, ret); | ||||
|     return ERROR_TIMEOUT; | ||||
|   | ||||
| @@ -2,9 +2,9 @@ | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
|  | ||||
| #include "i2c_bus.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include <Wire.h> | ||||
| #include "esphome/core/component.h" | ||||
| #include "i2c_bus.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace i2c { | ||||
| @@ -15,7 +15,7 @@ enum RecoveryCode { | ||||
|   RECOVERY_COMPLETED, | ||||
| }; | ||||
|  | ||||
| class ArduinoI2CBus : public I2CBus, public Component { | ||||
| class ArduinoI2CBus : public InternalI2CBus, public Component { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
| @@ -29,12 +29,15 @@ class ArduinoI2CBus : public I2CBus, public Component { | ||||
|   void set_frequency(uint32_t frequency) { frequency_ = frequency; } | ||||
|   void set_timeout(uint32_t timeout) { timeout_ = timeout; } | ||||
|  | ||||
|   int get_port() const override { return this->port_; } | ||||
|  | ||||
|  private: | ||||
|   void recover_(); | ||||
|   void set_pins_and_clock_(); | ||||
|   RecoveryCode recovery_result_; | ||||
|  | ||||
|  protected: | ||||
|   int8_t port_{-1}; | ||||
|   TwoWire *wire_; | ||||
|   uint8_t sda_pin_; | ||||
|   uint8_t scl_pin_; | ||||
|   | ||||
| @@ -2,9 +2,9 @@ | ||||
|  | ||||
| #ifdef USE_ESP_IDF | ||||
|  | ||||
| #include "i2c_bus.h" | ||||
| #include "esphome/core/component.h" | ||||
| #include <driver/i2c.h> | ||||
| #include "esphome/core/component.h" | ||||
| #include "i2c_bus.h" | ||||
|  | ||||
| namespace esphome { | ||||
| namespace i2c { | ||||
| @@ -15,7 +15,7 @@ enum RecoveryCode { | ||||
|   RECOVERY_COMPLETED, | ||||
| }; | ||||
|  | ||||
| class IDFI2CBus : public I2CBus, public Component { | ||||
| class IDFI2CBus : public InternalI2CBus, public Component { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
| @@ -31,6 +31,8 @@ class IDFI2CBus : public I2CBus, public Component { | ||||
|   void set_frequency(uint32_t frequency) { frequency_ = frequency; } | ||||
|   void set_timeout(uint32_t timeout) { timeout_ = timeout; } | ||||
|  | ||||
|   int get_port() const override { return static_cast<int>(this->port_); } | ||||
|  | ||||
|  private: | ||||
|   void recover_(); | ||||
|   RecoveryCode recovery_result_; | ||||
|   | ||||
| @@ -9,7 +9,7 @@ namespace i2s_audio { | ||||
|  | ||||
| static const char *const TAG = "i2s_audio"; | ||||
|  | ||||
| #if defined(USE_ESP_IDF) && (ESP_IDF_VERSION_MAJOR >= 5) | ||||
| #if ESP_IDF_VERSION_MAJOR >= 5 | ||||
| static const uint8_t I2S_NUM_MAX = SOC_I2S_NUM;  // because IDF 5+ took this away :( | ||||
| #endif | ||||
|  | ||||
| @@ -18,7 +18,7 @@ void I2SAudioComponent::setup() { | ||||
|  | ||||
|   static i2s_port_t next_port_num = I2S_NUM_0; | ||||
|   if (next_port_num >= I2S_NUM_MAX) { | ||||
|     ESP_LOGE(TAG, "Too many I2S Audio components"); | ||||
|     ESP_LOGE(TAG, "Too many components"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|   | ||||
| @@ -114,7 +114,7 @@ async def to_code(config): | ||||
|         cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) | ||||
|         cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) | ||||
|  | ||||
|     cg.add_library("WiFiClientSecure", None) | ||||
|     cg.add_library("NetworkClientSecure", None) | ||||
|     cg.add_library("HTTPClient", None) | ||||
|     cg.add_library("esphome/ESP32-audioI2S", "2.2.0") | ||||
|     cg.add_library("esphome/ESP32-audioI2S", "2.3.0") | ||||
|     cg.add_build_flag("-DAUDIO_NO_SD_FS") | ||||
|   | ||||
| @@ -45,7 +45,7 @@ void I2SAudioMicrophone::setup() { | ||||
| #if SOC_I2S_SUPPORTS_ADC | ||||
|   if (this->adc_) { | ||||
|     if (this->parent_->get_port() != I2S_NUM_0) { | ||||
|       ESP_LOGE(TAG, "Internal ADC only works on I2S0!"); | ||||
|       ESP_LOGE(TAG, "Internal ADC only works on I2S0"); | ||||
|       this->mark_failed(); | ||||
|       return; | ||||
|     } | ||||
| @@ -55,7 +55,7 @@ void I2SAudioMicrophone::setup() { | ||||
|   { | ||||
|     if (this->pdm_) { | ||||
|       if (this->parent_->get_port() != I2S_NUM_0) { | ||||
|         ESP_LOGE(TAG, "PDM only works on I2S0!"); | ||||
|         ESP_LOGE(TAG, "PDM only works on I2S0"); | ||||
|         this->mark_failed(); | ||||
|         return; | ||||
|       } | ||||
| @@ -64,14 +64,14 @@ void I2SAudioMicrophone::setup() { | ||||
|  | ||||
|   this->active_listeners_semaphore_ = xSemaphoreCreateCounting(MAX_LISTENERS, MAX_LISTENERS); | ||||
|   if (this->active_listeners_semaphore_ == nullptr) { | ||||
|     ESP_LOGE(TAG, "Failed to create semaphore"); | ||||
|     ESP_LOGE(TAG, "Creating semaphore failed"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   this->event_group_ = xEventGroupCreate(); | ||||
|   if (this->event_group_ == nullptr) { | ||||
|     ESP_LOGE(TAG, "Failed to create event group"); | ||||
|     ESP_LOGE(TAG, "Creating event group failed"); | ||||
|     this->mark_failed(); | ||||
|     return; | ||||
|   } | ||||
| @@ -79,6 +79,15 @@ void I2SAudioMicrophone::setup() { | ||||
|   this->configure_stream_settings_(); | ||||
| } | ||||
|  | ||||
| void I2SAudioMicrophone::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "Microphone:\n" | ||||
|                 "  Pin: %d\n" | ||||
|                 "  PDM: %s\n" | ||||
|                 "  DC offset correction: %s", | ||||
|                 static_cast<int8_t>(this->din_pin_), YESNO(this->pdm_), YESNO(this->correct_dc_offset_)); | ||||
| } | ||||
|  | ||||
| void I2SAudioMicrophone::configure_stream_settings_() { | ||||
|   uint8_t channel_count = 1; | ||||
| #ifdef USE_I2S_LEGACY | ||||
| @@ -127,6 +136,7 @@ bool I2SAudioMicrophone::start_driver_() { | ||||
|   if (!this->parent_->try_lock()) { | ||||
|     return false;  // Waiting for another i2s to return lock | ||||
|   } | ||||
|   this->locked_driver_ = true; | ||||
|   esp_err_t err; | ||||
|  | ||||
| #ifdef USE_I2S_LEGACY | ||||
| @@ -151,7 +161,7 @@ bool I2SAudioMicrophone::start_driver_() { | ||||
|     config.mode = (i2s_mode_t) (config.mode | I2S_MODE_ADC_BUILT_IN); | ||||
|     err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGE(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); | ||||
|       ESP_LOGE(TAG, "Error installing driver: %s", esp_err_to_name(err)); | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
| @@ -174,7 +184,7 @@ bool I2SAudioMicrophone::start_driver_() { | ||||
|  | ||||
|     err = i2s_driver_install(this->parent_->get_port(), &config, 0, nullptr); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGE(TAG, "Error installing I2S driver: %s", esp_err_to_name(err)); | ||||
|       ESP_LOGE(TAG, "Error installing driver: %s", esp_err_to_name(err)); | ||||
|       return false; | ||||
|     } | ||||
|  | ||||
| @@ -183,7 +193,7 @@ bool I2SAudioMicrophone::start_driver_() { | ||||
|  | ||||
|     err = i2s_set_pin(this->parent_->get_port(), &pin_config); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGE(TAG, "Error setting I2S pin: %s", esp_err_to_name(err)); | ||||
|       ESP_LOGE(TAG, "Error setting pin: %s", esp_err_to_name(err)); | ||||
|       return false; | ||||
|     } | ||||
|   } | ||||
| @@ -198,7 +208,7 @@ bool I2SAudioMicrophone::start_driver_() { | ||||
|   /* Allocate a new RX channel and get the handle of this channel */ | ||||
|   err = i2s_new_channel(&chan_cfg, NULL, &this->rx_handle_); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "Error creating new I2S channel: %s", esp_err_to_name(err)); | ||||
|     ESP_LOGE(TAG, "Error creating channel: %s", esp_err_to_name(err)); | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
| @@ -270,14 +280,14 @@ bool I2SAudioMicrophone::start_driver_() { | ||||
|     err = i2s_channel_init_std_mode(this->rx_handle_, &std_cfg); | ||||
|   } | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "Error initializing I2S channel: %s", esp_err_to_name(err)); | ||||
|     ESP_LOGE(TAG, "Error initializing channel: %s", esp_err_to_name(err)); | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   /* Before reading data, start the RX channel first */ | ||||
|   i2s_channel_enable(this->rx_handle_); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGE(TAG, "Error enabling I2S Microphone: %s", esp_err_to_name(err)); | ||||
|     ESP_LOGE(TAG, "Enabling failed: %s", esp_err_to_name(err)); | ||||
|     return false; | ||||
|   } | ||||
| #endif | ||||
| @@ -304,31 +314,37 @@ void I2SAudioMicrophone::stop_driver_() { | ||||
|   if (this->adc_) { | ||||
|     err = i2s_adc_disable(this->parent_->get_port()); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "Error disabling ADC - it may not have started: %s", esp_err_to_name(err)); | ||||
|       ESP_LOGW(TAG, "Error disabling ADC: %s", esp_err_to_name(err)); | ||||
|     } | ||||
|   } | ||||
| #endif | ||||
|   err = i2s_stop(this->parent_->get_port()); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err)); | ||||
|     ESP_LOGW(TAG, "Error stopping: %s", esp_err_to_name(err)); | ||||
|   } | ||||
|   err = i2s_driver_uninstall(this->parent_->get_port()); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "Error uninstalling I2S driver - it may not have started: %s", esp_err_to_name(err)); | ||||
|     ESP_LOGW(TAG, "Error uninstalling driver: %s", esp_err_to_name(err)); | ||||
|   } | ||||
| #else | ||||
|   /* Have to stop the channel before deleting it */ | ||||
|   err = i2s_channel_disable(this->rx_handle_); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "Error stopping I2S microphone - it may not have started: %s", esp_err_to_name(err)); | ||||
|   } | ||||
|   /* If the handle is not needed any more, delete it to release the channel resources */ | ||||
|   err = i2s_del_channel(this->rx_handle_); | ||||
|   if (err != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "Error deleting I2S channel - it may not have started: %s", esp_err_to_name(err)); | ||||
|   if (this->rx_handle_ != nullptr) { | ||||
|     /* Have to stop the channel before deleting it */ | ||||
|     err = i2s_channel_disable(this->rx_handle_); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "Error stopping: %s", esp_err_to_name(err)); | ||||
|     } | ||||
|     /* If the handle is not needed any more, delete it to release the channel resources */ | ||||
|     err = i2s_del_channel(this->rx_handle_); | ||||
|     if (err != ESP_OK) { | ||||
|       ESP_LOGW(TAG, "Error deleting channel: %s", esp_err_to_name(err)); | ||||
|     } | ||||
|     this->rx_handle_ = nullptr; | ||||
|   } | ||||
| #endif | ||||
|   this->parent_->unlock(); | ||||
|   if (this->locked_driver_) { | ||||
|     this->parent_->unlock(); | ||||
|     this->locked_driver_ = false; | ||||
|   } | ||||
| } | ||||
|  | ||||
| void I2SAudioMicrophone::mic_task(void *params) { | ||||
| @@ -400,7 +416,7 @@ size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_w | ||||
|     // Ignore ESP_ERR_TIMEOUT if ticks_to_wait = 0, as it will read the data on the next call | ||||
|     if (!this->status_has_warning()) { | ||||
|       // Avoid spamming the logs with the error message if its repeated | ||||
|       ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err)); | ||||
|       ESP_LOGW(TAG, "Read error: %s", esp_err_to_name(err)); | ||||
|     } | ||||
|     this->status_set_warning(); | ||||
|     return 0; | ||||
| @@ -428,19 +444,19 @@ void I2SAudioMicrophone::loop() { | ||||
|   uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); | ||||
|  | ||||
|   if (event_group_bits & MicrophoneEventGroupBits::TASK_STARTING) { | ||||
|     ESP_LOGD(TAG, "Task started, attempting to allocate buffer"); | ||||
|     ESP_LOGV(TAG, "Task started, attempting to allocate buffer"); | ||||
|     xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_STARTING); | ||||
|   } | ||||
|  | ||||
|   if (event_group_bits & MicrophoneEventGroupBits::TASK_RUNNING) { | ||||
|     ESP_LOGD(TAG, "Task is running and reading data"); | ||||
|     ESP_LOGV(TAG, "Task is running and reading data"); | ||||
|  | ||||
|     xEventGroupClearBits(this->event_group_, MicrophoneEventGroupBits::TASK_RUNNING); | ||||
|     this->state_ = microphone::STATE_RUNNING; | ||||
|   } | ||||
|  | ||||
|   if ((event_group_bits & MicrophoneEventGroupBits::TASK_STOPPED)) { | ||||
|     ESP_LOGD(TAG, "Task finished, freeing resources and uninstalling I2S driver"); | ||||
|     ESP_LOGV(TAG, "Task finished, freeing resources and uninstalling driver"); | ||||
|  | ||||
|     vTaskDelete(this->task_handle_); | ||||
|     this->task_handle_ = nullptr; | ||||
| @@ -470,7 +486,8 @@ void I2SAudioMicrophone::loop() { | ||||
|       } | ||||
|  | ||||
|       if (!this->start_driver_()) { | ||||
|         this->status_momentary_error("I2S driver failed to start, unloading it and attempting again in 1 second", 1000); | ||||
|         ESP_LOGE(TAG, "Driver failed to start; retrying in 1 second"); | ||||
|         this->status_momentary_error("driver_fail", 1000); | ||||
|         this->stop_driver_();  // Stop/frees whatever possibly started | ||||
|         break; | ||||
|       } | ||||
| @@ -480,7 +497,8 @@ void I2SAudioMicrophone::loop() { | ||||
|                     &this->task_handle_); | ||||
|  | ||||
|         if (this->task_handle_ == nullptr) { | ||||
|           this->status_momentary_error("Task failed to start, attempting again in 1 second", 1000); | ||||
|           ESP_LOGE(TAG, "Task failed to start, retrying in 1 second"); | ||||
|           this->status_momentary_error("task_fail", 1000); | ||||
|           this->stop_driver_();  // Stops the driver to return the lock; will be reloaded in next attempt | ||||
|         } | ||||
|       } | ||||
|   | ||||
| @@ -18,6 +18,7 @@ namespace i2s_audio { | ||||
| class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, public Component { | ||||
|  public: | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   void start() override; | ||||
|   void stop() override; | ||||
|  | ||||
| @@ -80,6 +81,7 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub | ||||
|   bool pdm_{false}; | ||||
|  | ||||
|   bool correct_dc_offset_; | ||||
|   bool locked_driver_{false}; | ||||
|   int32_t dc_offset_{0}; | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -110,29 +110,48 @@ void I2SAudioSpeaker::setup() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void I2SAudioSpeaker::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, | ||||
|                 "Speaker:\n" | ||||
|                 "  Pin: %d\n" | ||||
|                 "  Buffer duration: %" PRIu32, | ||||
|                 static_cast<int8_t>(this->dout_pin_), this->buffer_duration_ms_); | ||||
|   if (this->timeout_.has_value()) { | ||||
|     ESP_LOGCONFIG(TAG, "  Timeout: %" PRIu32 " ms", this->timeout_.value()); | ||||
|   } | ||||
| #ifdef USE_I2S_LEGACY | ||||
| #if SOC_I2S_SUPPORTS_DAC | ||||
|   ESP_LOGCONFIG(TAG, "  Internal DAC mode: %d", static_cast<int8_t>(this->internal_dac_mode_)); | ||||
| #endif | ||||
|   ESP_LOGCONFIG(TAG, "  Communication format: %d", static_cast<int8_t>(this->i2s_comm_fmt_)); | ||||
| #else | ||||
|   ESP_LOGCONFIG(TAG, "  Communication format: %s", this->i2s_comm_fmt_.c_str()); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void I2SAudioSpeaker::loop() { | ||||
|   uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); | ||||
|  | ||||
|   if (event_group_bits & SpeakerEventGroupBits::STATE_STARTING) { | ||||
|     ESP_LOGD(TAG, "Starting Speaker"); | ||||
|     ESP_LOGD(TAG, "Starting"); | ||||
|     this->state_ = speaker::STATE_STARTING; | ||||
|     xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STARTING); | ||||
|   } | ||||
|   if (event_group_bits & SpeakerEventGroupBits::STATE_RUNNING) { | ||||
|     ESP_LOGD(TAG, "Started Speaker"); | ||||
|     ESP_LOGD(TAG, "Started"); | ||||
|     this->state_ = speaker::STATE_RUNNING; | ||||
|     xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_RUNNING); | ||||
|     this->status_clear_warning(); | ||||
|     this->status_clear_error(); | ||||
|   } | ||||
|   if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPING) { | ||||
|     ESP_LOGD(TAG, "Stopping Speaker"); | ||||
|     ESP_LOGD(TAG, "Stopping"); | ||||
|     this->state_ = speaker::STATE_STOPPING; | ||||
|     xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::STATE_STOPPING); | ||||
|   } | ||||
|   if (event_group_bits & SpeakerEventGroupBits::STATE_STOPPED) { | ||||
|     if (!this->task_created_) { | ||||
|       ESP_LOGD(TAG, "Stopped Speaker"); | ||||
|       ESP_LOGD(TAG, "Stopped"); | ||||
|       this->state_ = speaker::STATE_STOPPED; | ||||
|       xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ALL_BITS); | ||||
|       this->speaker_task_handle_ = nullptr; | ||||
| @@ -140,20 +159,19 @@ void I2SAudioSpeaker::loop() { | ||||
|   } | ||||
|  | ||||
|   if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) { | ||||
|     this->status_set_error("Failed to start speaker task"); | ||||
|     this->status_set_error("Failed to start task"); | ||||
|     xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START); | ||||
|   } | ||||
|  | ||||
|   if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) { | ||||
|     uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS; | ||||
|     ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); | ||||
|     ESP_LOGW(TAG, "Writing failed: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); | ||||
|     this->status_set_warning(); | ||||
|   } | ||||
|  | ||||
|   if (event_group_bits & SpeakerEventGroupBits::ERR_ESP_NOT_SUPPORTED) { | ||||
|     this->status_set_error("Failed to adjust I2S bus to match the incoming audio"); | ||||
|     ESP_LOGE(TAG, | ||||
|              "Incompatible audio format: sample rate = %" PRIu32 ", channels = %" PRIu8 ", bits per sample = %" PRIu8, | ||||
|     this->status_set_error("Failed to adjust bus to match incoming audio"); | ||||
|     ESP_LOGE(TAG, "Incompatible audio format: sample rate = %" PRIu32 ", channels = %u, bits per sample = %u", | ||||
|              this->audio_stream_info_.get_sample_rate(), this->audio_stream_info_.get_channels(), | ||||
|              this->audio_stream_info_.get_bits_per_sample()); | ||||
|   } | ||||
| @@ -202,7 +220,7 @@ void I2SAudioSpeaker::set_mute_state(bool mute_state) { | ||||
|  | ||||
| size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) { | ||||
|   if (this->is_failed()) { | ||||
|     ESP_LOGE(TAG, "Cannot play audio, speaker failed to setup"); | ||||
|     ESP_LOGE(TAG, "Setup failed; cannot play audio"); | ||||
|     return 0; | ||||
|   } | ||||
|   if (this->state_ != speaker::STATE_RUNNING && this->state_ != speaker::STATE_STARTING) { | ||||
| @@ -466,7 +484,7 @@ bool I2SAudioSpeaker::send_esp_err_to_event_group_(esp_err_t err) { | ||||
| esp_err_t I2SAudioSpeaker::allocate_buffers_(size_t data_buffer_size, size_t ring_buffer_size) { | ||||
|   if (this->data_buffer_ == nullptr) { | ||||
|     // Allocate data buffer for temporarily storing audio from the ring buffer before writing to the I2S bus | ||||
|     ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||
|     RAMAllocator<uint8_t> allocator; | ||||
|     this->data_buffer_ = allocator.allocate(data_buffer_size); | ||||
|   } | ||||
|  | ||||
| @@ -680,7 +698,7 @@ void I2SAudioSpeaker::delete_task_(size_t buffer_size) { | ||||
|   this->audio_ring_buffer_.reset();  // Releases ownership of the shared_ptr | ||||
|  | ||||
|   if (this->data_buffer_ != nullptr) { | ||||
|     ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||
|     RAMAllocator<uint8_t> allocator; | ||||
|     allocator.deallocate(this->data_buffer_, buffer_size); | ||||
|     this->data_buffer_ = nullptr; | ||||
|   } | ||||
|   | ||||
| @@ -24,6 +24,7 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp | ||||
|   float get_setup_priority() const override { return esphome::setup_priority::PROCESSOR; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
|   void loop() override; | ||||
|  | ||||
|   void set_buffer_duration(uint32_t buffer_duration_ms) { this->buffer_duration_ms_ = buffer_duration_ms; } | ||||
|   | ||||
| @@ -129,6 +129,13 @@ void INA219Component::setup() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void INA219Component::on_powerdown() { | ||||
|   // Mode = 0 -> power down | ||||
|   if (!this->write_byte_16(INA219_REGISTER_CONFIG, 0)) { | ||||
|     ESP_LOGE(TAG, "powerdown error"); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void INA219Component::dump_config() { | ||||
|   ESP_LOGCONFIG(TAG, "INA219:"); | ||||
|   LOG_I2C_DEVICE(this); | ||||
|   | ||||
| @@ -15,6 +15,7 @@ class INA219Component : public PollingComponent, public i2c::I2CDevice { | ||||
|   void dump_config() override; | ||||
|   float get_setup_priority() const override; | ||||
|   void update() override; | ||||
|   void on_powerdown() override; | ||||
|  | ||||
|   void set_shunt_resistance_ohm(float shunt_resistance_ohm) { shunt_resistance_ohm_ = shunt_resistance_ohm; } | ||||
|   void set_max_current_a(float max_current_a) { max_current_a_ = max_current_a; } | ||||
|   | ||||
| @@ -57,8 +57,8 @@ void Inkplate6::setup() { | ||||
|  * Allocate buffers. May be called after setup to re-initialise if e.g. greyscale is changed. | ||||
|  */ | ||||
| void Inkplate6::initialize_() { | ||||
|   ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||
|   ExternalRAMAllocator<uint32_t> allocator32(ExternalRAMAllocator<uint32_t>::ALLOW_FAILURE); | ||||
|   RAMAllocator<uint8_t> allocator; | ||||
|   RAMAllocator<uint32_t> allocator32; | ||||
|   uint32_t buffer_size = this->get_buffer_length_(); | ||||
|   if (buffer_size == 0) | ||||
|     return; | ||||
|   | ||||
| @@ -19,9 +19,8 @@ void KMeterISOComponent::setup() { | ||||
|  | ||||
|   // Mark as not failed before initializing. Some devices will turn off sensors to save on batteries | ||||
|   // and when they come back on, the COMPONENT_STATE_FAILED bit must be unset on the component. | ||||
|   if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { | ||||
|     this->component_state_ &= ~COMPONENT_STATE_MASK; | ||||
|     this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; | ||||
|   if (this->is_failed()) { | ||||
|     this->reset_to_construction_state(); | ||||
|   } | ||||
|  | ||||
|   auto err = this->bus_->writev(this->address_, nullptr, 0); | ||||
|   | ||||
| @@ -3,6 +3,8 @@ from esphome.components import number | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_MOVE_THRESHOLD, | ||||
|     CONF_STILL_THRESHOLD, | ||||
|     CONF_TIMEOUT, | ||||
|     DEVICE_CLASS_DISTANCE, | ||||
|     DEVICE_CLASS_ILLUMINANCE, | ||||
| @@ -24,8 +26,6 @@ MaxDistanceTimeoutNumber = ld2410_ns.class_("MaxDistanceTimeoutNumber", number.N | ||||
| CONF_MAX_MOVE_DISTANCE_GATE = "max_move_distance_gate" | ||||
| CONF_MAX_STILL_DISTANCE_GATE = "max_still_distance_gate" | ||||
| CONF_LIGHT_THRESHOLD = "light_threshold" | ||||
| CONF_STILL_THRESHOLD = "still_threshold" | ||||
| CONF_MOVE_THRESHOLD = "move_threshold" | ||||
|  | ||||
| TIMEOUT_GROUP = "timeout" | ||||
|  | ||||
|   | ||||
| @@ -3,6 +3,7 @@ from esphome.components import sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_LIGHT, | ||||
|     CONF_MOVING_DISTANCE, | ||||
|     DEVICE_CLASS_DISTANCE, | ||||
|     DEVICE_CLASS_ILLUMINANCE, | ||||
|     ENTITY_CATEGORY_DIAGNOSTIC, | ||||
| @@ -17,7 +18,6 @@ from esphome.const import ( | ||||
| from . import CONF_LD2410_ID, LD2410Component | ||||
|  | ||||
| DEPENDENCIES = ["ld2410"] | ||||
| CONF_MOVING_DISTANCE = "moving_distance" | ||||
| CONF_STILL_DISTANCE = "still_distance" | ||||
| CONF_MOVING_ENERGY = "moving_energy" | ||||
| CONF_STILL_ENERGY = "still_energy" | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import esphome.codegen as cg | ||||
| from esphome.components import switch | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_BLUETOOTH, | ||||
|     DEVICE_CLASS_SWITCH, | ||||
|     ENTITY_CATEGORY_CONFIG, | ||||
|     ICON_BLUETOOTH, | ||||
| @@ -14,7 +15,6 @@ BluetoothSwitch = ld2410_ns.class_("BluetoothSwitch", switch.Switch) | ||||
| EngineeringModeSwitch = ld2410_ns.class_("EngineeringModeSwitch", switch.Switch) | ||||
|  | ||||
| CONF_ENGINEERING_MODE = "engineering_mode" | ||||
| CONF_BLUETOOTH = "bluetooth" | ||||
|  | ||||
| CONFIG_SCHEMA = { | ||||
|     cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component), | ||||
|   | ||||
| @@ -3,6 +3,8 @@ from esphome.components import number | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_MOVE_THRESHOLD, | ||||
|     CONF_STILL_THRESHOLD, | ||||
|     DEVICE_CLASS_DISTANCE, | ||||
|     ENTITY_CATEGORY_CONFIG, | ||||
|     ICON_MOTION_SENSOR, | ||||
| @@ -31,8 +33,6 @@ LD2420StillThresholdNumbers = ld2420_ns.class_( | ||||
| ) | ||||
| CONF_MIN_GATE_DISTANCE = "min_gate_distance" | ||||
| CONF_MAX_GATE_DISTANCE = "max_gate_distance" | ||||
| CONF_STILL_THRESHOLD = "still_threshold" | ||||
| CONF_MOVE_THRESHOLD = "move_threshold" | ||||
| CONF_GATE_MOVE_SENSITIVITY = "gate_move_sensitivity" | ||||
| CONF_GATE_STILL_SENSITIVITY = "gate_still_sensitivity" | ||||
| CONF_GATE_SELECT = "gate_select" | ||||
|   | ||||
| @@ -1,13 +1,17 @@ | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import sensor | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import CONF_ID, DEVICE_CLASS_DISTANCE, UNIT_CENTIMETER | ||||
| from esphome.const import ( | ||||
|     CONF_ID, | ||||
|     CONF_MOVING_DISTANCE, | ||||
|     DEVICE_CLASS_DISTANCE, | ||||
|     UNIT_CENTIMETER, | ||||
| ) | ||||
|  | ||||
| from .. import CONF_LD2420_ID, LD2420Component, ld2420_ns | ||||
|  | ||||
| LD2420Sensor = ld2420_ns.class_("LD2420Sensor", sensor.Sensor, cg.Component) | ||||
|  | ||||
| CONF_MOVING_DISTANCE = "moving_distance" | ||||
| CONF_GATE_ENERGY = "gate_energy" | ||||
|  | ||||
| CONFIG_SCHEMA = cv.All( | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import esphome.codegen as cg | ||||
| from esphome.components import switch | ||||
| import esphome.config_validation as cv | ||||
| from esphome.const import ( | ||||
|     CONF_BLUETOOTH, | ||||
|     DEVICE_CLASS_SWITCH, | ||||
|     ENTITY_CATEGORY_CONFIG, | ||||
|     ICON_BLUETOOTH, | ||||
| @@ -13,7 +14,6 @@ from .. import CONF_LD2450_ID, LD2450Component, ld2450_ns | ||||
| BluetoothSwitch = ld2450_ns.class_("BluetoothSwitch", switch.Switch) | ||||
| MultiTargetSwitch = ld2450_ns.class_("MultiTargetSwitch", switch.Switch) | ||||
|  | ||||
| CONF_BLUETOOTH = "bluetooth" | ||||
| CONF_MULTI_TARGET = "multi_target" | ||||
|  | ||||
| CONFIG_SCHEMA = { | ||||
|   | ||||
| @@ -3,28 +3,16 @@ | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
| #include <esp32-hal-ledc.h> | ||||
| #endif | ||||
| #include <driver/ledc.h> | ||||
|  | ||||
| #include <cinttypes> | ||||
|  | ||||
| #define CLOCK_FREQUENCY 80e6f | ||||
|  | ||||
| #ifdef USE_ARDUINO | ||||
| #ifdef SOC_LEDC_SUPPORT_XTAL_CLOCK | ||||
| #undef CLOCK_FREQUENCY | ||||
| // starting with ESP32 Arduino 2.0.2, the 40MHz crystal is used as clock by default if supported | ||||
| #define CLOCK_FREQUENCY 40e6f | ||||
| #endif | ||||
| #else | ||||
| #ifdef SOC_LEDC_SUPPORT_APB_CLOCK | ||||
| #define DEFAULT_CLK LEDC_USE_APB_CLK | ||||
| #else | ||||
| #define DEFAULT_CLK LEDC_AUTO_CLK | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| static const uint8_t SETUP_ATTEMPT_COUNT_MAX = 5; | ||||
|  | ||||
| @@ -34,7 +22,6 @@ namespace ledc { | ||||
| static const char *const TAG = "ledc.output"; | ||||
|  | ||||
| static const int MAX_RES_BITS = LEDC_TIMER_BIT_MAX - 1; | ||||
| #ifdef USE_ESP_IDF | ||||
| #if SOC_LEDC_SUPPORT_HS_MODE | ||||
| // Only ESP32 has LEDC_HIGH_SPEED_MODE | ||||
| inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_HIGH_SPEED_MODE : LEDC_LOW_SPEED_MODE; } | ||||
| @@ -44,7 +31,6 @@ inline ledc_mode_t get_speed_mode(uint8_t channel) { return channel < 8 ? LEDC_H | ||||
| // https://docs.espressif.com/projects/esp-idf/en/latest/esp32c3/api-reference/peripherals/ledc.html#functionality-overview | ||||
| inline ledc_mode_t get_speed_mode(uint8_t) { return LEDC_LOW_SPEED_MODE; } | ||||
| #endif | ||||
| #endif | ||||
|  | ||||
| float ledc_max_frequency_for_bit_depth(uint8_t bit_depth) { | ||||
|   return static_cast<float>(CLOCK_FREQUENCY) / static_cast<float>(1 << bit_depth); | ||||
| @@ -68,7 +54,6 @@ optional<uint8_t> ledc_bit_depth_for_frequency(float frequency) { | ||||
|   return {}; | ||||
| } | ||||
|  | ||||
| #ifdef USE_ESP_IDF | ||||
| esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_num, ledc_channel_t chan_num, | ||||
|                                     uint8_t channel, uint8_t &bit_depth, float frequency) { | ||||
|   bit_depth = *ledc_bit_depth_for_frequency(frequency); | ||||
| @@ -98,13 +83,10 @@ esp_err_t configure_timer_frequency(ledc_mode_t speed_mode, ledc_timer_t timer_n | ||||
|  | ||||
|   return init_result; | ||||
| } | ||||
| #endif | ||||
|  | ||||
| #ifdef USE_ESP_IDF | ||||
| constexpr int ledc_angle_to_htop(float angle, uint8_t bit_depth) { | ||||
|   return static_cast<int>(angle * ((1U << bit_depth) - 1) / 360.0f); | ||||
| } | ||||
| #endif  // USE_ESP_IDF | ||||
|  | ||||
| void LEDCOutput::write_state(float state) { | ||||
|   if (!this->initialized_) { | ||||
| @@ -120,10 +102,6 @@ void LEDCOutput::write_state(float state) { | ||||
|   const float duty_rounded = roundf(state * max_duty); | ||||
|   auto duty = static_cast<uint32_t>(duty_rounded); | ||||
|   ESP_LOGV(TAG, "Setting duty: %" PRIu32 " on channel %u", duty, this->channel_); | ||||
| #ifdef USE_ARDUINO | ||||
|   ledcWrite(this->channel_, duty); | ||||
| #endif | ||||
| #ifdef USE_ESP_IDF | ||||
|   auto speed_mode = get_speed_mode(this->channel_); | ||||
|   auto chan_num = static_cast<ledc_channel_t>(this->channel_ % 8); | ||||
|   int hpoint = ledc_angle_to_htop(this->phase_angle_, this->bit_depth_); | ||||
| @@ -135,18 +113,10 @@ void LEDCOutput::write_state(float state) { | ||||
|     ledc_set_duty_with_hpoint(speed_mode, chan_num, duty, hpoint); | ||||
|     ledc_update_duty(speed_mode, chan_num); | ||||
|   } | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void LEDCOutput::setup() { | ||||
|   ESP_LOGCONFIG(TAG, "Running setup"); | ||||
| #ifdef USE_ARDUINO | ||||
|   this->update_frequency(this->frequency_); | ||||
|   this->turn_off(); | ||||
|   // Attach pin after setting default value | ||||
|   ledcAttachPin(this->pin_->get_pin(), this->channel_); | ||||
| #endif | ||||
| #ifdef USE_ESP_IDF | ||||
|   auto speed_mode = get_speed_mode(this->channel_); | ||||
|   auto timer_num = static_cast<ledc_timer_t>((this->channel_ % 8) / 2); | ||||
|   auto chan_num = static_cast<ledc_channel_t>(this->channel_ % 8); | ||||
| @@ -175,7 +145,6 @@ void LEDCOutput::setup() { | ||||
|   ledc_channel_config(&chan_conf); | ||||
|   this->initialized_ = true; | ||||
|   this->status_clear_error(); | ||||
| #endif | ||||
| } | ||||
|  | ||||
| void LEDCOutput::dump_config() { | ||||
| @@ -208,38 +177,7 @@ void LEDCOutput::update_frequency(float frequency) { | ||||
|   } | ||||
|   this->bit_depth_ = bit_depth_opt.value_or(8); | ||||
|   this->frequency_ = frequency; | ||||
| #ifdef USE_ARDUINO | ||||
|   ESP_LOGV(TAG, "Using Arduino API - Trying to define channel, frequency and bit depth"); | ||||
|   u_int32_t configured_frequency = 0; | ||||
|  | ||||
|   // Configure LEDC channel, frequency and bit depth with fallback | ||||
|   int attempt_count_max = SETUP_ATTEMPT_COUNT_MAX; | ||||
|   while (attempt_count_max > 0 && configured_frequency == 0) { | ||||
|     ESP_LOGV(TAG, "Initializing channel %u with frequency %.1f and bit depth of %u", this->channel_, this->frequency_, | ||||
|              this->bit_depth_); | ||||
|     configured_frequency = ledcSetup(this->channel_, frequency, this->bit_depth_); | ||||
|     if (configured_frequency != 0) { | ||||
|       this->initialized_ = true; | ||||
|       this->status_clear_error(); | ||||
|       ESP_LOGV(TAG, "Configured frequency: %u with bit depth: %u", configured_frequency, this->bit_depth_); | ||||
|     } else { | ||||
|       ESP_LOGW(TAG, "Unable to initialize channel %u with frequency %.1f and bit depth of %u", this->channel_, | ||||
|                this->frequency_, this->bit_depth_); | ||||
|       // try again with a lower bit depth | ||||
|       this->bit_depth_--; | ||||
|     } | ||||
|     attempt_count_max--; | ||||
|   } | ||||
|  | ||||
|   if (configured_frequency == 0) { | ||||
|     ESP_LOGE(TAG, "Permanently failed to initialize channel %u with frequency %.1f and bit depth of %u", this->channel_, | ||||
|              this->frequency_, this->bit_depth_); | ||||
|     this->status_set_error(); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
| #endif  // USE_ARDUINO | ||||
| #ifdef USE_ESP_IDF | ||||
|   if (!this->initialized_) { | ||||
|     ESP_LOGW(TAG, "Not yet initialized"); | ||||
|     return; | ||||
| @@ -259,7 +197,7 @@ void LEDCOutput::update_frequency(float frequency) { | ||||
|   } | ||||
|  | ||||
|   this->status_clear_error(); | ||||
| #endif | ||||
|  | ||||
|   // re-apply duty | ||||
|   this->write_state(this->duty_); | ||||
| } | ||||
|   | ||||
| @@ -173,9 +173,9 @@ def _notify_old_style(config): | ||||
|  | ||||
| # The dev and latest branches will be at *least* this version, which is what matters. | ||||
| ARDUINO_VERSIONS = { | ||||
|     "dev": (cv.Version(1, 7, 0), "https://github.com/libretiny-eu/libretiny.git"), | ||||
|     "latest": (cv.Version(1, 7, 0), "libretiny"), | ||||
|     "recommended": (cv.Version(1, 7, 0), None), | ||||
|     "dev": (cv.Version(1, 9, 1), "https://github.com/libretiny-eu/libretiny.git"), | ||||
|     "latest": (cv.Version(1, 9, 1), "libretiny"), | ||||
|     "recommended": (cv.Version(1, 9, 1), None), | ||||
| } | ||||
|  | ||||
|  | ||||
| @@ -264,6 +264,7 @@ async def component_to_code(config): | ||||
|     # force using arduino framework | ||||
|     cg.add_platformio_option("framework", "arduino") | ||||
|     cg.add_build_flag("-DUSE_ARDUINO") | ||||
|     cg.set_cpp_standard("gnu++17") | ||||
|  | ||||
|     # disable library compatibility checks | ||||
|     cg.add_platformio_option("lib_ldf_mode", "off") | ||||
|   | ||||
| @@ -17,7 +17,7 @@ namespace light { | ||||
|  | ||||
| class LightOutput; | ||||
|  | ||||
| enum LightRestoreMode { | ||||
| enum LightRestoreMode : uint8_t { | ||||
|   LIGHT_RESTORE_DEFAULT_OFF, | ||||
|   LIGHT_RESTORE_DEFAULT_ON, | ||||
|   LIGHT_ALWAYS_OFF, | ||||
| @@ -212,12 +212,18 @@ class LightState : public EntityBase, public Component { | ||||
|  | ||||
|   /// Store the output to allow effects to have more access. | ||||
|   LightOutput *output_; | ||||
|   /// Value for storing the index of the currently active effect. 0 if no effect is active | ||||
|   uint32_t active_effect_index_{}; | ||||
|   /// The currently active transformer for this light (transition/flash). | ||||
|   std::unique_ptr<LightTransformer> transformer_{nullptr}; | ||||
|   /// Whether the light value should be written in the next cycle. | ||||
|   bool next_write_{true}; | ||||
|   /// List of effects for this light. | ||||
|   std::vector<LightEffect *> effects_; | ||||
|   /// Value for storing the index of the currently active effect. 0 if no effect is active | ||||
|   uint32_t active_effect_index_{}; | ||||
|   /// Default transition length for all transitions in ms. | ||||
|   uint32_t default_transition_length_{}; | ||||
|   /// Transition length to use for flash transitions. | ||||
|   uint32_t flash_transition_length_{}; | ||||
|   /// Gamma correction factor for the light. | ||||
|   float gamma_correct_{}; | ||||
|  | ||||
|   /// Object used to store the persisted values of the light. | ||||
|   ESPPreferenceObject rtc_; | ||||
| @@ -236,19 +242,13 @@ class LightState : public EntityBase, public Component { | ||||
|    */ | ||||
|   CallbackManager<void()> target_state_reached_callback_{}; | ||||
|  | ||||
|   /// Default transition length for all transitions in ms. | ||||
|   uint32_t default_transition_length_{}; | ||||
|   /// Transition length to use for flash transitions. | ||||
|   uint32_t flash_transition_length_{}; | ||||
|   /// Gamma correction factor for the light. | ||||
|   float gamma_correct_{}; | ||||
|   /// Restore mode of the light. | ||||
|   LightRestoreMode restore_mode_; | ||||
|   /// Initial state of the light. | ||||
|   optional<LightStateRTCState> initial_state_{}; | ||||
|   /// List of effects for this light. | ||||
|   std::vector<LightEffect *> effects_; | ||||
|  | ||||
|   /// Restore mode of the light. | ||||
|   LightRestoreMode restore_mode_; | ||||
|   /// Whether the light value should be written in the next cycle. | ||||
|   bool next_write_{true}; | ||||
|   // for effects, true if a transformer (transition) is active. | ||||
|   bool is_transformer_active_ = false; | ||||
| }; | ||||
|   | ||||
| @@ -324,7 +324,10 @@ async def to_code(config): | ||||
|     if CORE.using_arduino: | ||||
|         if config[CONF_HARDWARE_UART] == USB_CDC: | ||||
|             cg.add_build_flag("-DARDUINO_USB_CDC_ON_BOOT=1") | ||||
|             if CORE.is_esp32 and get_esp32_variant() == VARIANT_ESP32C3: | ||||
|             if CORE.is_esp32 and get_esp32_variant() in ( | ||||
|                 VARIANT_ESP32C3, | ||||
|                 VARIANT_ESP32C6, | ||||
|             ): | ||||
|                 cg.add_build_flag("-DARDUINO_USB_MODE=1") | ||||
|  | ||||
|     if CORE.using_esp_idf: | ||||
|   | ||||
| @@ -116,7 +116,7 @@ void Logger::log_vprintf_(int level, const char *tag, int line, const __FlashStr | ||||
|   if (this->baud_rate_ > 0) { | ||||
|     this->write_msg_(this->tx_buffer_ + msg_start); | ||||
|   } | ||||
|   this->call_log_callbacks_(level, tag, this->tx_buffer_ + msg_start); | ||||
|   this->log_callback_.call(level, tag, this->tx_buffer_ + msg_start); | ||||
|  | ||||
|   global_recursion_guard_ = false; | ||||
| } | ||||
| @@ -129,19 +129,6 @@ inline int Logger::level_for(const char *tag) { | ||||
|   return this->current_level_; | ||||
| } | ||||
|  | ||||
| void HOT Logger::call_log_callbacks_(int level, const char *tag, const char *msg) { | ||||
| #ifdef USE_ESP32 | ||||
|   // Suppress network-logging if memory constrained | ||||
|   // In some configurations (eg BLE enabled) there may be some transient | ||||
|   // memory exhaustion, and trying to log when OOM can lead to a crash. Skipping | ||||
|   // here usually allows the stack to recover instead. | ||||
|   // See issue #1234 for analysis. | ||||
|   if (xPortGetFreeHeapSize() < 2048) | ||||
|     return; | ||||
| #endif | ||||
|   this->log_callback_.call(level, tag, msg); | ||||
| } | ||||
|  | ||||
| Logger::Logger(uint32_t baud_rate, size_t tx_buffer_size) : baud_rate_(baud_rate), tx_buffer_size_(tx_buffer_size) { | ||||
|   // add 1 to buffer size for null terminator | ||||
|   this->tx_buffer_ = new char[this->tx_buffer_size_ + 1];  // NOLINT | ||||
| @@ -189,7 +176,7 @@ void Logger::loop() { | ||||
|                                   this->tx_buffer_size_); | ||||
|       this->write_footer_to_buffer_(this->tx_buffer_, &this->tx_buffer_at_, this->tx_buffer_size_); | ||||
|       this->tx_buffer_[this->tx_buffer_at_] = '\0'; | ||||
|       this->call_log_callbacks_(message->level, message->tag, this->tx_buffer_); | ||||
|       this->log_callback_.call(message->level, message->tag, this->tx_buffer_); | ||||
|       // At this point all the data we need from message has been transferred to the tx_buffer | ||||
|       // so we can release the message to allow other tasks to use it as soon as possible. | ||||
|       this->log_buffer_->release_message_main_loop(received_token); | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user