ESPHome 2025.5.0
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 Shelly Dimmer...");
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, " Leading Edge: %s", YESNO(this->leading_edge_));
123 ESP_LOGCONFIG(TAG, " Warmup Brightness: %d", this->warmup_brightness_);
124 // ESP_LOGCONFIG(TAG, " Warmup Time: %d", this->warmup_time_);
125 // ESP_LOGCONFIG(TAG, " Fade Rate: %d", this->fade_rate_);
126 ESP_LOGCONFIG(TAG, " Minimum Brightness: %d", this->min_brightness_);
127 ESP_LOGCONFIG(TAG, " Maximum Brightness: %d", this->max_brightness_);
128
129 LOG_UPDATE_INTERVAL(this);
130
131 ESP_LOGCONFIG(TAG, " STM32 current firmware version: %d.%d ", this->version_major_, this->version_minor_);
132 ESP_LOGCONFIG(TAG, " STM32 required firmware version: %d.%d", USE_SHD_FIRMWARE_MAJOR_VERSION,
133 USE_SHD_FIRMWARE_MINOR_VERSION);
134
135 if (this->version_major_ != USE_SHD_FIRMWARE_MAJOR_VERSION ||
136 this->version_minor_ != USE_SHD_FIRMWARE_MINOR_VERSION) {
137 ESP_LOGE(TAG, " Firmware version mismatch, put 'update: true' in the yaml to flash an update.");
138 }
139}
140
142 if (!this->ready_) {
143 return;
144 }
145
146 float brightness;
147 state->current_values_as_brightness(&brightness);
148
149 const uint16_t brightness_int = this->convert_brightness_(brightness);
150 if (brightness_int == this->brightness_) {
151 ESP_LOGV(TAG, "Not sending unchanged value");
152 return;
153 }
154 ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness);
155
156 this->send_brightness_(brightness_int);
157}
158#ifdef USE_SHD_FIRMWARE_DATA
160 ESP_LOGW(TAG, "Starting STM32 firmware upgrade");
161 this->reset_dfu_boot_();
162
163 // Cleanup with RAII
164 auto stm32 = stm32_init(this, STREAM_SERIAL, 1);
165
166 if (!stm32) {
167 ESP_LOGW(TAG, "Failed to initialize STM32");
168 return false;
169 }
170
171 // Erase STM32 flash.
172 if (stm32_erase_memory(stm32, 0, STM32_MASS_ERASE) != STM32_ERR_OK) {
173 ESP_LOGW(TAG, "Failed to erase STM32 flash memory");
174 return false;
175 }
176
177 static constexpr uint32_t BUFFER_SIZE = 256;
178
179 // Copy the STM32 firmware over in 256-byte chunks. Note that the firmware is stored
180 // in flash memory so all accesses need to be 4-byte aligned.
181 uint8_t buffer[BUFFER_SIZE];
182 const uint8_t *p = STM_FIRMWARE;
183 uint32_t offset = 0;
184 uint32_t addr = stm32->dev->fl_start;
185 const uint32_t end = addr + STM_FIRMWARE_SIZE_IN_BYTES;
186
187 while (addr < end && offset < STM_FIRMWARE_SIZE_IN_BYTES) {
188 const uint32_t left_of_buffer = std::min(end - addr, BUFFER_SIZE);
189 const uint32_t len = std::min(left_of_buffer, STM_FIRMWARE_SIZE_IN_BYTES - offset);
190
191 if (len == 0) {
192 break;
193 }
194
195 std::memcpy(buffer, p, BUFFER_SIZE);
196 p += BUFFER_SIZE;
197
198 if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) {
199 ESP_LOGW(TAG, "Failed to write to STM32 flash memory");
200 return false;
201 }
202
203 addr += len;
204 offset += len;
205 }
206
207 ESP_LOGI(TAG, "STM32 firmware upgrade successful");
208
209 return true;
210}
211#endif
212
213uint16_t ShellyDimmer::convert_brightness_(float brightness) {
214 // Special case for zero as only zero means turn off completely.
215 if (brightness == 0.0) {
216 return 0;
217 }
218
219 return remap<uint16_t, float>(brightness, 0.0f, 1.0f, this->min_brightness_, this->max_brightness_);
220}
221
222void ShellyDimmer::send_brightness_(uint16_t brightness) {
223 const uint8_t payload[] = {
224 // Brightness (%) * 10.
225 static_cast<uint8_t>(brightness & 0xff),
226 static_cast<uint8_t>(brightness >> 8),
227 };
228 static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE, "Invalid payload size");
229
230 this->send_command_(SHELLY_DIMMER_PROTO_CMD_SWITCH, payload, SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE);
231
232 this->brightness_ = brightness;
233}
234
236 const uint16_t fade_rate = std::min(uint16_t{100}, this->fade_rate_);
237
238 float brightness = 0.0;
239 if (this->state_ != nullptr) {
240 this->state_->current_values_as_brightness(&brightness);
241 }
242 const uint16_t brightness_int = this->convert_brightness_(brightness);
243 ESP_LOGD(TAG, "Brightness update: %d (raw: %f)", brightness_int, brightness);
244
245 const uint8_t payload[] = {
246 // Brightness (%) * 10.
247 static_cast<uint8_t>(brightness_int & 0xff),
248 static_cast<uint8_t>(brightness_int >> 8),
249 // Leading / trailing edge [0x01 = leading, 0x02 = trailing].
250 this->leading_edge_ ? uint8_t{0x01} : uint8_t{0x02},
251 0x00,
252 // Fade rate.
253 static_cast<uint8_t>(fade_rate & 0xff),
254 static_cast<uint8_t>(fade_rate >> 8),
255 // Warmup brightness.
256 static_cast<uint8_t>(this->warmup_brightness_ & 0xff),
257 static_cast<uint8_t>(this->warmup_brightness_ >> 8),
258 // Warmup time.
259 static_cast<uint8_t>(this->warmup_time_ & 0xff),
260 static_cast<uint8_t>(this->warmup_time_ >> 8),
261 };
262 static_assert(size(payload) == SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE, "Invalid payload size");
263
264 this->send_command_(SHELLY_DIMMER_PROTO_CMD_SETTINGS, payload, SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE);
265
266 // Also send brightness separately as it is ignored above.
267 this->send_brightness_(brightness_int);
268}
269
270bool ShellyDimmer::send_command_(uint8_t cmd, const uint8_t *const payload, uint8_t len) {
271 ESP_LOGD(TAG, "Sending command: 0x%02x (%d bytes) payload 0x%s", cmd, len, format_hex(payload, len).c_str());
272
273 // Prepare a command frame.
274 uint8_t frame[SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE];
275 const size_t frame_len = this->frame_command_(frame, cmd, payload, len);
276
277 // Write the frame and wait for acknowledgement.
278 int retries = SHELLY_DIMMER_MAX_RETRIES;
279 while (retries--) {
280 this->write_array(frame, frame_len);
281 this->flush();
282
283 ESP_LOGD(TAG, "Command sent, waiting for reply");
284 const uint32_t tx_time = millis();
285 while (millis() - tx_time < SHELLY_DIMMER_ACK_TIMEOUT) {
286 if (this->read_frame_()) {
287 return true;
288 }
289 delay(1);
290 }
291 ESP_LOGW(TAG, "Timeout while waiting for reply");
292 }
293 ESP_LOGW(TAG, "Failed to send command");
294 return false;
295}
296
297size_t ShellyDimmer::frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *const payload, size_t len) {
298 size_t pos = 0;
299
300 // Generate a frame.
301 data[0] = SHELLY_DIMMER_PROTO_START_BYTE;
302 data[1] = ++this->seq_;
303 data[2] = cmd;
304 data[3] = len;
305 pos += 4;
306
307 if (payload != nullptr) {
308 std::memcpy(data + 4, payload, len);
309 pos += len;
310 }
311
312 // Calculate checksum for the payload.
313 const uint16_t csum = shelly_dimmer_checksum(data + 1, 3 + len);
314 data[pos++] = static_cast<uint8_t>(csum >> 8);
315 data[pos++] = static_cast<uint8_t>(csum & 0xff);
316 data[pos++] = SHELLY_DIMMER_PROTO_END_BYTE;
317 return pos;
318}
319
321 const uint8_t pos = this->buffer_pos_;
322
323 if (pos == 0) {
324 // Must be start byte.
325 return c == SHELLY_DIMMER_PROTO_START_BYTE ? 1 : -1;
326 } else if (pos < 4) {
327 // Header.
328 return 1;
329 }
330
331 // Decode payload length from header.
332 const uint8_t payload_len = this->buffer_[3];
333 if ((4 + payload_len + 3) > SHELLY_DIMMER_BUFFER_SIZE) {
334 return -1;
335 }
336
337 if (pos < 4 + payload_len + 1) {
338 // Payload.
339 return 1;
340 }
341
342 if (pos == 4 + payload_len + 1) {
343 // Verify checksum.
344 const uint16_t csum = (this->buffer_[pos - 1] << 8 | c);
345 const uint16_t csum_verify = shelly_dimmer_checksum(&this->buffer_[1], 3 + payload_len);
346 if (csum != csum_verify) {
347 return -1;
348 }
349 return 1;
350 }
351
352 if (pos == 4 + payload_len + 2) {
353 // Must be end byte.
354 return c == SHELLY_DIMMER_PROTO_END_BYTE ? 0 : -1;
355 }
356 return -1;
357}
358
360 while (this->available()) {
361 const uint8_t c = this->read();
362 this->buffer_[this->buffer_pos_] = c;
363
364 ESP_LOGV(TAG, "Read byte: 0x%02x (pos %d)", c, this->buffer_pos_);
365
366 switch (this->handle_byte_(c)) {
367 case 0: {
368 // Frame successfully received.
369 this->handle_frame_();
370 this->buffer_pos_ = 0;
371 return true;
372 }
373 case -1: {
374 // Failure.
375 this->buffer_pos_ = 0;
376 break;
377 }
378 case 1: {
379 // Need more data.
380 this->buffer_pos_++;
381 break;
382 }
383 }
384 }
385 return false;
386}
387
389 const uint8_t seq = this->buffer_[1];
390 const uint8_t cmd = this->buffer_[2];
391 const uint8_t payload_len = this->buffer_[3];
392
393 ESP_LOGD(TAG, "Got frame: 0x%02x", cmd);
394
395 // Compare with expected identifier as the frame is always a response to
396 // our previously sent command.
397 if (seq != this->seq_) {
398 return false;
399 }
400
401 const uint8_t *payload = &this->buffer_[4];
402
403 // Handle response.
404 switch (cmd) {
405 case SHELLY_DIMMER_PROTO_CMD_POLL: {
406 if (payload_len < 16) {
407 return false;
408 }
409
410 const uint8_t hw_version = payload[0];
411 // payload[1] is unused.
412 const uint16_t brightness = encode_uint16(payload[3], payload[2]);
413
414 const uint32_t power_raw = encode_uint32(payload[7], payload[6], payload[5], payload[4]);
415
416 const uint32_t voltage_raw = encode_uint32(payload[11], payload[10], payload[9], payload[8]);
417
418 const uint32_t current_raw = encode_uint32(payload[15], payload[14], payload[13], payload[12]);
419
420 const uint16_t fade_rate = payload[16];
421
422 float power = 0;
423 if (power_raw > 0) {
424 power = POWER_SCALING_FACTOR / static_cast<float>(power_raw);
425 }
426
427 float voltage = 0;
428 if (voltage_raw > 0) {
429 voltage = VOLTAGE_SCALING_FACTOR / static_cast<float>(voltage_raw);
430 }
431
432 float current = 0;
433 if (current_raw > 0) {
434 current = CURRENT_SCALING_FACTOR / static_cast<float>(current_raw);
435 }
436
437 ESP_LOGI(TAG, "Got dimmer data:");
438 ESP_LOGI(TAG, " HW version: %d", hw_version);
439 ESP_LOGI(TAG, " Brightness: %d", brightness);
440 ESP_LOGI(TAG, " Fade rate: %d", fade_rate);
441 ESP_LOGI(TAG, " Power: %f W", power);
442 ESP_LOGI(TAG, " Voltage: %f V", voltage);
443 ESP_LOGI(TAG, " Current: %f A", 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 shelly_dimmer
525} // namespace esphome
526
527#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:301
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:195
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:191
void IRAM_ATTR HOT delay(uint32_t ms)
Definition core.cpp:28
uint32_t IRAM_ATTR HOT millis()
Definition core.cpp:27
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:162
uint8_t end[39]
Definition sun_gtil2.cpp:17
const uint8_t ESPHOME_WEBSERVER_INDEX_HTML[] PROGMEM
Definition web_server.h:29