ESPHome  2023.11.6
esp32_improv_component.cpp
Go to the documentation of this file.
2 
6 #include "esphome/core/log.h"
7 
8 #ifdef USE_ESP32
9 
10 namespace esphome {
11 namespace esp32_improv {
12 
13 static const char *const TAG = "esp32_improv.component";
14 static const char *const ESPHOME_MY_LINK = "https://my.home-assistant.io/redirect/config_flow_start?domain=esphome";
15 
17 
19 #ifdef USE_BINARY_SENSOR
20  if (this->authorizer_ != nullptr) {
21  this->authorizer_->add_on_state_callback([this](bool state) {
22  if (state) {
23  this->authorized_start_ = millis();
24  this->identify_start_ = 0;
25  }
26  });
27  }
28 #endif
29 }
30 
32  this->status_ = this->service_->create_characteristic(
34  BLEDescriptor *status_descriptor = new BLE2902();
35  this->status_->add_descriptor(status_descriptor);
36 
37  this->error_ = this->service_->create_characteristic(
39  BLEDescriptor *error_descriptor = new BLE2902();
40  this->error_->add_descriptor(error_descriptor);
41 
42  this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE);
43  this->rpc_->on_write([this](const std::vector<uint8_t> &data) {
44  if (!data.empty()) {
45  this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end());
46  }
47  });
48  BLEDescriptor *rpc_descriptor = new BLE2902();
49  this->rpc_->add_descriptor(rpc_descriptor);
50 
53  BLEDescriptor *rpc_response_descriptor = new BLE2902();
54  this->rpc_response_->add_descriptor(rpc_response_descriptor);
55 
56  this->capabilities_ =
57  this->service_->create_characteristic(improv::CAPABILITIES_UUID, BLECharacteristic::PROPERTY_READ);
58  BLEDescriptor *capabilities_descriptor = new BLE2902();
59  this->capabilities_->add_descriptor(capabilities_descriptor);
60  uint8_t capabilities = 0x00;
61 #ifdef USE_OUTPUT
62  if (this->status_indicator_ != nullptr)
63  capabilities |= improv::CAPABILITY_IDENTIFY;
64 #endif
65  this->capabilities_->set_value(capabilities);
66  this->setup_complete_ = true;
67 }
68 
70  if (!global_ble_server->is_running()) {
72  this->incoming_data_.clear();
73  return;
74  }
75  if (this->service_ == nullptr) {
76  // Setup the service
77  ESP_LOGD(TAG, "Creating Improv service");
78  global_ble_server->create_service(ESPBTUUID::from_raw(improv::SERVICE_UUID), true);
79  this->service_ = global_ble_server->get_service(ESPBTUUID::from_raw(improv::SERVICE_UUID));
80  this->setup_characteristics();
81  }
82 
83  if (!this->incoming_data_.empty())
84  this->process_incoming_data_();
85  uint32_t now = millis();
86 
87  switch (this->state_) {
89  this->set_status_indicator_state_(false);
90 
91  if (this->service_->is_created() && this->should_start_ && this->setup_complete_) {
92  if (this->service_->is_running()) {
94 
95  this->set_state_(improv::STATE_AWAITING_AUTHORIZATION);
96  this->set_error_(improv::ERROR_NONE);
97  ESP_LOGD(TAG, "Service started!");
98  } else {
99  this->service_->start();
100  }
101  }
102  break;
103  case improv::STATE_AWAITING_AUTHORIZATION: {
104 #ifdef USE_BINARY_SENSOR
105  if (this->authorizer_ == nullptr ||
106  (this->authorized_start_ != 0 && ((now - this->authorized_start_) < this->authorized_duration_))) {
107  this->set_state_(improv::STATE_AUTHORIZED);
108  } else
109 #else
110  this->set_state_(improv::STATE_AUTHORIZED);
111 #endif
112  {
113  if (!this->check_identify_())
114  this->set_status_indicator_state_(true);
115  }
116  break;
117  }
118  case improv::STATE_AUTHORIZED: {
119 #ifdef USE_BINARY_SENSOR
120  if (this->authorizer_ != nullptr) {
121  if (now - this->authorized_start_ > this->authorized_duration_) {
122  ESP_LOGD(TAG, "Authorization timeout");
123  this->set_state_(improv::STATE_AWAITING_AUTHORIZATION);
124  return;
125  }
126  }
127 #endif
128  if (!this->check_identify_()) {
129  this->set_status_indicator_state_((now % 1000) < 500);
130  }
131  break;
132  }
133  case improv::STATE_PROVISIONING: {
134  this->set_status_indicator_state_((now % 200) < 100);
137  this->connecting_sta_.get_password());
138  this->connecting_sta_ = {};
139  this->cancel_timeout("wifi-connect-timeout");
140  this->set_state_(improv::STATE_PROVISIONED);
141 
142  std::vector<std::string> urls = {ESPHOME_MY_LINK};
143 #ifdef USE_WEBSERVER
145  std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
146  urls.push_back(webserver_url);
147 #endif
148  std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
149  this->send_response_(data);
150  this->stop();
151  }
152  break;
153  }
154  case improv::STATE_PROVISIONED: {
155  this->incoming_data_.clear();
156  this->set_status_indicator_state_(false);
157  break;
158  }
159  }
160 }
161 
163 #ifdef USE_OUTPUT
164  if (this->status_indicator_ == nullptr)
165  return;
166  if (this->status_indicator_state_ == state)
167  return;
169  if (state) {
170  this->status_indicator_->turn_on();
171  } else {
172  this->status_indicator_->turn_off();
173  }
174 #endif
175 }
176 
178  uint32_t now = millis();
179 
180  bool identify = this->identify_start_ != 0 && now - this->identify_start_ <= this->identify_duration_;
181 
182  if (identify) {
183  uint32_t time = now % 1000;
184  this->set_status_indicator_state_(time < 600 && time % 200 < 100);
185  }
186  return identify;
187 }
188 
190  ESP_LOGV(TAG, "Setting state: %d", state);
191  this->state_ = state;
192  if (this->status_->get_value().empty() || this->status_->get_value()[0] != state) {
193  uint8_t data[1]{state};
194  this->status_->set_value(data, 1);
195  if (state != improv::STATE_STOPPED)
196  this->status_->notify();
197  }
198  std::vector<uint8_t> service_data(8, 0);
199  service_data[0] = 0x77; // PR
200  service_data[1] = 0x46; // IM
201  service_data[2] = static_cast<uint8_t>(state);
202 
203  uint8_t capabilities = 0x00;
204 #ifdef USE_OUTPUT
205  if (this->status_indicator_ != nullptr)
206  capabilities |= improv::CAPABILITY_IDENTIFY;
207 #endif
208 
209  service_data[3] = capabilities;
210  service_data[4] = 0x00; // Reserved
211  service_data[5] = 0x00; // Reserved
212  service_data[6] = 0x00; // Reserved
213  service_data[7] = 0x00; // Reserved
214 
216 }
217 
219  if (error != improv::ERROR_NONE) {
220  ESP_LOGE(TAG, "Error: %d", error);
221  }
222  if (this->error_->get_value().empty() || this->error_->get_value()[0] != error) {
223  uint8_t data[1]{error};
224  this->error_->set_value(data, 1);
225  if (this->state_ != improv::STATE_STOPPED)
226  this->error_->notify();
227  }
228 }
229 
230 void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) {
231  this->rpc_response_->set_value(response);
232  if (this->state_ != improv::STATE_STOPPED)
233  this->rpc_response_->notify();
234 }
235 
237  if (this->should_start_ || this->state_ != improv::STATE_STOPPED)
238  return;
239 
240  ESP_LOGD(TAG, "Setting Improv to start");
241  this->should_start_ = true;
242 }
243 
245  this->should_start_ = false;
246  this->set_timeout("end-service", 1000, [this] {
247  if (this->state_ == improv::STATE_STOPPED || this->service_ == nullptr)
248  return;
249  this->service_->stop();
251  });
252 }
253 
255 
257  ESP_LOGCONFIG(TAG, "ESP32 Improv:");
258 #ifdef USE_BINARY_SENSOR
259  LOG_BINARY_SENSOR(" ", "Authorizer", this->authorizer_);
260 #endif
261 #ifdef USE_OUTPUT
262  ESP_LOGCONFIG(TAG, " Status Indicator: '%s'", YESNO(this->status_indicator_ != nullptr));
263 #endif
264 }
265 
267  uint8_t length = this->incoming_data_[1];
268 
269  ESP_LOGD(TAG, "Processing bytes - %s", format_hex_pretty(this->incoming_data_).c_str());
270  if (this->incoming_data_.size() - 3 == length) {
271  this->set_error_(improv::ERROR_NONE);
272  improv::ImprovCommand command = improv::parse_improv_data(this->incoming_data_);
273  switch (command.command) {
274  case improv::BAD_CHECKSUM:
275  ESP_LOGW(TAG, "Error decoding Improv payload");
276  this->set_error_(improv::ERROR_INVALID_RPC);
277  this->incoming_data_.clear();
278  break;
279  case improv::WIFI_SETTINGS: {
280  if (this->state_ != improv::STATE_AUTHORIZED) {
281  ESP_LOGW(TAG, "Settings received, but not authorized");
282  this->set_error_(improv::ERROR_NOT_AUTHORIZED);
283  this->incoming_data_.clear();
284  return;
285  }
286  wifi::WiFiAP sta{};
287  sta.set_ssid(command.ssid);
288  sta.set_password(command.password);
289  this->connecting_sta_ = sta;
290 
293  this->set_state_(improv::STATE_PROVISIONING);
294  ESP_LOGD(TAG, "Received Improv wifi settings ssid=%s, password=" LOG_SECRET("%s"), command.ssid.c_str(),
295  command.password.c_str());
296 
297  auto f = std::bind(&ESP32ImprovComponent::on_wifi_connect_timeout_, this);
298  this->set_timeout("wifi-connect-timeout", 30000, f);
299  this->incoming_data_.clear();
300  break;
301  }
302  case improv::IDENTIFY:
303  this->incoming_data_.clear();
304  this->identify_start_ = millis();
305  break;
306  default:
307  ESP_LOGW(TAG, "Unknown Improv payload");
308  this->set_error_(improv::ERROR_UNKNOWN_RPC);
309  this->incoming_data_.clear();
310  }
311  } else if (this->incoming_data_.size() - 2 > length) {
312  ESP_LOGV(TAG, "Too much data came in, or malformed resetting buffer...");
313  this->incoming_data_.clear();
314  } else {
315  ESP_LOGV(TAG, "Waiting for split data packets...");
316  }
317 }
318 
320  this->set_error_(improv::ERROR_UNABLE_TO_CONNECT);
321  this->set_state_(improv::STATE_AUTHORIZED);
322 #ifdef USE_BINARY_SENSOR
323  if (this->authorizer_ != nullptr)
324  this->authorized_start_ = millis();
325 #endif
326  ESP_LOGW(TAG, "Timed out trying to connect to given WiFi network");
328 }
329 
330 void ESP32ImprovComponent::on_client_disconnect() { this->set_error_(improv::ERROR_NONE); };
331 
332 ESP32ImprovComponent *global_improv_component = nullptr; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
333 
334 } // namespace esp32_improv
335 } // namespace esphome
336 
337 #endif
virtual void turn_on()
Enable this binary output.
Definition: binary_output.h:43
std::string format_hex_pretty(const uint8_t *data, size_t length)
Format the byte array data of length len in pretty-printed, human-readable hex.
Definition: helpers.cpp:350
ESP32BLE * global_ble
Definition: ble.cpp:394
bool cancel_timeout(const std::string &name)
Cancel a timeout function.
Definition: component.cpp:72
const std::string & get_password() const
void save_wifi_sta(const std::string &ssid, const std::string &password)
virtual void turn_off()
Disable this binary output.
Definition: binary_output.h:51
std::string str() const
Definition: ip_address.h:97
void set_timeout(const std::string &name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a unique name.
Definition: component.cpp:68
void create_service(ESPBTUUID uuid, bool advertise=false, uint16_t num_handles=15, uint8_t inst_id=0)
Definition: ble_server.cpp:118
void set_value(const uint8_t *data, size_t length)
const float AFTER_BLUETOOTH
Definition: component.cpp:21
bool is_connected()
Return whether the node is connected to the network (through wifi, eth, ...)
Definition: util.cpp:15
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:25
void send_response_(std::vector< uint8_t > &response)
BLEService * get_service(ESPBTUUID uuid)
Definition: ble_server.cpp:143
void advertising_set_service_data(const std::vector< uint8_t > &data)
Definition: ble.cpp:64
void set_ssid(const std::string &ssid)
WiFiComponent * global_wifi_component
void on_write(const std::function< void(const std::vector< uint8_t > &)> &&func)
ESP32ImprovComponent * global_improv_component
std::string to_string(int value)
Definition: helpers.cpp:74
void add_descriptor(BLEDescriptor *descriptor)
void add_on_state_callback(std::function< void(bool)> &&callback)
Add a callback to be notified of state changes.
uint16_t length
Definition: tt21100.cpp:12
void set_sta(const WiFiAP &ap)
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
static ESPBTUUID from_raw(const uint8_t *data)
Definition: ble_uuid.cpp:28
const std::string & get_ssid() const
BLECharacteristic * create_characteristic(const std::string &uuid, esp_gatt_char_prop_t properties)
Definition: ble_service.cpp:34
bool state
Definition: fan.h:34