ESPHome 2025.9.0-dev
Loading...
Searching...
No Matches
shelly_dimmer.cpp
Go to the documentation of this file.
3
4#ifdef USE_ESP8266
5
6#include "shelly_dimmer.h"
7#ifdef USE_SHD_FIRMWARE_DATA
8#include "stm32flash.h"
9#endif
10
11#ifndef USE_ESP_IDF
12#include <HardwareSerial.h>
13#endif
14
15#include <algorithm>
16#include <cstring>
17#include <memory>
18#include <numeric>
19
20namespace {
21
22constexpr char TAG[] = "shelly_dimmer";
23
24constexpr uint8_t SHELLY_DIMMER_ACK_TIMEOUT = 200; // ms
25constexpr uint8_t SHELLY_DIMMER_MAX_RETRIES = 3;
26constexpr uint16_t SHELLY_DIMMER_MAX_BRIGHTNESS = 1000; // 100%
27
28// Protocol framing.
29constexpr uint8_t SHELLY_DIMMER_PROTO_START_BYTE = 0x01;
30constexpr uint8_t SHELLY_DIMMER_PROTO_END_BYTE = 0x04;
31
32// Supported commands.
33constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH = 0x01;
34constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_POLL = 0x10;
35constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_VERSION = 0x11;
36constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS = 0x20;
37
38// Command payload sizes.
39constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE = 2;
40constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE = 10;
41constexpr uint8_t SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE = 4 + 72 + 3;
42
43// STM Firmware
44#ifdef USE_SHD_FIRMWARE_DATA
45constexpr uint8_t STM_FIRMWARE[] PROGMEM = USE_SHD_FIRMWARE_DATA;
46constexpr uint32_t STM_FIRMWARE_SIZE_IN_BYTES = sizeof(STM_FIRMWARE);
47#endif
48
49// Scaling Constants
50constexpr float POWER_SCALING_FACTOR = 880373;
51constexpr float VOLTAGE_SCALING_FACTOR = 347800;
52constexpr float CURRENT_SCALING_FACTOR = 1448;
53
54// Essentially std::size() for pre c++17
55template<typename T, size_t N> constexpr size_t size(const T (&/*unused*/)[N]) noexcept { return N; }
56
57} // Anonymous namespace
58
59namespace esphome {
60namespace shelly_dimmer {
61
63uint16_t shelly_dimmer_checksum(const uint8_t *buf, int len) {
64 return std::accumulate<decltype(buf), uint16_t>(buf, buf + len, 0);
65}
66
68 return this->version_major_ == USE_SHD_FIRMWARE_MAJOR_VERSION &&
69 this->version_minor_ == USE_SHD_FIRMWARE_MINOR_VERSION;
70}
71
73 // Reset the STM32 and check the firmware version.
74 this->reset_normal_boot_();
75 this->send_command_(SHELLY_DIMMER_PROTO_CMD_VERSION, nullptr, 0);
76 ESP_LOGI(TAG, "STM32 current firmware version: %d.%d, desired version: %d.%d", this->version_major_,
77 this->version_minor_, USE_SHD_FIRMWARE_MAJOR_VERSION, USE_SHD_FIRMWARE_MINOR_VERSION);
78
80#ifdef USE_SHD_FIRMWARE_DATA
81 if (!this->upgrade_firmware_()) {
82 ESP_LOGW(TAG, "Failed to upgrade firmware");
83 this->mark_failed();
84 return;
85 }
86
87 this->reset_normal_boot_();
88 this->send_command_(SHELLY_DIMMER_PROTO_CMD_VERSION, nullptr, 0);
90 ESP_LOGE(TAG, "STM32 firmware upgrade already performed, but version is still incorrect");
91 this->mark_failed();
92 return;
93 }
94#else
95 ESP_LOGW(TAG, "Firmware version mismatch, put 'update: true' in the yaml to flash an update.");
96#endif
97 }
98}
99
101 this->pin_nrst_->setup();
102 this->pin_boot0_->setup();
103
104 this->handle_firmware();
105
106 this->send_settings_();
107 // Do an immediate poll to refresh current state.
108 this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0);
109
110 this->ready_ = true;
111}
112
113void ShellyDimmer::update() { this->send_command_(SHELLY_DIMMER_PROTO_CMD_POLL, nullptr, 0); }
114
116 ESP_LOGCONFIG(TAG, "ShellyDimmer:");
117 LOG_PIN(" NRST Pin: ", this->pin_nrst_);
118 LOG_PIN(" BOOT0 Pin: ", this->pin_boot0_);
119
120 ESP_LOGCONFIG(TAG,
121 " Leading Edge: %s\n"
122 " Warmup Brightness: %d\n"
123 " Minimum Brightness: %d\n"
124 " Maximum Brightness: %d",
125 YESNO(this->leading_edge_), this->warmup_brightness_, this->min_brightness_, this->max_brightness_);
126 // ESP_LOGCONFIG(TAG, " Warmup Time: %d", this->warmup_time_);
127 // ESP_LOGCONFIG(TAG, " Fade Rate: %d", this->fade_rate_);
128
129 LOG_UPDATE_INTERVAL(this);
130
131 ESP_LOGCONFIG(TAG,
132 " STM32 current firmware version: %d.%d \n"
133 " STM32 required firmware version: %d.%d",
134 this->version_major_, this->version_minor_, USE_SHD_FIRMWARE_MAJOR_VERSION,
135 USE_SHD_FIRMWARE_MINOR_VERSION);
136
137 if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION ||
138 this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) {
139 ESP_LOGE(TAG, " Firmware version mismatch, put 'update: true' in the yaml to flash an update.");
140 }
141}
142
144 if (!this->ready_) {
145 return;
146 }
147
148 float brightness;
149 state->current_values_as_brightness(&brightness);
150
151 const uint16_t brightness_int = this->convert_brightness_(brightness);
152 if (brightness_int == this->brightness_) {
153 ESP_LOGV(TAG, "Not sending unchanged value");
154 return;
155 }
156 ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness);
157
158 this->send_brightness_(brightness_int);
159}
160#ifdef USE_SHD_FIRMWARE_DATA
162 ESP_LOGW(TAG, "Starting STM32 firmware upgrade");
163 this->reset_dfu_boot_();
164
165 // Cleanup with RAII
166 auto stm32 = stm32_init(this, STREAM_SERIAL, 1);
167
168 if (!stm32) {
169 ESP_LOGW(TAG, "Failed to initialize STM32");
170 return false;
171 }
172
173 // Erase STM32 flash.
174 if (stm32_erase_memory(stm32, 0, STM32_MASS_ERASE) != STM32_ERR_OK) {
175 ESP_LOGW(TAG, "Failed to erase STM32 flash memory");
176 return false;
177 }
178
179 static constexpr uint32_t BUFFER_SIZE = 256;
180
181 // Copy the STM32 firmware over in 256-byte chunks. Note that the firmware is stored
182 // in flash memory so all accesses need to be 4-byte aligned.
183 uint8_t buffer[BUFFER_SIZE];
184 const uint8_t *p = STM_FIRMWARE;
185 uint32_t offset = 0;
186 uint32_t addr = stm32->dev->fl_start;
187 const uint32_t end = addr + STM_FIRMWARE_SIZE_IN_BYTES;
188
189 while (addr < end && offset < STM_FIRMWARE_SIZE_IN_BYTES) {
190 const uint32_t left_of_buffer = std::min(end - addr, BUFFER_SIZE);
191 const uint32_t len = std::min(left_of_buffer, STM_FIRMWARE_SIZE_IN_BYTES - offset);
192
193 if (len == 0) {
194 break;
195 }
196
197 std::memcpy(buffer, p, BUFFER_SIZE);
198 p += BUFFER_SIZE;
199
200 if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) {
201 ESP_LOGW(TAG, "Failed to write to STM32 flash memory");
202 return false;
203 }
204
205 addr += len;
206 offset += len;
207 }
208
209 ESP_LOGI(TAG, "STM32 firmware upgrade successful");
210
211 return true;
212}
213#endif
214
215uint16_t ShellyDimmer::convert_brightness_(float brightness) {
216 // Special case for zero as only zero means turn off completely.
217 if (brightness == 0.0) {
218 return 0;
219 }
220
221 return remap<uint16_t, float>(brightness, 0.0f, 1.0f, this->min_brightness_, this->max_brightness_);
222}
223
224void ShellyDimmer::send_brightness_(uint16_t brightness) {
225 const uint8_t payload[] = {
226 // Brightness (%) * 10.
227 static_cast<uint8_t>(brightness & 0xff),
228 static_cast<uint8_t>(brightness >> 8),
229 };
230 static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE, "Invalid payload size");
231
232 this->send_command_(SHELLY_DIMMER_PROTO_CMD_SWITCH, payload, SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE);
233
234 this->brightness_ = brightness;
235}
236
238 const uint16_t fade_rate = std::min(uint16_t{100}, this->fade_rate_);
239
240 float brightness = 0.0;
241 if (this->state_ != nullptr) {
242 this->state_->current_values_as_brightness(&brightness);
243 }
244 const uint16_t brightness_int = this->convert_brightness_(brightness);
245 ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness);
246
247 const uint8_t payload[] = {
248 // Brightness (%) * 10.
249 static_cast<uint8_t>(brightness_int & 0xff),
250 static_cast<uint8_t>(brightness_int >> 8),
251 // Leading / trailing edge [0x01 = leading, 0x02 = trailing].
252 this->leading_edge_ ? uint8_t{0x01} : uint8_t{0x02},
253 0x00,
254 // Fade rate.
255 static_cast<uint8_t>(fade_rate & 0xff),
256 static_cast<uint8_t>(fade_rate >> 8),
257 // Warmup brightness.
258 static_cast<uint8_t>(this->warmup_brightness_ & 0xff),
259 static_cast<uint8_t>(this->warmup_brightness_ >> 8),
260 // Warmup time.
261 static_cast<uint8_t>(this->warmup_time_ & 0xff),
262 static_cast<uint8_t>(this->warmup_time_ >> 8),
263 };
264 static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE, "Invalid payload size");
265
266 this->send_command_(SHELLY_DIMMER_PROTO_CMD_SETTINGS, payload, SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE);
267
268 // Also send brightness separately as it is ignored above.
269 this->send_brightness_(brightness_int);
270}
271
272bool ShellyDimmer::send_command_(uint8_t cmd, const uint8_t *const payload, uint8_t len) {
273 ESP_LOGD(TAG, "Sending command: 0x%02x (%d bytes) payload 0x%s", cmd, len, format_hex(payload, len).c_str());
274
275 // Prepare a command frame.
276 uint8_t frame[SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE];
277 const size_t frame_len = this->frame_command_(frame, cmd, payload, len);
278
279 // Write the frame and wait for acknowledgement.
280 int retries = SHELLY_DIMMER_MAX_RETRIES;
281 while (retries--) {
282 this->write_array(frame, frame_len);
283 this->flush();
284
285 ESP_LOGD(TAG, "Command sent, waiting for reply");
286 const uint32_t tx_time = millis();
287 while (millis() - tx_time < SHELLY_DIMMER_ACK_TIMEOUT) {
288 if (this->read_frame_()) {
289 return true;
290 }
291 delay(1);
292 }
293 ESP_LOGW(TAG, "Timeout while waiting for reply");
294 }
295 ESP_LOGW(TAG, "Failed to send command");
296 return false;
297}
298
299size_t ShellyDimmer::frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *const payload, size_t len) {
300 size_t pos = 0;
301
302 // Generate a frame.
303 data[0] = SHELLY_DIMMER_PROTO_START_BYTE;
304 data[1] = ++this->seq_;
305 data[2] = cmd;
306 data[3] = len;
307 pos += 4;
308
309 if (payload != nullptr) {
310 std::memcpy(data + 4, payload, len);
311 pos += len;
312 }
313
314 // Calculate checksum for the payload.
315 const uint16_t csum = shelly_dimmer_checksum(data + 1, 3 + len);
316 data[pos++] = static_cast<uint8_t>(csum >> 8);
317 data[pos++] = static_cast<uint8_t>(csum & 0xff);
318 data[pos++] = SHELLY_DIMMER_PROTO_END_BYTE;
319 return pos;
320}
321
323 const uint8_t pos = this->buffer_pos_;
324
325 if (pos == 0) {
326 // Must be start byte.
327 return c == SHELLY_DIMMER_PROTO_START_BYTE ? 1 : -1;
328 } else if (pos < 4) {
329 // Header.
330 return 1;
331 }
332
333 // Decode payload length from header.
334 const uint8_t payload_len = this->buffer_[3];
335 if ((4 + payload_len + 3) > SHELLY_DIMMER_BUFFER_SIZE) {
336 return -1;
337 }
338
339 if (pos < 4 + payload_len + 1) {
340 // Payload.
341 return 1;
342 }
343
344 if (pos == 4 + payload_len + 1) {
345 // Verify checksum.
346 const uint16_t csum = (this->buffer_[pos - 1] << 8 | c);
347 const uint16_t csum_verify = shelly_dimmer_checksum(&this->buffer_[1], 3 + payload_len);
348 if (csum != csum_verify) {
349 return -1;
350 }
351 return 1;
352 }
353
354 if (pos == 4 + payload_len + 2) {
355 // Must be end byte.
356 return c == SHELLY_DIMMER_PROTO_END_BYTE ? 0 : -1;
357 }
358 return -1;
359}
360
362 while (this->available()) {
363 const uint8_t c = this->read();
364 this->buffer_[this->buffer_pos_] = c;
365
366 ESP_LOGV(TAG, "Read byte: 0x%02x (pos %d)", c, this->buffer_pos_);
367
368 switch (this->handle_byte_(c)) {
369 case 0: {
370 // Frame successfully received.
371 this->handle_frame_();
372 this->buffer_pos_ = 0;
373 return true;
374 }
375 case -1: {
376 // Failure.
377 this->buffer_pos_ = 0;
378 break;
379 }
380 case 1: {
381 // Need more data.
382 this->buffer_pos_++;
383 break;
384 }
385 }
386 }
387 return false;
388}
389
391 const uint8_t seq = this->buffer_[1];
392 const uint8_t cmd = this->buffer_[2];
393 const uint8_t payload_len = this->buffer_[3];
394
395 ESP_LOGD(TAG, "Got frame: 0x%02x", cmd);
396
397 // Compare with expected identifier as the frame is always a response to
398 // our previously sent command.
399 if (seq != this->seq_) {
400 return false;
401 }
402
403 const uint8_t *payload = &this->buffer_[4];
404
405 // Handle response.
406 switch (cmd) {
407 case SHELLY_DIMMER_PROTO_CMD_POLL: {
408 if (payload_len < 16) {
409 return false;
410 }
411
412 const uint8_t hw_version = payload[0];
413 // payload[1] is unused.
414 const uint16_t brightness = encode_uint16(payload[3], payload[2]);
415
416 const uint32_t power_raw = encode_uint32(payload[7], payload[6], payload[5], payload[4]);
417
418 const uint32_t voltage_raw = encode_uint32(payload[11], payload[10], payload[9], payload[8]);
419
420 const uint32_t current_raw = encode_uint32(payload[15], payload[14], payload[13], payload[12]);
421
422 const uint16_t fade_rate = payload[16];
423
424 float power = 0;
425 if (power_raw > 0) {
426 power = POWER_SCALING_FACTOR / static_cast<float>(power_raw);
427 }
428
429 float voltage = 0;
430 if (voltage_raw > 0) {
431 voltage = VOLTAGE_SCALING_FACTOR / static_cast<float>(voltage_raw);
432 }
433
434 float current = 0;
435 if (current_raw > 0) {
436 current = CURRENT_SCALING_FACTOR / static_cast<float>(current_raw);
437 }
438
439 ESP_LOGI(TAG, "Got dimmer data:");
440 ESP_LOGI(TAG, " HW version: %d", hw_version);
441 ESP_LOGI(TAG, " Brightness: %d", brightness);
442 ESP_LOGI(TAG, " Fade rate: %d", fade_rate);
443 ESP_LOGI(TAG, " Power: %f W", power);
444 ESP_LOGI(TAG, " Voltage: %f V", voltage);
445 ESP_LOGI(TAG, " Current: %f A", current);
446
447 // Update sensors.
448 if (this->power_sensor_ != nullptr) {
449 this->power_sensor_->publish_state(power);
450 }
451 if (this->voltage_sensor_ != nullptr) {
452 this->voltage_sensor_->publish_state(voltage);
453 }
454 if (this->current_sensor_ != nullptr) {
455 this->current_sensor_->publish_state(current);
456 }
457
458 return true;
459 }
460 case SHELLY_DIMMER_PROTO_CMD_VERSION: {
461 if (payload_len < 2) {
462 return false;
463 }
464
465 this->version_minor_ = payload[0];
466 this->version_major_ = payload[1];
467 return true;
468 }
469 case SHELLY_DIMMER_PROTO_CMD_SWITCH:
470 case SHELLY_DIMMER_PROTO_CMD_SETTINGS: {
471 return payload_len >= 1 && payload[0] == 0x01;
472 }
473 default: {
474 return false;
475 }
476 }
477}
478
479void ShellyDimmer::reset_(bool boot0) {
480 ESP_LOGD(TAG, "Reset STM32, boot0=%d", boot0);
481
482 this->pin_boot0_->digital_write(boot0);
483 this->pin_nrst_->digital_write(false);
484
485 // Wait 50ms for the STM32 to reset.
486 delay(50); // NOLINT
487
488 // Clear receive buffer.
489 while (this->available()) {
490 this->read();
491 }
492
493 this->pin_nrst_->digital_write(true);
494 // Wait 50ms for the STM32 to boot.
495 delay(50); // NOLINT
496
497 ESP_LOGD(TAG, "Reset STM32 done");
498}
499
501 // set NONE parity in normal mode
502
503#ifndef USE_ESP_IDF // workaround for reconfiguring the uart
504 Serial.end();
505 Serial.begin(115200, SERIAL_8N1);
506 Serial.flush();
507#endif
508
509 this->flush();
510 this->reset_(false);
511}
512
514 // set EVEN parity in bootloader mode
515
516#ifndef USE_ESP_IDF // workaround for reconfiguring the uart
517 Serial.end();
518 Serial.begin(115200, SERIAL_8E1);
519 Serial.flush();
520#endif
521
522 this->flush();
523 this->reset_(true);
524}
525
526} // namespace shelly_dimmer
527} // namespace esphome
528
529#endif // USE_ESP8266
virtual void mark_failed()
Mark this component as failed.
virtual void setup()=0
virtual void digital_write(bool value)=0
This class represents the communication layer between the front-end MQTT layer and the hardware outpu...
Definition light_state.h:66
void current_values_as_brightness(float *brightness)
void publish_state(float state)
Publish a new state to the front-end.
Definition sensor.cpp:45
void write_state(light::LightState *state) override
void reset_dfu_boot_()
Reset STM32 to boot into DFU mode to enable firmware upgrades.
bool upgrade_firmware_()
Performs a firmware upgrade.
void reset_normal_boot_()
Reset STM32 to boot the regular firmware.
bool handle_frame_()
Handles a complete frame.
void send_brightness_(uint16_t brightness)
Sends the given brightness value.
void send_settings_()
Sends dimmer configuration.
int handle_byte_(uint8_t c)
Handles a single byte as part of a protocol frame.
std::array< uint8_t, SHELLY_DIMMER_BUFFER_SIZE > buffer_
uint16_t convert_brightness_(float brightness)
Convert relative brightness into a dimmer brightness value.
size_t frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *payload, size_t len)
Frames a given command payload.
void reset_(bool boot0)
Reset STM32 with the BOOT0 pin set to the given value.
bool read_frame_()
Reads a response frame.
bool send_command_(uint8_t cmd, const uint8_t *payload, uint8_t len)
Sends a command and waits for an acknowledgement.
void write_array(const uint8_t *data, size_t len)
Definition uart.h:21
bool state
Definition fan.h:0
uint16_t shelly_dimmer_checksum(const uint8_t *buf, int len)
Computes a crappy checksum as defined by the Shelly Dimmer protocol.
stm32_unique_ptr stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init)
constexpr auto STREAM_SERIAL
Definition stm32flash.h:40
stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data, const unsigned int len)
constexpr auto STM32_MASS_ERASE
Definition stm32flash.h:47
stm32_err_t stm32_erase_memory(const stm32_unique_ptr &stm, uint32_t spage, uint32_t pages)
const char *const TAG
Definition spi.cpp:8
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
std::string format_hex(const uint8_t *data, size_t length)
Format the byte array data of length len in lowercased hex.
Definition helpers.cpp:249
std::string size_t len
Definition helpers.h:279
constexpr uint32_t encode_uint32(uint8_t byte1, uint8_t byte2, uint8_t byte3, uint8_t byte4)
Encode a 32-bit value given four bytes in most to least significant byte order.
Definition helpers.h:181
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:173
void IRAM_ATTR HOT delay(uint32_t ms)
Definition core.cpp:29
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:28
T remap(U value, U min, U max, T min_out, T max_out)
Remap value from the range (min, max) to (min_out, max_out).
Definition helpers.h:144
uint8_t end[39]
Definition sun_gtil2.cpp:17
const uint8_t ESPHOME_WEBSERVER_INDEX_HTML[] PROGMEM
Definition web_server.h:24