ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
renogy_inverter_ble.cpp
Go to the documentation of this file.
2
3#ifdef USE_ESP32
4
5#include <cstring>
7#include "esphome/core/log.h"
8
10
11ESPHOME_LOG_TAG(TAG, "renogy_inverter_ble");
12
13static constexpr uint16_t SERVICE_NOTIFY_UUID = 0xFFF0;
14static constexpr uint16_t CHAR_NOTIFY_UUID = 0xFFF1;
15static constexpr uint16_t SERVICE_WRITE_UUID = 0xFFD0;
16static constexpr uint16_t CHAR_WRITE_UUID = 0xFFD1;
17static constexpr uint16_t CHAR_INIT_UUID = 0xFFD4;
18
19static constexpr uint8_t MODBUS_DEVICE_ID = 0x20;
20static constexpr uint8_t MODBUS_READ_HOLDING = 0x03;
21static constexpr uint16_t REG_MAIN = 4000;
22static constexpr uint16_t REG_MAIN_WORDS = 32;
23static constexpr uint16_t REG_LOAD = 4408;
24static constexpr uint16_t REG_LOAD_WORDS = 6;
25
26// Watchdog: returns the cycle to IDLE when the inverter stops answering
27static constexpr uint32_t CYCLE_TIMEOUT_MS = 3000;
28// Gap the inverter needs between two consecutive register reads
29static constexpr uint32_t LOAD_READ_DELAY_MS = 300;
30
31static constexpr uint32_t CYCLE_TIMEOUT_ID = 0;
32static constexpr uint32_t LOAD_READ_ID = 1;
33
34static void publish(sensor::Sensor *s, float value) {
35 if (s != nullptr) {
36 s->publish_state(value);
37 }
38}
39
41 ESP_LOGCONFIG(TAG, "Renogy Inverter BLE:");
42 LOG_UPDATE_INTERVAL(this);
43 LOG_SENSOR(" ", "AC input voltage", this->ac_input_voltage_sensor_);
44 LOG_SENSOR(" ", "AC output voltage", this->ac_output_voltage_sensor_);
45 LOG_SENSOR(" ", "AC output current", this->ac_output_current_sensor_);
46 LOG_SENSOR(" ", "AC output frequency", this->ac_output_frequency_sensor_);
47 LOG_SENSOR(" ", "Input frequency", this->input_frequency_sensor_);
48 LOG_SENSOR(" ", "Battery voltage", this->battery_voltage_sensor_);
49 LOG_SENSOR(" ", "Temperature", this->temperature_sensor_);
50 LOG_SENSOR(" ", "Load current", this->load_current_sensor_);
51 LOG_SENSOR(" ", "Load active power", this->load_active_power_sensor_);
52 LOG_SENSOR(" ", "Load apparent power", this->load_apparent_power_sensor_);
53}
54
56 this->cancel_timeout(CYCLE_TIMEOUT_ID);
57 this->cancel_timeout(LOAD_READ_ID);
58 this->frame_len_ = 0;
59 this->state_ = State::IDLE;
60}
61
62void RenogyInverterBle::read_register_(uint16_t start_register, uint16_t word_count) {
63 std::array<uint8_t, 8> req{MODBUS_DEVICE_ID,
64 MODBUS_READ_HOLDING,
65 static_cast<uint8_t>(start_register >> 8),
66 static_cast<uint8_t>(start_register),
67 static_cast<uint8_t>(word_count >> 8),
68 static_cast<uint8_t>(word_count)};
69 const uint16_t crc = crc16(req.data(), 6);
70 req[6] = crc & 0xFF; // Modbus sends the CRC low byte first
71 req[7] = crc >> 8;
72
73 this->frame_len_ = 0;
74 const esp_err_t status =
75 esp_ble_gattc_write_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(), this->write_handle_,
76 req.size(), req.data(), ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
77 if (status != ESP_OK) {
78 ESP_LOGW(TAG, "write_char (reg %u) failed, status=%d", start_register, status);
79 this->abort_cycle_();
80 }
81}
82
84 this->set_timeout(CYCLE_TIMEOUT_ID, CYCLE_TIMEOUT_MS, [this]() {
85 ESP_LOGW(TAG, "Read cycle timed out (state=%u); resetting", static_cast<uint8_t>(this->state_));
86 this->abort_cycle_();
87 });
88 if (this->init_handle_ == 0) {
89 // No init characteristic discovered; try the main read directly
90 this->state_ = State::MAIN;
91 this->read_register_(REG_MAIN, REG_MAIN_WORDS);
92 return;
93 }
94 this->state_ = State::INIT;
95 const esp_err_t status = esp_ble_gattc_read_char(this->parent_->get_gattc_if(), this->parent_->get_conn_id(),
96 this->init_handle_, ESP_GATT_AUTH_REQ_NONE);
97 if (status != ESP_OK) {
98 ESP_LOGW(TAG, "init read_char failed, status=%d", status);
99 this->abort_cycle_();
100 }
101}
102
103void RenogyInverterBle::update() {
104 if (this->node_state != espbt::ClientState::ESTABLISHED) {
105 ESP_LOGD(TAG, "Not connected yet; skipping poll");
106 return;
107 }
108 if (this->state_ != State::IDLE) {
109 ESP_LOGD(TAG, "Previous read cycle still in progress; skipping");
110 return;
111 }
112 this->start_cycle_();
113}
114
116 // A Modbus frame with its CRC appended checks to zero
117 if (len < 5 || crc16(this->frame_.data(), len) != 0) {
118 ESP_LOGW(TAG, "CRC mismatch (state=%u, len=%u)", static_cast<uint8_t>(this->state_), len);
119 this->abort_cycle_();
120 return;
121 }
122 // Exception response: function code with the high bit set, third byte is the exception code
123 if ((this->frame_[1] & 0x80) != 0) {
124 ESP_LOGW(TAG, "Modbus exception 0x%02X (state=%u)", this->frame_[2], static_cast<uint8_t>(this->state_));
125 this->abort_cycle_();
126 return;
127 }
128 // Only the reply to the outstanding read is accepted: same device, same function, the requested word count
129 const bool awaiting_reply = this->state_ == State::MAIN || this->state_ == State::LOAD;
130 const uint16_t expected_words = this->state_ == State::MAIN ? REG_MAIN_WORDS : REG_LOAD_WORDS;
131 if (!awaiting_reply || this->frame_[0] != MODBUS_DEVICE_ID || this->frame_[1] != MODBUS_READ_HOLDING ||
132 this->frame_[2] != 2 * expected_words) {
133 ESP_LOGW(TAG, "Unexpected response %02X %02X %02X (state=%u)", this->frame_[0], this->frame_[1], this->frame_[2],
134 static_cast<uint8_t>(this->state_));
135 this->abort_cycle_();
136 return;
137 }
138 auto reg16 = [this](uint8_t i) { return encode_uint16(this->frame_[3 + 2 * i], this->frame_[4 + 2 * i]); };
139
140 if (this->state_ == State::MAIN) {
141 publish(this->ac_input_voltage_sensor_, reg16(0) * 0.1f);
142 publish(this->ac_output_voltage_sensor_, reg16(2) * 0.1f);
143 publish(this->ac_output_current_sensor_, reg16(3) * 0.01f);
144 publish(this->ac_output_frequency_sensor_, reg16(4) * 0.01f);
145 publish(this->battery_voltage_sensor_, reg16(5) * 0.1f);
146 publish(this->temperature_sensor_, reg16(6) * 0.1f);
147 publish(this->input_frequency_sensor_, reg16(9) * 0.01f);
148 this->frame_len_ = 0;
149 this->set_timeout(LOAD_READ_ID, LOAD_READ_DELAY_MS, [this]() {
150 this->state_ = State::LOAD;
151 this->read_register_(REG_LOAD, REG_LOAD_WORDS);
152 });
153 return;
154 }
155 publish(this->load_current_sensor_, reg16(0) * 0.01f);
156 publish(this->load_active_power_sensor_, static_cast<float>(reg16(1)));
157 publish(this->load_apparent_power_sensor_, static_cast<float>(reg16(2)));
158 this->abort_cycle_();
159}
160
161void RenogyInverterBle::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
162 esp_ble_gattc_cb_param_t *param) {
163 switch (event) {
164 case ESP_GATTC_DISCONNECT_EVT: {
165 this->abort_cycle_();
166 this->node_state = espbt::ClientState::IDLE;
167 break;
168 }
169 case ESP_GATTC_SEARCH_CMPL_EVT: {
170 auto *notify_chr = this->parent_->get_characteristic(espbt::ESPBTUUID::from_uint16(SERVICE_NOTIFY_UUID),
171 espbt::ESPBTUUID::from_uint16(CHAR_NOTIFY_UUID));
172 auto *write_chr = this->parent_->get_characteristic(espbt::ESPBTUUID::from_uint16(SERVICE_WRITE_UUID),
173 espbt::ESPBTUUID::from_uint16(CHAR_WRITE_UUID));
174 auto *init_chr = this->parent_->get_characteristic(espbt::ESPBTUUID::from_uint16(SERVICE_WRITE_UUID),
175 espbt::ESPBTUUID::from_uint16(CHAR_INIT_UUID));
176 if (notify_chr == nullptr || write_chr == nullptr) {
177 ESP_LOGE(TAG, "Required characteristics not found; not a Renogy inverter?");
178 break;
179 }
180 this->notify_handle_ = notify_chr->handle;
181 this->write_handle_ = write_chr->handle;
182 this->init_handle_ = (init_chr != nullptr) ? init_chr->handle : 0;
183 ESP_LOGI(TAG, "handles: notify=%u write=%u init=%u", this->notify_handle_, this->write_handle_,
184 this->init_handle_);
185 const esp_err_t status = this->parent_->register_for_notify(this->notify_handle_);
186 if (status != ESP_OK) {
187 ESP_LOGW(TAG, "register_for_notify failed, status=%d", status);
188 }
189 break;
190 }
191 case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
192 // Other nodes on the same client see this event too
193 if (param->reg_for_notify.handle != this->notify_handle_) {
194 break;
195 }
196 if (param->reg_for_notify.status != ESP_GATT_OK) {
197 ESP_LOGW(TAG, "Notification registration failed, status=%d", param->reg_for_notify.status);
198 break;
199 }
200 this->node_state = espbt::ClientState::ESTABLISHED;
201 ESP_LOGI(TAG, "Connected; will poll the inverter every update interval");
202 break;
203 }
204 case ESP_GATTC_READ_CHAR_EVT: {
205 if (this->state_ != State::INIT || param->read.handle != this->init_handle_) {
206 break;
207 }
208 if (param->read.status != ESP_GATT_OK) {
209 ESP_LOGW(TAG, "init read failed, status=%d", param->read.status);
210 this->abort_cycle_();
211 break;
212 }
213 // Init handshake done; the inverter now answers Modbus reads
214 this->state_ = State::MAIN;
215 this->read_register_(REG_MAIN, REG_MAIN_WORDS);
216 break;
217 }
218 case ESP_GATTC_NOTIFY_EVT: {
219 if (param->notify.handle != this->notify_handle_) {
220 break;
221 }
222 const uint16_t value_len = param->notify.value_len;
223 if (this->frame_len_ + value_len > MAX_FRAME) {
224 ESP_LOGW(TAG, "Frame overflow; resetting");
225 this->abort_cycle_();
226 break;
227 }
228 memcpy(&this->frame_[this->frame_len_], param->notify.value, value_len);
229 this->frame_len_ += value_len;
230 if (this->frame_len_ < 3) {
231 break;
232 }
233 // An exception frame ([id][func|0x80][exc][crc][crc]) is 5 bytes; a normal response is
234 // [id][func][byte_count][...data][crc][crc]
235 const uint16_t expected_len = ((this->frame_[1] & 0x80) != 0) ? 5 : 3 + this->frame_[2] + 2;
236 if (this->frame_len_ >= expected_len) {
237 this->on_frame_complete_(expected_len);
238 }
239 break;
240 }
241 default:
242 break;
243 }
244}
245
246} // namespace esphome::renogy_inverter_ble
247
248#endif // USE_ESP32
uint8_t status
Definition bl0942.h:8
bool cancel_timeout(const char *name)
Cancel a timeout function.
virtual void dump_config()
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
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)
void read_register_(uint16_t start_register, uint16_t word_count)
Base-class for all sensors.
Definition sensor.h:47
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:65
ESPHOME_LOG_TAG(TAG, "renogy_inverter_ble")
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
constexpr uint16_t encode_uint16(uint8_t msb, uint8_t lsb)
Encode a 16-bit value given the most and least significant byte.
Definition helpers.h:980
static void uint32_t