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