ESPHome  2024.10.0
shelly_dimmer.cpp
Go to the documentation of this file.
1 #include "esphome/core/defines.h"
2 #include "esphome/core/helpers.h"
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 
20 namespace {
21 
22 constexpr char TAG[] = "shelly_dimmer";
23 
24 constexpr uint8_t SHELLY_DIMMER_ACK_TIMEOUT = 200; // ms
25 constexpr uint8_t SHELLY_DIMMER_MAX_RETRIES = 3;
26 constexpr uint16_t SHELLY_DIMMER_MAX_BRIGHTNESS = 1000; // 100%
27 
28 // Protocol framing.
29 constexpr uint8_t SHELLY_DIMMER_PROTO_START_BYTE = 0x01;
30 constexpr uint8_t SHELLY_DIMMER_PROTO_END_BYTE = 0x04;
31 
32 // Supported commands.
33 constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH = 0x01;
34 constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_POLL = 0x10;
35 constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_VERSION = 0x11;
36 constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS = 0x20;
37 
38 // Command payload sizes.
39 constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SWITCH_SIZE = 2;
40 constexpr uint8_t SHELLY_DIMMER_PROTO_CMD_SETTINGS_SIZE = 10;
41 constexpr uint8_t SHELLY_DIMMER_PROTO_MAX_FRAME_SIZE = 4 + 72 + 3;
42 
43 // STM Firmware
44 #ifdef USE_SHD_FIRMWARE_DATA
45 constexpr uint8_t STM_FIRMWARE[] PROGMEM = USE_SHD_FIRMWARE_DATA;
46 constexpr uint32_t STM_FIRMWARE_SIZE_IN_BYTES = sizeof(STM_FIRMWARE);
47 #endif
48 
49 // Scaling Constants
50 constexpr float POWER_SCALING_FACTOR = 880373;
51 constexpr float VOLTAGE_SCALING_FACTOR = 347800;
52 constexpr float CURRENT_SCALING_FACTOR = 1448;
53 
54 // Essentially std::size() for pre c++17
55 template<typename T, size_t N> constexpr size_t size(const T (&/*unused*/)[N]) noexcept { return N; }
56 
57 } // Anonymous namespace
58 
59 namespace esphome {
60 namespace shelly_dimmer {
61 
63 uint16_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 
115 void 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 
213 uint16_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 
222 void 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 
270 bool 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 
297 size_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 
477 void 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
size_t frame_command_(uint8_t *data, uint8_t cmd, const uint8_t *payload, size_t len)
Frames a given command payload.
virtual void digital_write(bool value)=0
void reset_normal_boot_()
Reset STM32 to boot the regular firmware.
This class represents the communication layer between the front-end MQTT layer and the hardware outpu...
Definition: light_state.h:34
void send_settings_()
Sends dimmer configuration.
void write_array(const uint8_t *data, size_t len)
Definition: uart.h:21
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:347
std::array< uint8_t, SHELLY_DIMMER_BUFFER_SIZE > buffer_
Definition: shelly_dimmer.h:58
int handle_byte_(uint8_t c)
Handles a single byte as part of a protocol frame.
void reset_dfu_boot_()
Reset STM32 to boot into DFU mode to enable firmware upgrades.
stm32_err_t stm32_write_memory(const stm32_unique_ptr &stm, uint32_t address, const uint8_t *data, const unsigned int len)
Definition: stm32flash.cpp:698
bool read_frame_()
Reads a response frame.
void write_state(light::LightState *state) override
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:186
virtual void setup()=0
void reset_(bool boot0)
Reset STM32 with the BOOT0 pin set to the given value.
constexpr auto STREAM_SERIAL
Definition: stm32flash.h:40
uint32_t IRAM_ATTR HOT millis()
Definition: core.cpp:25
bool send_command_(uint8_t cmd, const uint8_t *payload, uint8_t len)
Sends a command and waits for an acknowledgement.
const char *const TAG
Definition: spi.cpp:8
uint16_t convert_brightness_(float brightness)
Convert relative brightness into a dimmer brightness value.
uint16_t shelly_dimmer_checksum(const uint8_t *buf, int len)
Computes a crappy checksum as defined by the Shelly Dimmer protocol.
bool upgrade_firmware_()
Performs a firmware upgrade.
void publish_state(float state)
Publish a new state to the front-end.
Definition: sensor.cpp:39
void current_values_as_brightness(float *brightness)
stm32_unique_ptr stm32_init(uart::UARTDevice *stream, const uint8_t flags, const char init)
Definition: stm32flash.cpp:500
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:182
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)
Definition: stm32flash.cpp:811
std::string size_t len
Definition: helpers.h:292
bool handle_frame_()
Handles a complete frame.
const uint8_t ESPHOME_WEBSERVER_INDEX_HTML [] PROGMEM
Definition: web_server.h:25
virtual void mark_failed()
Mark this component as failed.
Definition: component.cpp:118
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
uint8_t end[39]
Definition: sun_gtil2.cpp:31
stm32_cmd_t * cmd
Definition: stm32flash.h:96
void send_brightness_(uint16_t brightness)
Sends the given brightness value.
bool state
Definition: fan.h:34
void IRAM_ATTR HOT delay(uint32_t ms)
Definition: core.cpp:26