ESPHome  2024.11.0
sonoff_d1.cpp
Go to the documentation of this file.
1 /*
2  sonoff_d1.cpp - Sonoff D1 Dimmer support for ESPHome
3 
4  Copyright © 2021 Anatoly Savchenkov
5  Copyright © 2020 Jeff Rescignano
6 
7  Permission is hereby granted, free of charge, to any person obtaining a copy of this software
8  and associated documentation files (the “Software”), to deal in the Software without
9  restriction, including without limitation the rights to use, copy, modify, merge, publish,
10  distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom
11  the Software is furnished to do so, subject to the following conditions:
12 
13  The above copyright notice and this permission notice shall be included in all copies or
14  substantial portions of the Software.
15 
16  THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING
17  BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18  NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
19  DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
20  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 
22  -----
23 
24  If modifying this file, in addition to the license above, please ensure to include links back to the original code:
25  https://jeffresc.dev/blog/2020-10-10
26  https://github.com/JeffResc/Sonoff-D1-Dimmer
27  https://github.com/arendst/Tasmota/blob/2d4a6a29ebc7153dbe2717e3615574ac1c84ba1d/tasmota/xdrv_37_sonoff_d1.ino#L119-L131
28 
29  -----
30 */
31 
32 /*********************************************************************************************\
33  * Sonoff D1 dimmer 433
34  * Mandatory/Optional
35  * ^ 0 1 2 3 4 5 6 7 8 9 A B C D E F 10
36  * M AA 55 - Header
37  * M 01 04 - Version?
38  * M 00 0A - Following data length (10 bytes)
39  * O 01 - Power state (00 = off, 01 = on, FF = ignore)
40  * O 64 - Dimmer percentage (01 to 64 = 1 to 100%, 0 - ignore)
41  * O FF FF FF FF FF FF FF FF - Not used
42  * M 6C - CRC over bytes 2 to F (Addition)
43 \*********************************************************************************************/
44 #include "sonoff_d1.h"
45 
46 namespace esphome {
47 namespace sonoff_d1 {
48 
49 static const char *const TAG = "sonoff_d1";
50 
51 uint8_t SonoffD1Output::calc_checksum_(const uint8_t *cmd, const size_t len) {
52  uint8_t crc = 0;
53  for (int i = 2; i < len - 1; i++) {
54  crc += cmd[i];
55  }
56  return crc;
57 }
58 
59 void SonoffD1Output::populate_checksum_(uint8_t *cmd, const size_t len) {
60  // Update the checksum
61  cmd[len - 1] = this->calc_checksum_(cmd, len);
62 }
63 
65  size_t garbage = 0;
66  // Read out everything from the UART FIFO
67  while (this->available()) {
68  uint8_t value = this->read();
69  ESP_LOGW(TAG, "[%04d] Skip %02d: 0x%02x from the dimmer", this->write_count_, garbage, value);
70  garbage++;
71  }
72 
73  // Warn about unexpected bytes in the protocol with UART dimmer
74  if (garbage) {
75  ESP_LOGW(TAG, "[%04d] Skip %d bytes from the dimmer", this->write_count_, garbage);
76  }
77 }
78 
79 // This assumes some data is already available
80 bool SonoffD1Output::read_command_(uint8_t *cmd, size_t &len) {
81  // Do consistency check
82  if (cmd == nullptr || len < 7) {
83  ESP_LOGW(TAG, "[%04d] Too short command buffer (actual len is %d bytes, minimal is 7)", this->write_count_, len);
84  return false;
85  }
86 
87  // Read a minimal packet
88  if (this->read_array(cmd, 6)) {
89  ESP_LOGV(TAG, "[%04d] Reading from dimmer:", this->write_count_);
90  ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(cmd, 6).c_str());
91 
92  if (cmd[0] != 0xAA || cmd[1] != 0x55) {
93  ESP_LOGW(TAG, "[%04d] RX: wrong header (%x%x, must be AA55)", this->write_count_, cmd[0], cmd[1]);
94  this->skip_command_();
95  return false;
96  }
97  if ((cmd[5] + 7 /*mandatory header + crc suffix length*/) > len) {
98  ESP_LOGW(TAG, "[%04d] RX: Payload length is unexpected (%d, max expected %d)", this->write_count_, cmd[5],
99  len - 7);
100  this->skip_command_();
101  return false;
102  }
103  if (this->read_array(&cmd[6], cmd[5] + 1 /*checksum suffix*/)) {
104  ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(&cmd[6], cmd[5] + 1).c_str());
105 
106  // Check the checksum
107  uint8_t valid_checksum = this->calc_checksum_(cmd, cmd[5] + 7);
108  if (valid_checksum != cmd[cmd[5] + 7 - 1]) {
109  ESP_LOGW(TAG, "[%04d] RX: checksum mismatch (%d, expected %d)", this->write_count_, cmd[cmd[5] + 7 - 1],
110  valid_checksum);
111  this->skip_command_();
112  return false;
113  }
114  len = cmd[5] + 7 /*mandatory header + suffix length*/;
115 
116  // Read remaining gardbled data (just in case, I don't see where this can appear now)
117  this->skip_command_();
118  return true;
119  }
120  } else {
121  ESP_LOGW(TAG, "[%04d] RX: feedback timeout", this->write_count_);
122  this->skip_command_();
123  }
124  return false;
125 }
126 
127 bool SonoffD1Output::read_ack_(const uint8_t *cmd, const size_t len) {
128  // Expected acknowledgement from rf chip
129  uint8_t ref_buffer[7] = {0xAA, 0x55, cmd[2], cmd[3], 0x00, 0x00, 0x00};
130  uint8_t buffer[sizeof(ref_buffer)] = {0};
131  uint32_t pos = 0;
132  size_t buf_len = sizeof(ref_buffer);
133 
134  // Update the reference checksum
135  this->populate_checksum_(ref_buffer, sizeof(ref_buffer));
136 
137  // Read ack code, this either reads 7 bytes or exits with a timeout
138  this->read_command_(buffer, buf_len);
139 
140  // Compare response with expected response
141  while (pos < sizeof(ref_buffer) && ref_buffer[pos] == buffer[pos]) {
142  pos++;
143  }
144  if (pos == sizeof(ref_buffer)) {
145  ESP_LOGD(TAG, "[%04d] Acknowledge received", this->write_count_);
146  return true;
147  } else {
148  ESP_LOGW(TAG, "[%04d] Unexpected acknowledge received (possible clash of RF/HA commands), expected ack was:",
149  this->write_count_);
150  ESP_LOGW(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(ref_buffer, sizeof(ref_buffer)).c_str());
151  }
152  return false;
153 }
154 
155 bool SonoffD1Output::write_command_(uint8_t *cmd, const size_t len, bool needs_ack) {
156  // Do some consistency checks
157  if (len < 7) {
158  ESP_LOGW(TAG, "[%04d] Too short command (actual len is %d bytes, minimal is 7)", this->write_count_, len);
159  return false;
160  }
161  if (cmd[0] != 0xAA || cmd[1] != 0x55) {
162  ESP_LOGW(TAG, "[%04d] Wrong header (%x%x, must be AA55)", this->write_count_, cmd[0], cmd[1]);
163  return false;
164  }
165  if ((cmd[5] + 7 /*mandatory header + suffix length*/) != len) {
166  ESP_LOGW(TAG, "[%04d] Payload length field does not match packet length (%d, expected %d)", this->write_count_,
167  cmd[5], len - 7);
168  return false;
169  }
170  this->populate_checksum_(cmd, len);
171 
172  // Need retries here to handle the following cases:
173  // 1. On power up companion MCU starts to respond with a delay, so few first commands are ignored
174  // 2. UART command initiated by this component can clash with a command initiated by RF
175  uint32_t retries = 10;
176  do {
177  ESP_LOGV(TAG, "[%04d] Writing to the dimmer:", this->write_count_);
178  ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(cmd, len).c_str());
179  this->write_array(cmd, len);
180  this->write_count_++;
181  if (!needs_ack)
182  return true;
183  retries--;
184  } while (!this->read_ack_(cmd, len) && retries > 0);
185 
186  if (retries) {
187  return true;
188  } else {
189  ESP_LOGE(TAG, "[%04d] Unable to write to the dimmer", this->write_count_);
190  }
191  return false;
192 }
193 
194 bool SonoffD1Output::control_dimmer_(const bool binary, const uint8_t brightness) {
195  // Include our basic code from the Tasmota project, thank you again!
196  // 0 1 2 3 4 5 6 7 8
197  uint8_t cmd[17] = {0xAA, 0x55, 0x01, 0x04, 0x00, 0x0A, 0x00, 0x00, 0xFF,
198  // 9 10 11 12 13 14 15 16
199  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00};
200 
201  cmd[6] = binary;
202  cmd[7] = remap<uint8_t, uint8_t>(brightness, 0, 100, this->min_value_, this->max_value_);
203  ESP_LOGI(TAG, "[%04d] Setting dimmer state to %s, raw brightness=%d", this->write_count_, ONOFF(binary), cmd[7]);
204  return this->write_command_(cmd, sizeof(cmd));
205 }
206 
207 void SonoffD1Output::process_command_(const uint8_t *cmd, const size_t len) {
208  if (cmd[2] == 0x01 && cmd[3] == 0x04 && cmd[4] == 0x00 && cmd[5] == 0x0A) {
209  uint8_t ack_buffer[7] = {0xAA, 0x55, cmd[2], cmd[3], 0x00, 0x00, 0x00};
210  // Ack a command from RF to ESP to prevent repeating commands
211  this->write_command_(ack_buffer, sizeof(ack_buffer), false);
212  ESP_LOGI(TAG, "[%04d] RF sets dimmer state to %s, raw brightness=%d", this->write_count_, ONOFF(cmd[6]), cmd[7]);
213  const uint8_t new_brightness = remap<uint8_t, uint8_t>(cmd[7], this->min_value_, this->max_value_, 0, 100);
214  const bool new_state = cmd[6];
215 
216  // Got light change state command. In all cases we revert the command immediately
217  // since we want to rely on ESP controlled transitions
218  if (new_state != this->last_binary_ || new_brightness != this->last_brightness_) {
219  this->control_dimmer_(this->last_binary_, this->last_brightness_);
220  }
221 
222  if (!this->use_rm433_remote_) {
223  // If RF remote is not used, this is a known ghost RF command
224  ESP_LOGI(TAG, "[%04d] Ghost command from RF detected, reverted", this->write_count_);
225  } else {
226  // If remote is used, initiate transition to the new state
227  this->publish_state_(new_state, new_brightness);
228  }
229  } else {
230  ESP_LOGW(TAG, "[%04d] Unexpected command received", this->write_count_);
231  }
232 }
233 
234 void SonoffD1Output::publish_state_(const bool is_on, const uint8_t brightness) {
235  if (light_state_) {
236  ESP_LOGV(TAG, "Publishing new state: %s, brightness=%d", ONOFF(is_on), brightness);
237  auto call = light_state_->make_call();
238  call.set_state(is_on);
239  if (brightness != 0) {
240  // Brightness equal to 0 has a special meaning.
241  // D1 uses 0 as "previously set brightness".
242  // Usually zero brightness comes inside light ON command triggered by RF remote.
243  // Since we unconditionally override commands coming from RF remote in process_command_(),
244  // here we mimic the original behavior but with LightCall functionality
245  call.set_brightness((float) brightness / 100.0f);
246  }
247  call.perform();
248  }
249 }
250 
251 // Set the device's traits
253  auto traits = light::LightTraits();
254  traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS});
255  return traits;
256 }
257 
259  bool binary;
260  float brightness;
261 
262  // Fill our variables with the device's current state
263  state->current_values_as_binary(&binary);
264  state->current_values_as_brightness(&brightness);
265 
266  // Convert ESPHome's brightness (0-1) to the device's internal brightness (0-100)
267  const uint8_t calculated_brightness = (uint8_t) roundf(brightness * 100);
268 
269  if (calculated_brightness == 0) {
270  // if(binary) ESP_LOGD(TAG, "current_values_as_binary() returns true for zero brightness");
271  binary = false;
272  }
273 
274  // If a new value, write to the dimmer
275  if (binary != this->last_binary_ || calculated_brightness != this->last_brightness_) {
276  if (this->control_dimmer_(binary, calculated_brightness)) {
277  this->last_brightness_ = calculated_brightness;
278  this->last_binary_ = binary;
279  } else {
280  // Return to original value if failed to write to the dimmer
281  // TODO: Test me, can be tested if high-voltage part is not connected
282  ESP_LOGW(TAG, "Failed to update the dimmer, publishing the previous state");
283  this->publish_state_(this->last_binary_, this->last_brightness_);
284  }
285  }
286 }
287 
289  ESP_LOGCONFIG(TAG, "Sonoff D1 Dimmer: '%s'", this->light_state_ ? this->light_state_->get_name().c_str() : "");
290  ESP_LOGCONFIG(TAG, " Use RM433 Remote: %s", ONOFF(this->use_rm433_remote_));
291  ESP_LOGCONFIG(TAG, " Minimal brightness: %d", this->min_value_);
292  ESP_LOGCONFIG(TAG, " Maximal brightness: %d", this->max_value_);
293 }
294 
296  // Read commands from the dimmer
297  // RF chip notifies ESP about remotely changed state with the same commands as we send
298  if (this->available()) {
299  ESP_LOGV(TAG, "Have some UART data in loop()");
300  uint8_t buffer[17] = {0};
301  size_t len = sizeof(buffer);
302  if (this->read_command_(buffer, len)) {
303  this->process_command_(buffer, len);
304  }
305  }
306 }
307 
308 } // namespace sonoff_d1
309 } // namespace esphome
bool control_dimmer_(bool binary, uint8_t brightness)
Definition: sonoff_d1.cpp:194
This class represents the communication layer between the front-end MQTT layer and the hardware outpu...
Definition: light_state.h:63
optional< std::array< uint8_t, N > > read_array()
Definition: uart.h:33
std::string format_hex_pretty(const uint8_t *data, size_t length)
Format the byte array data of length len in pretty-printed, human-readable hex.
Definition: helpers.cpp:364
void write_array(const uint8_t *data, size_t len)
Definition: uart.h:21
bool read_command_(uint8_t *cmd, size_t &len)
Definition: sonoff_d1.cpp:80
uint8_t calc_checksum_(const uint8_t *cmd, size_t len)
Definition: sonoff_d1.cpp:51
void publish_state_(bool is_on, uint8_t brightness)
Definition: sonoff_d1.cpp:234
void process_command_(const uint8_t *cmd, size_t len)
Definition: sonoff_d1.cpp:207
void current_values_as_binary(bool *binary)
The result of all the current_values_as_* methods have gamma correction applied.
bool write_command_(uint8_t *cmd, size_t len, bool needs_ack=true)
Definition: sonoff_d1.cpp:155
light::LightTraits get_traits() override
Definition: sonoff_d1.cpp:252
void current_values_as_brightness(float *brightness)
Master brightness of the light can be controlled.
constexpr const char * c_str() const
Definition: string_ref.h:68
This class is used to represent the capabilities of a light.
Definition: light_traits.h:11
bool read_ack_(const uint8_t *cmd, size_t len)
Definition: sonoff_d1.cpp:127
void populate_checksum_(uint8_t *cmd, size_t len)
Definition: sonoff_d1.cpp:59
std::string size_t len
Definition: helpers.h:293
Implementation of SPI Controller mode.
Definition: a01nyub.cpp:7
light::LightState * light_state_
Definition: sonoff_d1.h:71
void write_state(light::LightState *state) override
Definition: sonoff_d1.cpp:258
const StringRef & get_name() const
Definition: entity_base.cpp:10
stm32_cmd_t * cmd
Definition: stm32flash.h:96
bool state
Definition: fan.h:34