ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
delta_wallbox.cpp
Go to the documentation of this file.
1#include "delta_wallbox.h"
2#include "esphome/core/log.h"
4#include <algorithm>
5#include <array>
6#include <cctype>
7#include <cstdio>
8#include <cstdlib>
9#include <cstring>
10
11#ifdef USE_ESP32
12
14ESPHOME_LOG_TAG(TAG, "delta_wallbox");
15// Modbus function codes
16static constexpr uint8_t FC_READ_HOLDING_REGISTERS = 0x03;
17static constexpr uint8_t FC_WRITE_SINGLE_REGISTER = 0x06;
18static constexpr uint8_t FC_WRITE_MULTIPLE_REGISTERS = 0x10;
19static constexpr uint8_t FC_EXCEPTION_FLAG = 0x80;
20
21// Response lengths in bytes, CRC included
22static constexpr size_t READ_RESPONSE_OVERHEAD = 5;
23static constexpr size_t WRITE_RESPONSE_LEN = 8;
24static constexpr size_t EXCEPTION_RESPONSE_LEN = 5;
25
26// Frame bodies without CRC
27static constexpr size_t LOGIN_BODY_LEN = 35;
28static constexpr size_t LOGIN_PASS_LEN = 22;
29static constexpr size_t LOGIN_USER_LEN = 6;
30static constexpr size_t TIME_SYNC_BODY_LEN = 15;
31
32static constexpr uint8_t REGISTER_BYTE_COUNT = 2;
33static constexpr uint8_t TWO_REGISTER_BYTE_COUNT = 4;
34static constexpr uint8_t CHARGING_INFO_BYTE_COUNT = 18;
35static constexpr uint8_t SERIAL_TEXT_BYTE_COUNT = 14;
36static constexpr uint32_t RETRANSMIT_TIMEOUT_MS = 4000;
37static constexpr uint32_t RETRANSMIT_TIMEOUT_ID = 0;
38static constexpr uint8_t MAX_PAIRING_ATTEMPTS = 5;
39static constexpr uint16_t PAIRING_STATUS_READY = 0x0007;
40static constexpr uint16_t PAIRING_STATUS_WAITING = 0x0008;
41static constexpr uint8_t LOGIN_ACCEPTED = 0x01;
42
43static constexpr uint8_t CMD_POLL_CHARGING[] = {0x01, 0x03, 0x01, 0x1B, 0x00, 0x09};
44static constexpr uint8_t CMD_GET_MODEL[] = {0x01, 0x03, 0x01, 0x85, 0x00, 0x0E};
45static constexpr uint8_t CMD_GET_BT_FW_VERSION[] = {0x02, 0x03, 0x01, 0x00, 0x00, 0x01};
46static constexpr uint8_t CMD_CHECK_PAIRING[] = {0x02, 0x03, 0x01, 0x00, 0x00, 0x03};
47static constexpr uint8_t CMD_GET_LOGIN_USER[] = {0x01, 0x03, 0x01, 0x7D, 0x00, 0x03};
48static constexpr uint8_t CMD_READ_LOGIN_STATUS[] = {0x01, 0x03, 0x01, 0x80, 0x00, 0x01};
49static constexpr uint8_t CMD_GET_LOGIN_ROLE[] = {0x01, 0x03, 0x01, 0x81, 0x00, 0x01};
50static constexpr uint8_t CMD_GET_CHARGER_FW_VERSION[] = {0x01, 0x03, 0x01, 0x10, 0x00, 0x02};
51static constexpr uint8_t CMD_GET_MAX_CURRENT[] = {0x01, 0x03, 0x01, 0x12, 0x00, 0x02};
52static constexpr uint8_t CMD_GET_SERIAL_NUMBER[] = {0x01, 0x03, 0x01, 0x09, 0x00, 0x01};
53static constexpr uint8_t CMD_GET_ERROR_CODE[] = {0x01, 0x03, 0x01, 0xB5, 0x00, 0x01};
54static constexpr uint8_t CMD_START_CHARGING[] = {0x01, 0x06, 0x01, 0x02, 0x00, 0x01};
55static constexpr uint8_t CMD_STOP_CHARGING[] = {0x01, 0x06, 0x01, 0x03, 0x00, 0x01};
56
57// Indexed by DeltaState
58PROGMEM_STRING_TABLE(DeltaStateStrings, "CHECK_PAIRING", "CONNECTED", "GET_CHARGER_FW_VERSION", "GET_BT_FW_VERSION",
59 "GET_LOGIN_RESULT", "GET_LOGIN_RESULT0", "GET_LOGIN_ROLE", "GET_LOGIN_USER", "GET_MODEL",
60 "GET_SERIAL_NUMBER", "HALTED", "IDLE", "SEND_LOGIN_CREDENTIALS", "TIME_SYNC");
61static_assert(DeltaStateStrings::LAST_INDEX == static_cast<uint8_t>(DeltaState::DELTA_STATE_TIME_SYNC),
62 "DeltaStateStrings must list every DeltaState");
63static const LogString *delta_state_to_string(DeltaState state) {
64 return DeltaStateStrings::get_log_str(static_cast<uint8_t>(state), DeltaStateStrings::LAST_INDEX);
65}
66
67// The function code a handshake step waits for; 0 where any frame is handled
68static uint8_t expected_function_code(DeltaState state) {
69 switch (state) {
73 return 0;
76 return FC_WRITE_MULTIPLE_REGISTERS;
77 default:
78 return FC_READ_HOLDING_REGISTERS;
79 }
80}
81
82// UUID references
83static const esp32_ble_tracker::ESPBTUUID SERVICE_UUID =
84 esp32_ble_tracker::ESPBTUUID::from_raw("6e400001-b5a3-f393-e0a9-e50e24dcca9e");
85static const esp32_ble_tracker::ESPBTUUID TX_UUID =
86 esp32_ble_tracker::ESPBTUUID::from_raw("6e400002-b5a3-f393-e0a9-e50e24dcca9e");
87static const esp32_ble_tracker::ESPBTUUID RX_UUID =
88 esp32_ble_tracker::ESPBTUUID::from_raw("6e400003-b5a3-f393-e0a9-e50e24dcca9e");
89
90// Appends the little-endian Modbus CRC-16 to the len bytes in frame and returns the new length.
91static size_t append_crc(uint8_t *frame, size_t len) {
92 const uint16_t crc = crc16(frame, static_cast<uint16_t>(len));
93 frame[len] = static_cast<uint8_t>(crc & 0xFF);
94 frame[len + 1] = static_cast<uint8_t>(crc >> 8);
95 return len + 2;
96}
97
98static bool verify_crc(const uint8_t *data, size_t len) {
99 if (len < EXCEPTION_RESPONSE_LEN)
100 return false;
101 const uint16_t calculated = crc16(data, static_cast<uint16_t>(len - 2));
102 const uint16_t received = data[len - 2] | (data[len - 1] << 8);
103 return calculated == received;
104}
105
106// Login write body: 22-byte password and 6-byte user, both left-padded with zeros
107static std::array<uint8_t, LOGIN_BODY_LEN> build_login_body(StringRef user, StringRef password) {
108 std::array<uint8_t, LOGIN_BODY_LEN> frame = {
109 0x01, 0x10, 0x01, 0x72, 0x00, 0x0E, 0x1C, // Unit, FC16, register 0x0172, 14 registers, 28 bytes
110 };
111
112 const size_t pass_len = std::min(password.size(), LOGIN_PASS_LEN);
113 std::copy_n(password.c_str(), pass_len, frame.begin() + 7 + (LOGIN_PASS_LEN - pass_len));
114
115 uint8_t *user_chunk = frame.data() + 7 + LOGIN_PASS_LEN;
116 bool is_digit = !user.empty();
117 for (size_t i = 0; i < user.size() && is_digit; i++) {
118 is_digit = isdigit(static_cast<unsigned char>(user.c_str()[i]));
119 }
120 if (is_digit) {
121 const uint64_t val = strtoull(user.c_str(), nullptr, 10);
122 for (size_t i = 0; i < LOGIN_USER_LEN; i++) {
123 user_chunk[LOGIN_USER_LEN - 1 - i] = static_cast<uint8_t>((val >> (8 * i)) & 0xFF);
124 }
125 } else {
126 const size_t user_len = std::min(user.size(), LOGIN_USER_LEN);
127 std::copy_n(user.c_str(), user_len, user_chunk + LOGIN_USER_LEN - user_len);
128 }
129 return frame;
130}
131
132static std::array<uint8_t, TIME_SYNC_BODY_LEN> build_time_sync_body(int year, int month, int day, int hour, int minute,
133 int second, int weekday) {
134 const uint8_t weekday_bitmap = static_cast<uint8_t>(1 << weekday); // Sunday=bit0, Mon=bit1...
135 return {
136 0x01,
137 0x10,
138 0x01,
139 0x14,
140 0x00,
141 0x04,
142 0x08, // Unit, FC16, register 0x0114, 4 registers, 8 bytes
143 static_cast<uint8_t>(year % 100),
144 static_cast<uint8_t>(month),
145 weekday_bitmap,
146 static_cast<uint8_t>(day),
147 static_cast<uint8_t>(hour),
148 static_cast<uint8_t>(minute),
149 static_cast<uint8_t>(second),
150 0x00,
151 };
152}
153
155 ESP_LOGCONFIG(TAG, "Delta Wallbox:");
156 ESP_LOGCONFIG(TAG, " User: %s", this->user_.c_str());
157 ESP_LOGCONFIG(TAG, " BLE PIN configured: %s", this->has_pin_ ? LOG_STR_LITERAL("YES") : LOG_STR_LITERAL("NO"));
158 LOG_UPDATE_INTERVAL(this);
159}
160
162 if (this->is_connected()) {
163 // Start poll flow sequence
164 ESP_LOGD(TAG, "Polling charging info...");
165 this->send_frame_(CMD_POLL_CHARGING, sizeof(CMD_POLL_CHARGING));
166 }
167}
168
169void DeltaWallbox::write_frame_(const uint8_t *frame, size_t len) {
170 if (this->tx_char_handle_ == 0) {
171 ESP_LOGW(TAG, "TX characteristic handle is not ready while in state %s. Dropping %u-byte frame.",
172 LOG_STR_ARG(delta_state_to_string(this->state_)), (unsigned int) len);
173 return;
174 }
175 ESP_LOGD(TAG, "Sending %u-byte frame in state %s (conn_id=%u, tx_handle=0x%04X).", (unsigned int) len,
176 LOG_STR_ARG(delta_state_to_string(this->state_)), this->parent_->get_conn_id(), this->tx_char_handle_);
177 auto status =
178 esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->tx_char_handle_, len,
179 const_cast<uint8_t *>(frame), ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
180 if (status != ESP_GATT_OK) {
181 ESP_LOGW(TAG, "esp_ble_gattc_write_char failed, status=%d", status);
182 }
183}
184
185// Copies body into a stack buffer, appends the CRC and writes the frame.
186void DeltaWallbox::send_frame_(const uint8_t *body, size_t len) {
187 std::array<uint8_t, MAX_FRAME_LEN> frame{};
188 std::copy_n(body, len, frame.begin());
189 this->write_frame_(frame.data(), append_crc(frame.data(), len));
190}
191
193 count = std::min(count, this->rx_len_);
194 std::memmove(this->rx_buffer_.data(), this->rx_buffer_.data() + count, this->rx_len_ - count);
195 this->rx_len_ -= count;
196}
197
199 ESP_LOGD(TAG, "Preparing command for state %s.", LOG_STR_ARG(delta_state_to_string(this->state_)));
200 switch (this->state_) {
202 ESP_LOGI(TAG, "Requesting charger model name...");
203 this->send_frame_(CMD_GET_MODEL, sizeof(CMD_GET_MODEL));
204 break;
206 ESP_LOGI(TAG, "Requesting BT firmware version...");
207 this->send_frame_(CMD_GET_BT_FW_VERSION, sizeof(CMD_GET_BT_FW_VERSION));
208 break;
210 ESP_LOGI(TAG, "Checking pairing status...");
211 this->send_frame_(CMD_CHECK_PAIRING, sizeof(CMD_CHECK_PAIRING));
212 break;
214 ESP_LOGI(TAG, "Querying user profile...");
215 this->send_frame_(CMD_GET_LOGIN_USER, sizeof(CMD_GET_LOGIN_USER));
216 break;
218 ESP_LOGI(TAG, "Checking pre-login status register...");
219 this->send_frame_(CMD_READ_LOGIN_STATUS, sizeof(CMD_READ_LOGIN_STATUS));
220 break;
222 ESP_LOGI(TAG, "Transmitting login credentials...");
223 const auto body = build_login_body(this->user_, this->password_);
224 this->send_frame_(body.data(), body.size());
225 break;
226 }
228 ESP_LOGI(TAG, "Verifying login result...");
229 this->send_frame_(CMD_READ_LOGIN_STATUS, sizeof(CMD_READ_LOGIN_STATUS));
230 break;
232 ESP_LOGI(TAG, "Checking role privileges...");
233 this->send_frame_(CMD_GET_LOGIN_ROLE, sizeof(CMD_GET_LOGIN_ROLE));
234 break;
236#ifdef USE_TIME
237 // Only a valid time is written; the charger keeps its own clock otherwise
238 if (this->time_id_ != nullptr) {
239 auto tm = this->time_id_->now();
240 if (tm.is_valid()) {
241 ESP_LOGI(TAG, "Synchronizing wallbox clock...");
242 // ESPTime day_of_week is 1 (Sunday) to 7 (Saturday); the charger counts from 0
243 const auto body = build_time_sync_body(tm.year, tm.month, tm.day_of_month, tm.hour, tm.minute, tm.second,
244 tm.day_of_week - 1);
245 this->send_frame_(body.data(), body.size());
246 break;
247 }
248 }
249#endif
251 break;
252 }
254 ESP_LOGI(TAG, "Requesting charger main firmware version...");
255 this->send_frame_(CMD_GET_CHARGER_FW_VERSION, sizeof(CMD_GET_CHARGER_FW_VERSION));
256 break;
258 ESP_LOGI(TAG, "Requesting wallbox serial number...");
259 this->send_frame_(CMD_GET_SERIAL_NUMBER, sizeof(CMD_GET_SERIAL_NUMBER));
260 break;
261 default:
262 this->cancel_timeout(RETRANSMIT_TIMEOUT_ID);
263 return;
264 }
265 this->set_timeout(RETRANSMIT_TIMEOUT_ID, RETRANSMIT_TIMEOUT_MS, [this]() { this->retransmit_(); });
266}
267
269 ESP_LOGW(TAG, "No answer in login state %s within %u ms. Sending the command again...",
270 LOG_STR_ARG(delta_state_to_string(this->state_)), (unsigned int) RETRANSMIT_TIMEOUT_MS);
272}
273
275 ESP_LOGD(TAG, "State transition: %s -> %s", LOG_STR_ARG(delta_state_to_string(this->state_)),
276 LOG_STR_ARG(delta_state_to_string(next)));
277 this->state_ = next;
279}
280
281void DeltaWallbox::handle_frame_(const uint8_t *frame, size_t len) {
282 ESP_LOGD(TAG, "Received valid frame in state %s: func=0x%02X len=%u",
283 LOG_STR_ARG(delta_state_to_string(this->state_)), frame[1], (unsigned int) len);
284 if (frame[1] & FC_EXCEPTION_FLAG) {
285 ESP_LOGE(TAG, "Modbus exception received in state %s! Error code: 0x%02X",
286 LOG_STR_ARG(delta_state_to_string(this->state_)), frame[2]);
288 // The clock write is optional, so a charger that refuses it is used without it
289 ESP_LOGW(TAG, "Charger refused the clock sync; continuing without it");
291 } else if (expected_function_code(this->state_) != 0) {
292 // The charger refused a handshake step; sending the same request again would not change that
293 this->halt_(LOG_STR("Modbus exception during login"));
294 }
295 return;
296 }
297 const uint8_t expected = expected_function_code(this->state_);
298 if (expected != 0 && frame[1] != expected) {
299 // A late answer to a retransmitted request; the pending one is answered or sent again by the timer
300 ESP_LOGD(TAG, "Ignoring func=0x%02X reply while waiting for 0x%02X", frame[1], expected);
301 return;
302 }
303
304 switch (this->state_) {
306 break; // No command is pending; ignore the frame.
308 char model[MAX_FRAME_LEN];
309 const size_t data_len = std::min<size_t>(frame[2], len - READ_RESPONSE_OVERHEAD);
310 size_t model_len = 0;
311 for (size_t i = 0; i < data_len; i++) {
312 if (frame[3 + i] != '\0')
313 model[model_len++] = static_cast<char>(frame[3 + i]);
314 }
315 model[model_len] = '\0';
316 ESP_LOGI(TAG, "Discovered Wallbox Model Name: %s", model);
317 if (this->model_name_text_sensor_ != nullptr) {
318 this->model_name_text_sensor_->publish_state(model);
319 }
321 break;
322 }
324 if (frame[2] >= REGISTER_BYTE_COUNT) {
325 uint8_t major = frame[3] % 16;
326 uint8_t minor = frame[4];
327 char ver[16];
328 snprintf(ver, sizeof(ver), "%d.%d", major, minor);
329 ESP_LOGI(TAG, "Discovered Wallbox BT FW: %s", ver);
330 if (this->bt_fw_version_text_sensor_ != nullptr) {
331 this->bt_fw_version_text_sensor_->publish_state(ver);
332 }
333 }
335 break;
336 }
338 if (frame[2] >= REGISTER_BYTE_COUNT) {
339 uint16_t status = (frame[3] << 8) | frame[4];
340 ESP_LOGI(TAG, "Pairing Status Register: 0x%04X", status);
341 if (status == PAIRING_STATUS_READY) {
342 this->pairing_in_progress_ = false;
343 this->pairing_attempts_ = 0;
344 ESP_LOGI(TAG, "Pairing ready and confirmed!");
346 } else if (status == PAIRING_STATUS_WAITING) {
347 ESP_LOGW(TAG, "Wallbox is waiting to be paired by the system controller (attempt %u, in_progress=%s).",
348 this->pairing_attempts_ + 1,
349 this->pairing_in_progress_ ? LOG_STR_LITERAL("YES") : LOG_STR_LITERAL("NO"));
350 this->pairing_attempts_++;
351 if (!this->pairing_in_progress_) {
352 ESP_LOGI(TAG, "Initiating BLE pairing with wallbox.");
353 auto err = this->parent_->pair();
354 if (err != ESP_OK) {
355 ESP_LOGW(TAG, "Failed to initiate BLE pairing, err=%d", err);
356 } else {
357 this->pairing_in_progress_ = true;
358 }
359 }
360 } else {
361 ESP_LOGW(TAG, "Unexpected pairing status 0x%04X", status);
362 this->pairing_attempts_++;
363 }
364 if (this->pairing_attempts_ >= MAX_PAIRING_ATTEMPTS) {
365 this->halt_(LOG_STR("Too many failed/waiting pairing checks"));
366 }
367 } else {
368 ESP_LOGW(TAG, "Pairing status response too short: byte_count=%u frame_len=%u", frame[2], (unsigned int) len);
369 }
370 break;
371 }
374 break;
375 }
377 if (frame[2] == REGISTER_BYTE_COUNT) {
379 }
380 break;
381 }
384 break;
385 }
387 // Only the one-register status answer counts; a late reply to another read has a different length
388 if (frame[2] == REGISTER_BYTE_COUNT) {
389 uint8_t login_val = frame[4]; // respondent data_lo
390 if (login_val == LOGIN_ACCEPTED) {
391 ESP_LOGI(TAG, "Credentials accepted! Logged in as %s.", this->user_.c_str());
393 } else {
394 ESP_LOGE(TAG, "Login validation failed (0x%02X). Check password.", login_val);
395 this->halt_(LOG_STR("Bad credentials"));
396 }
397 }
398 break;
399 }
401 break; // Authentication is halted; ignore any late replies.
402 }
404 if (frame[2] == REGISTER_BYTE_COUNT) {
406 }
407 break;
408 }
411 break;
412 }
414 if (frame[2] >= TWO_REGISTER_BYTE_COUNT) {
415 char ver[16];
416 snprintf(ver, sizeof(ver), "%d.%d", frame[3], frame[4]);
417 ESP_LOGI(TAG, "Discovered Charger Main FW: %s", ver);
418 if (this->fw_version_text_sensor_ != nullptr) {
419 this->fw_version_text_sensor_->publish_state(ver);
420 }
421 }
423 break;
424 }
426 // Serial read (0x0109) returns 14 ASCII bytes instead of one register
427 if (frame[2] >= SERIAL_TEXT_BYTE_COUNT) {
428 char serial[SERIAL_TEXT_BYTE_COUNT + 1];
429 size_t serial_len = 0;
430 for (size_t i = 0; i < SERIAL_TEXT_BYTE_COUNT; i++) {
431 if (frame[3 + i] != '\0')
432 serial[serial_len++] = static_cast<char>(frame[3 + i]);
433 }
434 serial[serial_len] = '\0';
435 ESP_LOGI(TAG, "Wallbox serial text: %s", serial);
436 if (this->serial_number_text_sensor_ != nullptr)
437 this->serial_number_text_sensor_->publish_state(serial);
438 }
440 ESP_LOGI(TAG, "Authentication complete. Core parameters loaded.");
441 this->status_clear_warning();
442 this->update(); // perform first poll update immediately
443 break;
444 }
446 // Connected polling responses
447 if (frame[1] == FC_READ_HOLDING_REGISTERS) {
448 uint8_t byte_count = frame[2];
449 if (byte_count == CHARGING_INFO_BYTE_COUNT) {
450 // Parsing 0x011B charging info (9 registers)
451 uint32_t power_raw = (frame[3] << 24) | (frame[4] << 16) | (frame[5] << 8) | frame[6];
452 uint32_t energy_raw = (frame[7] << 24) | (frame[8] << 16) | (frame[9] << 8) | frame[10];
453 uint8_t minutes = frame[15];
454 uint8_t hours = frame[16];
455 uint8_t state_byte = frame[17];
456
457 float power_kw = power_raw / 1000.0f;
458 float energy_kwh = energy_raw / 1000.0f;
459 float charging_minutes = hours * 60 + minutes;
460
461 if (this->power_sensor_ != nullptr)
462 this->power_sensor_->publish_state(power_kw);
463 if (this->energy_sensor_ != nullptr)
464 this->energy_sensor_->publish_state(energy_kwh);
465 if (this->charging_time_sensor_ != nullptr)
466 this->charging_time_sensor_->publish_state(charging_minutes);
467 if (this->charger_state_sensor_ != nullptr)
468 this->charger_state_sensor_->publish_state(state_byte);
469
470 // Retrieve maximum current output setting next
471 this->send_frame_(CMD_GET_MAX_CURRENT, sizeof(CMD_GET_MAX_CURRENT));
472 } else if (byte_count == TWO_REGISTER_BYTE_COUNT) {
473 // Parsing 0x0112 max output setting (2 registers)
474 uint32_t current_raw = (frame[3] << 24) | (frame[4] << 16) | (frame[5] << 8) | frame[6];
475 float amps = current_raw / 10.0f;
476 if (this->max_current_sensor_ != nullptr) {
477 this->max_current_sensor_->publish_state(amps);
478 }
479
480 // Poll the error code register every cycle
481 this->send_frame_(CMD_GET_ERROR_CODE, sizeof(CMD_GET_ERROR_CODE));
482 } else if (byte_count == REGISTER_BYTE_COUNT) {
483 // Error status register (0x01B5)
484 uint16_t reg_val = (frame[3] << 8) | frame[4];
485 char hex_str[16];
486 snprintf(hex_str, sizeof(hex_str), "%04X", reg_val);
487 if (this->error_code_text_sensor_ != nullptr)
488 this->error_code_text_sensor_->publish_state(hex_str);
489 }
490 } else if (frame[1] == FC_WRITE_SINGLE_REGISTER) {
491 // Confirmation frame of Start/Stop write actions
492 ESP_LOGI(TAG, "Wallbox charging command successfully acknowledged.");
493 }
494 break;
495 }
496 }
497}
498
500 ESP_LOGW(TAG, "Resetting component state from %s. pairing_attempts=%u, pairing_in_progress=%s, rx_buffer=%u bytes",
501 LOG_STR_ARG(delta_state_to_string(this->state_)), this->pairing_attempts_,
502 this->pairing_in_progress_ ? LOG_STR_LITERAL("YES") : LOG_STR_LITERAL("NO"), (unsigned int) this->rx_len_);
503 this->cancel_timeout(RETRANSMIT_TIMEOUT_ID);
504 this->rx_len_ = 0;
506 this->pairing_attempts_ = 0;
507 this->pairing_in_progress_ = false;
508 this->status_set_warning();
509}
510
511void DeltaWallbox::halt_(const LogString *reason) {
512 ESP_LOGE(TAG, "Halting authentication: %s. A reconnect is required to retry.", LOG_STR_ARG(reason));
513 this->cancel_timeout(RETRANSMIT_TIMEOUT_ID);
514 this->rx_len_ = 0;
516 this->pairing_attempts_ = 0;
517 this->pairing_in_progress_ = false;
518 this->status_set_warning(reason);
519}
520
521void DeltaWallbox::set_charging_state(bool state) {
522 if (!this->is_connected()) {
523 ESP_LOGW(TAG, "Cannot send charging command while wallbox is not fully connected.");
524 return;
525 }
526
527 if (state) {
528 ESP_LOGI(TAG, "Sending remote START charging command...");
529 this->send_frame_(CMD_START_CHARGING, sizeof(CMD_START_CHARGING));
530 } else {
531 ESP_LOGI(TAG, "Sending remote STOP charging command...");
532 this->send_frame_(CMD_STOP_CHARGING, sizeof(CMD_STOP_CHARGING));
533 }
534}
535
536void DeltaWallbox::start_charging() { this->set_charging_state(true); }
537
538void DeltaWallbox::stop_charging() { this->set_charging_state(false); }
539
540void DeltaWallbox::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
541 esp_ble_gattc_cb_param_t *param) {
542 switch (event) {
543 case ESP_GATTC_DISCONNECT_EVT: {
544 ESP_LOGW(TAG, "Disconnected from Bluetooth wallbox client (conn_id=%u, reason=%d). Resetting state.",
545 param->disconnect.conn_id, param->disconnect.reason);
546 this->reset_state_();
548 break;
549 }
550 case ESP_GATTC_OPEN_EVT: {
551 ESP_LOGI(TAG, "GATT open event received: status=%d conn_id=%u", param->open.status, param->open.conn_id);
552 break;
553 }
554 case ESP_GATTC_SEARCH_CMPL_EVT: {
555 ESP_LOGI(TAG, "GATT service discovery completed: status=%d", param->search_cmpl.status);
556 auto *tx_char = this->parent_->get_characteristic(SERVICE_UUID, TX_UUID);
557 auto *rx_char = this->parent_->get_characteristic(SERVICE_UUID, RX_UUID);
558 if (tx_char == nullptr || rx_char == nullptr) {
559 ESP_LOGE(TAG, "Could not discover matching NUS TX/RX service on this BLE device.");
560 this->reset_state_();
561 break;
562 }
563 this->tx_char_handle_ = tx_char->handle;
564 this->rx_char_handle_ = rx_char->handle;
565
566 esp_err_t err = this->parent_->register_for_notify(this->rx_char_handle_);
567 if (err != ESP_OK) {
568 ESP_LOGW(TAG, "register_for_notify failed, status=%d", err);
569 }
570 break;
571 }
572 case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
573 ESP_LOGI(TAG, "Register-for-notify event: status=%d handle=0x%04X", param->reg_for_notify.status,
574 param->reg_for_notify.handle);
575 if (param->reg_for_notify.handle == this->rx_char_handle_) {
576 ESP_LOGI(TAG, "Subscribed to RX notification handle 0x%04X successfully.", this->rx_char_handle_);
579 ESP_LOGI(TAG, "GATT connection established. Starting authentication state machine.");
581 }
582 }
583 break;
584 }
585 case ESP_GATTC_NOTIFY_EVT: {
586 if (param->notify.handle != this->rx_char_handle_)
587 break;
588
589 const size_t value_len = param->notify.value_len;
590 ESP_LOGD(TAG, "Notification received on handle 0x%04X with %u bytes. Buffer before append=%u bytes.",
591 param->notify.handle, param->notify.value_len, (unsigned int) this->rx_len_);
592 if (value_len > MAX_FRAME_LEN - this->rx_len_) {
593 ESP_LOGW(TAG, "Notification overflows the %u-byte RX buffer. Discarding buffered data.",
594 (unsigned int) MAX_FRAME_LEN);
595 this->rx_len_ = 0;
596 if (value_len > MAX_FRAME_LEN)
597 break;
598 }
599 std::memcpy(this->rx_buffer_.data() + this->rx_len_, param->notify.value, value_len);
600 this->rx_len_ += value_len;
601
602 bool progress = true;
603 while (progress && this->rx_len_ >= 3) {
604 progress = false;
605 uint8_t func_code = this->rx_buffer_[1];
606 size_t expected_len = 0;
607
608 if (func_code == FC_READ_HOLDING_REGISTERS) {
609 expected_len = this->rx_buffer_[2] + READ_RESPONSE_OVERHEAD;
610 } else if (func_code == FC_WRITE_MULTIPLE_REGISTERS || func_code == FC_WRITE_SINGLE_REGISTER) {
611 expected_len = WRITE_RESPONSE_LEN;
612 } else if (func_code == (FC_EXCEPTION_FLAG | FC_READ_HOLDING_REGISTERS) ||
613 func_code == (FC_EXCEPTION_FLAG | FC_WRITE_MULTIPLE_REGISTERS) ||
614 func_code == (FC_EXCEPTION_FLAG | FC_WRITE_SINGLE_REGISTER)) {
615 expected_len = EXCEPTION_RESPONSE_LEN;
616 }
617
618 if (expected_len > 0) {
619 if (this->rx_len_ >= expected_len) {
620 if (verify_crc(this->rx_buffer_.data(), expected_len)) {
621 // The frame stays in rx_buffer_ during handle_frame_(); it is dropped afterwards.
622 this->handle_frame_(this->rx_buffer_.data(), expected_len);
623 this->drop_rx_bytes_(expected_len);
624 progress = true;
625 } else {
626 ESP_LOGW(TAG, "CRC mismatch while parsing notification data in state %s. Dropping one byte from buffer.",
627 LOG_STR_ARG(delta_state_to_string(this->state_)));
628 // Shift by one byte to realign with the next possible frame start
629 this->drop_rx_bytes_(1);
630 progress = true;
631 }
632 }
633 } else {
634 ESP_LOGW(TAG,
635 "Unsupported function code 0x%02X while parsing notification data in state %s. Advancing buffer.",
636 func_code, LOG_STR_ARG(delta_state_to_string(this->state_)));
637 this->drop_rx_bytes_(1);
638 progress = true;
639 }
640 }
641 break;
642 }
643 default:
644 break;
645 }
646}
647
648void DeltaWallbox::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
649 switch (event) {
650 case ESP_GAP_BLE_PASSKEY_REQ_EVT: {
651 if (!this->parent_->check_addr(param->ble_security.ble_req.bd_addr))
652 return;
653
654 if (!this->has_pin_) {
655 ESP_LOGW(TAG, "Wallbox requested a BLE passkey, but no pin is configured. Set pin: under delta_wallbox:.");
656 return;
657 }
658
659 ESP_LOGI(TAG, "Wallbox requested a BLE passkey. Replying with configured YAML pin.");
660 esp_err_t err = esp_ble_passkey_reply(param->ble_security.ble_req.bd_addr, true, this->pin_);
661 if (err != ESP_OK) {
662 ESP_LOGW(TAG, "Failed to send BLE passkey reply: %s", esp_err_to_name(err));
663 }
664 break;
665 }
666 case ESP_GAP_BLE_AUTH_CMPL_EVT:
667 if (!this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr))
668 return;
669
670 this->pairing_in_progress_ = false;
671 if (param->ble_security.auth_cmpl.success) {
672 ESP_LOGI(TAG,
673 "BLE pairing completed successfully. auth_mode=%d addr_type=%d. Re-checking wallbox pairing status.",
674 param->ble_security.auth_cmpl.auth_mode, param->ble_security.auth_cmpl.addr_type);
675 this->pairing_attempts_ = 0;
679 }
680 } else {
681 ESP_LOGW(TAG, "BLE pairing failed, reason=%d", param->ble_security.auth_cmpl.fail_reason);
682 }
683 break;
684 default:
685 break;
686 }
687}
688
689} // namespace esphome::delta_wallbox
690
691#endif
uint8_t status
Definition bl0942.h:8
bool cancel_timeout(const char *name)
Cancel a timeout function.
void set_timeout(const char *name, uint32_t timeout, std::function< void()> &&f)
Set a timeout function with a const char* name.
Definition component.cpp:96
void status_clear_warning()
Definition component.h:296
constexpr const char * c_str() const
Definition string_ref.h:78
static ESPBTUUID from_raw(const uint8_t *data)
Construct from raw 16-byte little-endian UUID.
void write_frame_(const uint8_t *frame, size_t len)
void halt_(const LogString *reason)
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 handle_frame_(const uint8_t *frame, size_t len)
std::array< uint8_t, MAX_FRAME_LEN > rx_buffer_
void send_frame_(const uint8_t *body, size_t len)
void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override
esp_err_t register_for_notify(uint16_t char_handle)
Register for notifications, holding the service release until the registration completes.
BLECharacteristic * get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr)
ESPTime now()
Get the time in the currently defined timezone.
uint8_t month
Definition date_entity.h:1
uint16_t year
Definition date_entity.h:0
uint8_t day
Definition date_entity.h:2
uint8_t second
uint8_t minute
uint8_t hour
NameCopy * next
bool state
Definition fan.h:2
unsigned count
Definition gif.cpp:51
mopeka_std_values val[3]
ESPHOME_LOG_TAG(TAG, "delta_wallbox")
PROGMEM_STRING_TABLE(DeltaStateStrings, "CHECK_PAIRING", "CONNECTED", "GET_CHARGER_FW_VERSION", "GET_BT_FW_VERSION", "GET_LOGIN_RESULT", "GET_LOGIN_RESULT0", "GET_LOGIN_ROLE", "GET_LOGIN_USER", "GET_MODEL", "GET_SERIAL_NUMBER", "HALTED", "IDLE", "SEND_LOGIN_CREDENTIALS", "TIME_SYNC")
std::span< const uint8_t > data
uint16_t crc16(const uint8_t *data, uint16_t len, uint16_t crc, uint16_t reverse_poly, bool refin, bool refout)
Calculate a CRC-16 checksum of data with size len.
Definition helpers.cpp:95
const void size_t len
Definition hal.h:68
static void uint32_t