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