ESPHome 2026.1.1
Loading...
Searching...
No Matches
esp32_improv_component.cpp
Go to the documentation of this file.
2
8#include "esphome/core/log.h"
9
10#ifdef USE_ESP32
11
12namespace esphome {
13namespace esp32_improv {
14
15using namespace bytebuffer;
16
17static const char *const TAG = "esp32_improv.component";
18static constexpr size_t IMPROV_MAX_LOG_BYTES = 128;
19static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
20static constexpr uint16_t STOP_ADVERTISING_DELAY =
21 10000; // Delay (ms) before stopping service to allow BLE clients to read the final state
22static constexpr uint16_t NAME_ADVERTISING_INTERVAL = 60000; // Advertise name every 60 seconds
23static constexpr uint16_t NAME_ADVERTISING_DURATION = 1000; // Advertise name for 1 second
24
25// Improv service data constants
26static constexpr uint8_t IMPROV_SERVICE_DATA_SIZE = 8;
27static constexpr uint8_t IMPROV_PROTOCOL_ID_1 = 0x77; // 'P' << 1 | 'R' >> 7
28static constexpr uint8_t IMPROV_PROTOCOL_ID_2 = 0x46; // 'I' << 1 | 'M' >> 7
29
31
33#ifdef USE_BINARY_SENSOR
34 if (this->authorizer_ != nullptr) {
35 this->authorizer_->add_on_state_callback([this](bool state) {
36 if (state) {
37 this->authorized_start_ = millis();
38 this->identify_start_ = 0;
39 }
40 });
41 }
42#endif
43 global_ble_server->on_disconnect([this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
44
45 // Start with loop disabled - will be enabled by start() when needed
46 this->disable_loop();
47}
48
52 BLEDescriptor *status_descriptor = new BLE2902();
53 this->status_->add_descriptor(status_descriptor);
54
57 BLEDescriptor *error_descriptor = new BLE2902();
58 this->error_->add_descriptor(error_descriptor);
59
60 this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE);
61 this->rpc_->on_write([this](std::span<const uint8_t> data, uint16_t id) {
62 if (!data.empty()) {
63 this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end());
64 }
65 });
66 BLEDescriptor *rpc_descriptor = new BLE2902();
67 this->rpc_->add_descriptor(rpc_descriptor);
68
71 BLEDescriptor *rpc_response_descriptor = new BLE2902();
72 this->rpc_response_->add_descriptor(rpc_response_descriptor);
73
74 this->capabilities_ =
75 this->service_->create_characteristic(improv::CAPABILITIES_UUID, BLECharacteristic::PROPERTY_READ);
76 BLEDescriptor *capabilities_descriptor = new BLE2902();
77 this->capabilities_->add_descriptor(capabilities_descriptor);
78 uint8_t capabilities = 0x00;
79#ifdef USE_OUTPUT
80 if (this->status_indicator_ != nullptr)
81 capabilities |= improv::CAPABILITY_IDENTIFY;
82#endif
83 this->capabilities_->set_value(ByteBuffer::wrap(capabilities));
84 this->setup_complete_ = true;
85}
86
89 if (this->state_ != improv::STATE_STOPPED) {
90 this->state_ = improv::STATE_STOPPED;
91#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
92 this->state_callback_.call(this->state_, this->error_state_);
93#endif
94 }
95 this->incoming_data_.clear();
96 return;
97 }
98 if (this->service_ == nullptr) {
99 // Setup the service
100 ESP_LOGD(TAG, "Creating Improv service");
101 this->service_ = global_ble_server->create_service(ESPBTUUID::from_raw(improv::SERVICE_UUID), true);
102 this->setup_characteristics();
103 }
104
105 if (!this->incoming_data_.empty())
107 uint32_t now = App.get_loop_component_start_time();
108
109 // Check if we need to update advertising type
110 if (this->state_ != improv::STATE_STOPPED && this->state_ != improv::STATE_PROVISIONED) {
112 }
113
114 switch (this->state_) {
115 case improv::STATE_STOPPED:
116 this->set_status_indicator_state_(false);
117
118 if (this->should_start_ && this->setup_complete_) {
119 if (this->service_->is_created()) {
120 this->service_->start();
121 } else if (this->service_->is_running()) {
122 // Start by advertising the device name first BEFORE setting any state
123 ESP_LOGV(TAG, "Starting with device name advertising");
124 this->advertising_device_name_ = true;
126 esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>{}, true);
128
129 // Set initial state based on whether we have an authorizer
130 this->set_state_(this->get_initial_state_(), false);
131 this->set_error_(improv::ERROR_NONE);
132 this->should_start_ = false; // Clear flag after starting
133 ESP_LOGD(TAG, "Service started!");
134 }
135 }
136 break;
137 case improv::STATE_AWAITING_AUTHORIZATION: {
138#ifdef USE_BINARY_SENSOR
139 if (this->authorizer_ == nullptr ||
140 (this->authorized_start_ != 0 && ((now - this->authorized_start_) < this->authorized_duration_))) {
141 this->set_state_(improv::STATE_AUTHORIZED);
142 } else {
143 if (!this->check_identify_())
144 this->set_status_indicator_state_(true);
145 }
146#else
147 this->set_state_(improv::STATE_AUTHORIZED);
148#endif
150 break;
151 }
152 case improv::STATE_AUTHORIZED: {
153#ifdef USE_BINARY_SENSOR
154 if (this->authorizer_ != nullptr && now - this->authorized_start_ > this->authorized_duration_) {
155 ESP_LOGD(TAG, "Authorization timeout");
156 this->set_state_(improv::STATE_AWAITING_AUTHORIZATION);
157 return;
158 }
159#endif
160 if (!this->check_identify_()) {
161 this->set_status_indicator_state_((now % 1000) < 500);
162 }
164 break;
165 }
166 case improv::STATE_PROVISIONING: {
167 this->set_status_indicator_state_((now % 200) < 100);
169 break;
170 }
171 case improv::STATE_PROVISIONED: {
172 this->incoming_data_.clear();
173 this->set_status_indicator_state_(false);
174 // Provisioning complete, no further loop execution needed
175 this->disable_loop();
176 break;
177 }
178 }
179}
180
182#ifdef USE_OUTPUT
183 if (this->status_indicator_ == nullptr)
184 return;
185 if (this->status_indicator_state_ == state)
186 return;
188 if (state) {
189 this->status_indicator_->turn_on();
190 } else {
192 }
193#endif
194}
195
196#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
198 switch (state) {
199 case improv::STATE_STOPPED:
200 return "STOPPED";
201 case improv::STATE_AWAITING_AUTHORIZATION:
202 return "AWAITING_AUTHORIZATION";
203 case improv::STATE_AUTHORIZED:
204 return "AUTHORIZED";
205 case improv::STATE_PROVISIONING:
206 return "PROVISIONING";
207 case improv::STATE_PROVISIONED:
208 return "PROVISIONED";
209 default:
210 return "UNKNOWN";
211 }
212}
213#endif
214
216 uint32_t now = millis();
217
218 bool identify = this->identify_start_ != 0 && now - this->identify_start_ <= this->identify_duration_;
219
220 if (identify) {
221 uint32_t time = now % 1000;
222 this->set_status_indicator_state_(time < 600 && time % 200 < 100);
223 }
224 return identify;
225}
226
227void ESP32ImprovComponent::set_state_(improv::State state, bool update_advertising) {
228 // Skip if state hasn't changed
229 if (this->state_ == state) {
230 return;
231 }
232
233#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
234 ESP_LOGD(TAG, "State transition: %s (0x%02X) -> %s (0x%02X)", this->state_to_string_(this->state_), this->state_,
235 this->state_to_string_(state), state);
236#endif
237 this->state_ = state;
238 if (this->status_ != nullptr && (this->status_->get_value().empty() || this->status_->get_value()[0] != state)) {
239 this->status_->set_value(ByteBuffer::wrap(static_cast<uint8_t>(state)));
240 if (state != improv::STATE_STOPPED)
241 this->status_->notify();
242 }
243 // Only advertise valid Improv states (0x01-0x04).
244 // STATE_STOPPED (0x00) is internal only and not part of the Improv spec.
245 // Advertising 0x00 causes undefined behavior in some clients and makes them
246 // repeatedly connect trying to determine the actual state.
247 if (state != improv::STATE_STOPPED && update_advertising) {
248 // State change always overrides name advertising and resets the timer
249 this->advertising_device_name_ = false;
250 // Reset the timer so we wait another 60 seconds before advertising name
252 // Advertise the new state via service data
254 }
255#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
256 this->state_callback_.call(this->state_, this->error_state_);
257#endif
258}
259
260void ESP32ImprovComponent::set_error_(improv::Error error) {
261 if (error != improv::ERROR_NONE) {
262 ESP_LOGE(TAG, "Error: %d", error);
263 }
264 // The error_ characteristic is initialized in setup_characteristics() which is called
265 // from the loop, while the BLE disconnect callback is registered in setup().
266 // error_ can be nullptr if:
267 // 1. A client connects/disconnects before setup_characteristics() is called
268 // 2. The device is already provisioned so the service never starts (should_start_ is false)
269 if (this->error_ != nullptr && (this->error_->get_value().empty() || this->error_->get_value()[0] != error)) {
270 this->error_->set_value(ByteBuffer::wrap(static_cast<uint8_t>(error)));
271 if (this->state_ != improv::STATE_STOPPED)
272 this->error_->notify();
273 }
274}
275
276void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &&response) {
277 this->rpc_response_->set_value(std::move(response));
278 if (this->state_ != improv::STATE_STOPPED)
279 this->rpc_response_->notify();
280}
281
283 if (this->should_start_ || this->state_ != improv::STATE_STOPPED)
284 return;
285
286 ESP_LOGD(TAG, "Setting Improv to start");
287 this->should_start_ = true;
288 this->enable_loop();
289}
290
292 this->should_start_ = false;
293 // Wait before stopping the service to ensure all BLE clients see the state change.
294 // This prevents clients from repeatedly reconnecting and wasting resources by allowing
295 // them to observe that the device is provisioned before the service disappears.
296 this->set_timeout("end-service", STOP_ADVERTISING_DELAY, [this] {
297 if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr)
298 return;
299 this->service_->stop();
300 this->set_state_(improv::STATE_STOPPED);
301 });
302}
303
305
307 ESP_LOGCONFIG(TAG, "ESP32 Improv:");
308#ifdef USE_BINARY_SENSOR
309 LOG_BINARY_SENSOR(" ", "Authorizer", this->authorizer_);
310#endif
311#ifdef USE_OUTPUT
312 ESP_LOGCONFIG(TAG, " Status Indicator: '%s'", YESNO(this->status_indicator_ != nullptr));
313#endif
314}
315
317 uint8_t length = this->incoming_data_[1];
318
319#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
320 char hex_buf[format_hex_pretty_size(IMPROV_MAX_LOG_BYTES)];
321 ESP_LOGV(TAG, "Processing bytes - %s",
322 format_hex_pretty_to(hex_buf, this->incoming_data_.data(), this->incoming_data_.size()));
323#endif
324 if (this->incoming_data_.size() - 3 == length) {
325 this->set_error_(improv::ERROR_NONE);
326 improv::ImprovCommand command = improv::parse_improv_data(this->incoming_data_);
327 switch (command.command) {
328 case improv::BAD_CHECKSUM:
329 ESP_LOGW(TAG, "Error decoding Improv payload");
330 this->set_error_(improv::ERROR_INVALID_RPC);
331 this->incoming_data_.clear();
332 break;
333 case improv::WIFI_SETTINGS: {
334 if (this->state_ != improv::STATE_AUTHORIZED) {
335 ESP_LOGW(TAG, "Settings received, but not authorized");
336 this->set_error_(improv::ERROR_NOT_AUTHORIZED);
337 this->incoming_data_.clear();
338 return;
339 }
340 wifi::WiFiAP sta{};
341 sta.set_ssid(command.ssid);
342 sta.set_password(command.password);
343 this->connecting_sta_ = sta;
344
347 this->set_state_(improv::STATE_PROVISIONING);
348 ESP_LOGD(TAG, "Received Improv Wi-Fi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
349 command.password.c_str());
350
351 auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this);
352 this->set_timeout("wifi-connect-timeout", 30000, f);
353 this->incoming_data_.clear();
354 break;
355 }
356 case improv::IDENTIFY:
357 this->incoming_data_.clear();
358 this->identify_start_ = millis();
359 break;
360 default:
361 ESP_LOGW(TAG, "Unknown Improv payload");
362 this->set_error_(improv::ERROR_UNKNOWN_RPC);
363 this->incoming_data_.clear();
364 }
365 } else if (this->incoming_data_.size() - 2 > length) {
366 ESP_LOGV(TAG, "Too much data received or data malformed; resetting buffer");
367 this->incoming_data_.clear();
368 } else {
369 ESP_LOGV(TAG, "Waiting for split data packets");
370 }
371}
372
374 this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
375 this->set_state_(improv::STATE_AUTHORIZED);
376#ifdef USE_BINARY_SENSOR
377 if (this->authorizer_ != nullptr)
378 this->authorized_start_ = millis();
379#endif
380 ESP_LOGW(TAG, "Timed out while connecting to Wi-Fi network");
382}
383
385 if (!wifi::global_wifi_component->is_connected()) {
386 return;
387 }
388
389 if (this->state_ == improv::STATE_PROVISIONING) {
390 wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password());
391 this->connecting_sta_ = {};
392 this->cancel_timeout("wifi-connect-timeout");
393
394 // Build URL list with minimal allocations
395 // Maximum 3 URLs: custom next_url + ESPHOME_MY_LINK + webserver URL
396 std::string url_strings[3];
397 size_t url_count = 0;
398
399#ifdef USE_ESP32_IMPROV_NEXT_URL
400 // Add next_url if configured (should be first per Improv BLE spec)
401 {
402 char url_buffer[384];
403 size_t len = this->get_formatted_next_url_(url_buffer, sizeof(url_buffer));
404 if (len > 0) {
405 url_strings[url_count++] = std::string(url_buffer, len);
406 }
407 }
408#endif
409
410 // Add default URLs for backward compatibility
411 url_strings[url_count++] = ESPHOME_MY_LINK;
412#ifdef USE_WEBSERVER
413 for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
414 if (ip.is_ip4()) {
415 // "http://" (7) + IPv4 max (15) + ":" (1) + port max (5) + null = 29
416 char url_buffer[32];
417 memcpy(url_buffer, "http://", 7); // NOLINT(bugprone-not-null-terminated-result) - str_to null-terminates
418 ip.str_to(url_buffer + 7);
419 size_t len = strlen(url_buffer);
420 snprintf(url_buffer + len, sizeof(url_buffer) - len, ":%d", USE_WEBSERVER_PORT);
421 url_strings[url_count++] = url_buffer;
422 break;
423 }
424 }
425#endif
426 this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS,
427 std::vector<std::string>(url_strings, url_strings + url_count)));
428 } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
429 ESP_LOGD(TAG, "WiFi provisioned externally");
430 }
431
432 this->set_state_(improv::STATE_PROVISIONED);
433 this->stop();
434}
435
437 uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {};
438 service_data[0] = IMPROV_PROTOCOL_ID_1; // PR
439 service_data[1] = IMPROV_PROTOCOL_ID_2; // IM
440 service_data[2] = static_cast<uint8_t>(this->state_);
441
442 uint8_t capabilities = 0x00;
443#ifdef USE_OUTPUT
444 if (this->status_indicator_ != nullptr)
445 capabilities |= improv::CAPABILITY_IDENTIFY;
446#endif
447
448 service_data[3] = capabilities;
449 // service_data[4-7] are already 0 (Reserved)
450
451 // Atomically set service data and disable name in advertising
452 esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>(service_data), false);
453}
454
456 uint32_t now = App.get_loop_component_start_time();
457
458 // If we're advertising the device name and it's been more than NAME_ADVERTISING_DURATION, switch back to service data
459 if (this->advertising_device_name_) {
460 if (now - this->last_name_adv_time_ >= NAME_ADVERTISING_DURATION) {
461 ESP_LOGV(TAG, "Switching back to service data advertising");
462 this->advertising_device_name_ = false;
463 // Restore service data advertising
465 }
466 return;
467 }
468
469 // Check if it's time to advertise the device name (every NAME_ADVERTISING_INTERVAL)
470 if (now - this->last_name_adv_time_ >= NAME_ADVERTISING_INTERVAL) {
471 ESP_LOGV(TAG, "Switching to device name advertising");
472 this->advertising_device_name_ = true;
473 this->last_name_adv_time_ = now;
474
475 // Atomically clear service data and enable name in advertising data
476 esp32_ble::global_ble->advertising_set_service_data_and_name(std::span<const uint8_t>{}, true);
477 }
478}
479
481#ifdef USE_BINARY_SENSOR
482 // If we have an authorizer, start in awaiting authorization state
483 return this->authorizer_ == nullptr ? improv::STATE_AUTHORIZED : improv::STATE_AWAITING_AUTHORIZATION;
484#else
485 // No binary_sensor support = no authorizer possible, start as authorized
486 return improv::STATE_AUTHORIZED;
487#endif
488}
489
490ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
491
492} // namespace esp32_improv
493} // namespace esphome
494
495#endif
uint32_t IRAM_ATTR HOT get_loop_component_start_time() const
Get the cached time in milliseconds from when the current component started its loop execution.
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") void set_timeout(const std voi set_timeout)(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
Definition component.h:445
void enable_loop()
Enable this component's loop.
void disable_loop()
Disable this component's loop.
ESPDEPRECATED("Use const char* or uint32_t overload instead. Removed in 2026.7.0", "2026.1.0") bool cancel_timeout(const std boo cancel_timeout)(const char *name)
Cancel a timeout function.
Definition component.h:465
void add_on_state_callback(std::function< void(T)> &&callback)
static ByteBuffer wrap(T value, Endian endianness=LITTLE)
Definition bytebuffer.h:156
void advertising_set_service_data_and_name(std::span< const uint8_t > data, bool include_name)
Definition ble.cpp:106
static ESPBTUUID from_raw(const uint8_t *data)
Definition ble_uuid.cpp:29
void on_write(std::function< void(std::span< const uint8_t >, uint16_t)> &&callback)
void add_descriptor(BLEDescriptor *descriptor)
BLEService * create_service(ESPBTUUID uuid, bool advertise=false, uint16_t num_handles=15)
void on_disconnect(std::function< void(uint16_t)> &&callback)
Definition ble_server.h:62
BLECharacteristic * create_characteristic(const std::string &uuid, esp_gatt_char_prop_t properties)
void send_response_(std::vector< uint8_t > &&response)
CallbackManager< void(improv::State, improv::Error)> state_callback_
void set_state_(improv::State state, bool update_advertising=true)
const char * state_to_string_(improv::State state)
size_t get_formatted_next_url_(char *buffer, size_t buffer_size)
Format next_url_ into buffer, replacing placeholders. Returns length written.
virtual void turn_off()
Disable this binary output.
virtual void turn_on()
Enable this binary output.
const std::string & get_ssid() const
void set_ssid(const std::string &ssid)
void set_sta(const WiFiAP &ap)
void save_wifi_sta(const std::string &ssid, const std::string &password)
void start_connecting(const WiFiAP &ap)
bool state
Definition fan.h:0
ESP32BLE * global_ble
Definition ble.cpp:673
ESP32ImprovComponent * global_improv_component
const float AFTER_BLUETOOTH
Definition component.cpp:84
WiFiComponent * global_wifi_component
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
std::string size_t len
Definition helpers.h:595
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:334
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:830
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:25
Application App
Global storage of Application pointer - only one Application can exist.
uint16_t length
Definition tt21100.cpp:0