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