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;
22static constexpr size_t READ_RESPONSE_OVERHEAD = 5;
23static constexpr size_t WRITE_RESPONSE_LEN = 8;
24static constexpr size_t EXCEPTION_RESPONSE_LEN = 5;
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;
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;
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};
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");
62 "DeltaStateStrings must list every DeltaState");
64 return DeltaStateStrings::get_log_str(
static_cast<uint8_t
>(
state), DeltaStateStrings::LAST_INDEX);
76 return FC_WRITE_MULTIPLE_REGISTERS;
78 return FC_READ_HOLDING_REGISTERS;
83static const esp32_ble_tracker::ESPBTUUID SERVICE_UUID =
85static const esp32_ble_tracker::ESPBTUUID TX_UUID =
87static const esp32_ble_tracker::ESPBTUUID RX_UUID =
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);
98static bool verify_crc(
const uint8_t *data,
size_t len) {
99 if (
len < EXCEPTION_RESPONSE_LEN)
101 const uint16_t calculated =
crc16(data,
static_cast<uint16_t
>(
len - 2));
103 return calculated == received;
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,
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));
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]));
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);
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);
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);
143 static_cast<uint8_t
>(
year % 100),
144 static_cast<uint8_t
>(
month),
146 static_cast<uint8_t
>(
day),
147 static_cast<uint8_t
>(
hour),
148 static_cast<uint8_t
>(
minute),
149 static_cast<uint8_t
>(
second),
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);
164 ESP_LOGD(TAG,
"Polling charging info...");
165 this->
send_frame_(CMD_POLL_CHARGING,
sizeof(CMD_POLL_CHARGING));
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);
175 ESP_LOGD(TAG,
"Sending %u-byte frame in state %s (conn_id=%u, tx_handle=0x%04X).", (
unsigned int)
len,
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);
187 std::array<uint8_t, MAX_FRAME_LEN> frame{};
188 std::copy_n(body,
len, frame.begin());
199 ESP_LOGD(TAG,
"Preparing command for state %s.", LOG_STR_ARG(delta_state_to_string(this->
state_)));
202 ESP_LOGI(TAG,
"Requesting charger model name...");
203 this->
send_frame_(CMD_GET_MODEL,
sizeof(CMD_GET_MODEL));
206 ESP_LOGI(TAG,
"Requesting BT firmware version...");
207 this->
send_frame_(CMD_GET_BT_FW_VERSION,
sizeof(CMD_GET_BT_FW_VERSION));
210 ESP_LOGI(TAG,
"Checking pairing status...");
211 this->
send_frame_(CMD_CHECK_PAIRING,
sizeof(CMD_CHECK_PAIRING));
214 ESP_LOGI(TAG,
"Querying user profile...");
215 this->
send_frame_(CMD_GET_LOGIN_USER,
sizeof(CMD_GET_LOGIN_USER));
218 ESP_LOGI(TAG,
"Checking pre-login status register...");
219 this->
send_frame_(CMD_READ_LOGIN_STATUS,
sizeof(CMD_READ_LOGIN_STATUS));
222 ESP_LOGI(TAG,
"Transmitting login credentials...");
228 ESP_LOGI(TAG,
"Verifying login result...");
229 this->
send_frame_(CMD_READ_LOGIN_STATUS,
sizeof(CMD_READ_LOGIN_STATUS));
232 ESP_LOGI(TAG,
"Checking role privileges...");
233 this->
send_frame_(CMD_GET_LOGIN_ROLE,
sizeof(CMD_GET_LOGIN_ROLE));
241 ESP_LOGI(TAG,
"Synchronizing wallbox clock...");
243 const auto body = build_time_sync_body(tm.year, tm.month, tm.day_of_month, tm.hour, tm.minute, tm.second,
254 ESP_LOGI(TAG,
"Requesting charger main firmware version...");
255 this->
send_frame_(CMD_GET_CHARGER_FW_VERSION,
sizeof(CMD_GET_CHARGER_FW_VERSION));
258 ESP_LOGI(TAG,
"Requesting wallbox serial number...");
259 this->
send_frame_(CMD_GET_SERIAL_NUMBER,
sizeof(CMD_GET_SERIAL_NUMBER));
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);
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)));
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]);
289 ESP_LOGW(TAG,
"Charger refused the clock sync; continuing without it");
291 }
else if (expected_function_code(this->
state_) != 0) {
293 this->
halt_(LOG_STR(
"Modbus exception during login"));
297 const uint8_t expected = expected_function_code(this->
state_);
298 if (expected != 0 && frame[1] != expected) {
300 ESP_LOGD(TAG,
"Ignoring func=0x%02X reply while waiting for 0x%02X", frame[1], expected);
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]);
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);
324 if (frame[2] >= REGISTER_BYTE_COUNT) {
325 uint8_t major = frame[3] % 16;
326 uint8_t minor = frame[4];
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);
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) {
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).",
352 ESP_LOGI(TAG,
"Initiating BLE pairing with wallbox.");
355 ESP_LOGW(TAG,
"Failed to initiate BLE pairing, err=%d", err);
361 ESP_LOGW(TAG,
"Unexpected pairing status 0x%04X",
status);
365 this->
halt_(LOG_STR(
"Too many failed/waiting pairing checks"));
368 ESP_LOGW(TAG,
"Pairing status response too short: byte_count=%u frame_len=%u", frame[2], (
unsigned int)
len);
377 if (frame[2] == REGISTER_BYTE_COUNT) {
388 if (frame[2] == REGISTER_BYTE_COUNT) {
389 uint8_t login_val = frame[4];
390 if (login_val == LOGIN_ACCEPTED) {
391 ESP_LOGI(TAG,
"Credentials accepted! Logged in as %s.", this->
user_.
c_str());
394 ESP_LOGE(TAG,
"Login validation failed (0x%02X). Check password.", login_val);
395 this->
halt_(LOG_STR(
"Bad credentials"));
404 if (frame[2] == REGISTER_BYTE_COUNT) {
414 if (frame[2] >= TWO_REGISTER_BYTE_COUNT) {
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);
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]);
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);
440 ESP_LOGI(TAG,
"Authentication complete. Core parameters loaded.");
447 if (frame[1] == FC_READ_HOLDING_REGISTERS) {
448 uint8_t byte_count = frame[2];
449 if (byte_count == CHARGING_INFO_BYTE_COUNT) {
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];
457 float power_kw = power_raw / 1000.0f;
458 float energy_kwh = energy_raw / 1000.0f;
459 float charging_minutes = hours * 60 + minutes;
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);
471 this->
send_frame_(CMD_GET_MAX_CURRENT,
sizeof(CMD_GET_MAX_CURRENT));
472 }
else if (byte_count == TWO_REGISTER_BYTE_COUNT) {
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);
481 this->
send_frame_(CMD_GET_ERROR_CODE,
sizeof(CMD_GET_ERROR_CODE));
482 }
else if (byte_count == REGISTER_BYTE_COUNT) {
484 uint16_t reg_val = (frame[3] << 8) | frame[4];
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);
490 }
else if (frame[1] == FC_WRITE_SINGLE_REGISTER) {
492 ESP_LOGI(TAG,
"Wallbox charging command successfully acknowledged.");
500 ESP_LOGW(TAG,
"Resetting component state from %s. pairing_attempts=%u, pairing_in_progress=%s, rx_buffer=%u bytes",
512 ESP_LOGE(TAG,
"Halting authentication: %s. A reconnect is required to retry.", LOG_STR_ARG(reason));
521void DeltaWallbox::set_charging_state(
bool state) {
523 ESP_LOGW(TAG,
"Cannot send charging command while wallbox is not fully connected.");
528 ESP_LOGI(TAG,
"Sending remote START charging command...");
529 this->
send_frame_(CMD_START_CHARGING,
sizeof(CMD_START_CHARGING));
531 ESP_LOGI(TAG,
"Sending remote STOP charging command...");
532 this->
send_frame_(CMD_STOP_CHARGING,
sizeof(CMD_STOP_CHARGING));
541 esp_ble_gattc_cb_param_t *param) {
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);
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);
554 case ESP_GATTC_SEARCH_CMPL_EVT: {
555 ESP_LOGI(TAG,
"GATT service discovery completed: status=%d", param->search_cmpl.status);
558 if (tx_char ==
nullptr || rx_char ==
nullptr) {
559 ESP_LOGE(TAG,
"Could not discover matching NUS TX/RX service on this BLE device.");
568 ESP_LOGW(TAG,
"register_for_notify failed, status=%d", err);
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.");
585 case ESP_GATTC_NOTIFY_EVT: {
586 if (param->notify.handle != this->rx_char_handle_)
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);
596 if (value_len > MAX_FRAME_LEN)
599 std::memcpy(this->
rx_buffer_.data() + this->rx_len_, param->notify.value, value_len);
602 bool progress =
true;
603 while (progress && this->
rx_len_ >= 3) {
606 size_t expected_len = 0;
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;
618 if (expected_len > 0) {
619 if (this->
rx_len_ >= expected_len) {
620 if (verify_crc(this->
rx_buffer_.data(), expected_len)) {
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_)));
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_)));
650 case ESP_GAP_BLE_PASSKEY_REQ_EVT: {
655 ESP_LOGW(TAG,
"Wallbox requested a BLE passkey, but no pin is configured. Set pin: under delta_wallbox:.");
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_);
662 ESP_LOGW(TAG,
"Failed to send BLE passkey reply: %s", esp_err_to_name(err));
666 case ESP_GAP_BLE_AUTH_CMPL_EVT:
671 if (param->ble_security.auth_cmpl.success) {
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);
681 ESP_LOGW(TAG,
"BLE pairing failed, reason=%d", param->ble_security.auth_cmpl.fail_reason);
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")