ESPHome  2024.4.1
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, buf_len = sizeof(ref_buffer);
132 
133  // Update the reference checksum
134  this->populate_checksum_(ref_buffer, sizeof(ref_buffer));
135 
136  // Read ack code, this either reads 7 bytes or exits with a timeout
137  this->read_command_(buffer, buf_len);
138 
139  // Compare response with expected response
140  while (pos < sizeof(ref_buffer) && ref_buffer[pos] == buffer[pos]) {
141  pos++;
142  }
143  if (pos == sizeof(ref_buffer)) {
144  ESP_LOGD(TAG, "[%04d] Acknowledge received", this->write_count_);
145  return true;
146  } else {
147  ESP_LOGW(TAG, "[%04d] Unexpected acknowledge received (possible clash of RF/HA commands), expected ack was:",
148  this->write_count_);
149  ESP_LOGW(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(ref_buffer, sizeof(ref_buffer)).c_str());
150  }
151  return false;
152 }
153 
154 bool SonoffD1Output::write_command_(uint8_t *cmd, const size_t len, bool needs_ack) {
155  // Do some consistency checks
156  if (len < 7) {
157  ESP_LOGW(TAG, "[%04d] Too short command (actual len is %d bytes, minimal is 7)", this->write_count_, len);
158  return false;
159  }
160  if (cmd[0] != 0xAA || cmd[1] != 0x55) {
161  ESP_LOGW(TAG, "[%04d] Wrong header (%x%x, must be AA55)", this->write_count_, cmd[0], cmd[1]);
162  return false;
163  }
164  if ((cmd[5] + 7 /*mandatory header + suffix length*/) != len) {
165  ESP_LOGW(TAG, "[%04d] Payload length field does not match packet length (%d, expected %d)", this->write_count_,
166  cmd[5], len - 7);
167  return false;
168  }
169  this->populate_checksum_(cmd, len);
170 
171  // Need retries here to handle the following cases:
172  // 1. On power up companion MCU starts to respond with a delay, so few first commands are ignored
173  // 2. UART command initiated by this component can clash with a command initiated by RF
174  uint32_t retries = 10;
175  do {
176  ESP_LOGV(TAG, "[%04d] Writing to the dimmer:", this->write_count_);
177  ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty(cmd, len).c_str());
178  this->write_array(cmd, len);
179  this->write_count_++;
180  if (!needs_ack)
181  return true;
182  retries--;
183  } while (!this->read_ack_(cmd, len) && retries > 0);
184 
185  if (retries) {
186  return true;
187  } else {
188  ESP_LOGE(TAG, "[%04d] Unable to write to the dimmer", this->write_count_);
189  }
190  return false;
191 }
192 
193 bool SonoffD1Output::control_dimmer_(const bool binary, const uint8_t brightness) {
194  // Include our basic code from the Tasmota project, thank you again!
195  // 0 1 2 3 4 5 6 7 8
196  uint8_t cmd[17] = {0xAA, 0x55, 0x01, 0x04, 0x00, 0x0A, 0x00, 0x00, 0xFF,
197  // 9 10 11 12 13 14 15 16
198  0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00};
199 
200  cmd[6] = binary;
201  cmd[7] = remap<uint8_t, uint8_t>(brightness, 0, 100, this->min_value_, this->max_value_);
202  ESP_LOGI(TAG, "[%04d] Setting dimmer state to %s, raw brightness=%d", this->write_count_, ONOFF(binary), cmd[7]);
203  return this->write_command_(cmd, sizeof(cmd));
204 }
205 
206 void SonoffD1Output::process_command_(const uint8_t *cmd, const size_t len) {
207  if (cmd[2] == 0x01 && cmd[3] == 0x04 && cmd[4] == 0x00 && cmd[5] == 0x0A) {
208  uint8_t ack_buffer[7] = {0xAA, 0x55, cmd[2], cmd[3], 0x00, 0x00, 0x00};
209  // Ack a command from RF to ESP to prevent repeating commands
210  this->write_command_(ack_buffer, sizeof(ack_buffer), false);
211  ESP_LOGI(TAG, "[%04d] RF sets dimmer state to %s, raw brightness=%d", this->write_count_, ONOFF(cmd[6]), cmd[7]);
212  const uint8_t new_brightness = remap<uint8_t, uint8_t>(cmd[7], this->min_value_, this->max_value_, 0, 100);
213  const bool new_state = cmd[6];
214 
215  // Got light change state command. In all cases we revert the command immediately
216  // since we want to rely on ESP controlled transitions
217  if (new_state != this->last_binary_ || new_brightness != this->last_brightness_) {
218  this->control_dimmer_(this->last_binary_, this->last_brightness_);
219  }
220 
221  if (!this->use_rm433_remote_) {
222  // If RF remote is not used, this is a known ghost RF command
223  ESP_LOGI(TAG, "[%04d] Ghost command from RF detected, reverted", this->write_count_);
224  } else {
225  // If remote is used, initiate transition to the new state
226  this->publish_state_(new_state, new_brightness);
227  }
228  } else {
229  ESP_LOGW(TAG, "[%04d] Unexpected command received", this->write_count_);
230  }
231 }
232 
233 void SonoffD1Output::publish_state_(const bool is_on, const uint8_t brightness) {
234  if (light_state_) {
235  ESP_LOGV(TAG, "Publishing new state: %s, brightness=%d", ONOFF(is_on), brightness);
236  auto call = light_state_->make_call();
237  call.set_state(is_on);
238  if (brightness != 0) {
239  // Brightness equal to 0 has a special meaning.
240  // D1 uses 0 as "previously set brightness".
241  // Usually zero brightness comes inside light ON command triggered by RF remote.
242  // Since we unconditionally override commands coming from RF remote in process_command_(),
243  // here we mimic the original behavior but with LightCall functionality
244  call.set_brightness((float) brightness / 100.0f);
245  }
246  call.perform();
247  }
248 }
249 
250 // Set the device's traits
252  auto traits = light::LightTraits();
253  traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS});
254  return traits;
255 }
256 
258  bool binary;
259  float brightness;
260 
261  // Fill our variables with the device's current state
262  state->current_values_as_binary(&binary);
263  state->current_values_as_brightness(&brightness);
264 
265  // Convert ESPHome's brightness (0-1) to the device's internal brightness (0-100)
266  const uint8_t calculated_brightness = (uint8_t) roundf(brightness * 100);
267 
268  if (calculated_brightness == 0) {
269  // if(binary) ESP_LOGD(TAG, "current_values_as_binary() returns true for zero brightness");
270  binary = false;
271  }
272 
273  // If a new value, write to the dimmer
274  if (binary != this->last_binary_ || calculated_brightness != this->last_brightness_) {
275  if (this->control_dimmer_(binary, calculated_brightness)) {
276  this->last_brightness_ = calculated_brightness;
277  this->last_binary_ = binary;
278  } else {
279  // Return to original value if failed to write to the dimmer
280  // TODO: Test me, can be tested if high-voltage part is not connected
281  ESP_LOGW(TAG, "Failed to update the dimmer, publishing the previous state");
282  this->publish_state_(this->last_binary_, this->last_brightness_);
283  }
284  }
285 }
286 
288  ESP_LOGCONFIG(TAG, "Sonoff D1 Dimmer: '%s'", this->light_state_ ? this->light_state_->get_name().c_str() : "");
289  ESP_LOGCONFIG(TAG, " Use RM433 Remote: %s", ONOFF(this->use_rm433_remote_));
290  ESP_LOGCONFIG(TAG, " Minimal brightness: %d", this->min_value_);
291  ESP_LOGCONFIG(TAG, " Maximal brightness: %d", this->max_value_);
292 }
293 
295  // Read commands from the dimmer
296  // RF chip notifies ESP about remotely changed state with the same commands as we send
297  if (this->available()) {
298  ESP_LOGV(TAG, "Have some UART data in loop()");
299  uint8_t buffer[17] = {0};
300  size_t len = sizeof(buffer);
301  if (this->read_command_(buffer, len)) {
302  this->process_command_(buffer, len);
303  }
304  }
305 }
306 
307 } // namespace sonoff_d1
308 } // namespace esphome
bool control_dimmer_(bool binary, uint8_t brightness)
Definition: sonoff_d1.cpp:193
This class represents the communication layer between the front-end MQTT layer and the hardware outpu...
Definition: light_state.h:34
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:361
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:233
void process_command_(const uint8_t *cmd, size_t len)
Definition: sonoff_d1.cpp:206
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:154
light::LightTraits get_traits() override
Definition: sonoff_d1.cpp:251
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:292
This is a workaround until we can figure out a way to get the tflite-micro idf component code availab...
Definition: a01nyub.cpp:7
light::LightState * light_state_
Definition: sonoff_d1.h:71
void write_state(light::LightState *state) override
Definition: sonoff_d1.cpp:257
const StringRef & get_name() const
Definition: entity_base.cpp:10
stm32_cmd_t * cmd
Definition: stm32flash.h:96
bool state
Definition: fan.h:34