ESPHome 2026.2.3
Loading...
Searching...
No Matches
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"
46
47namespace esphome {
48namespace sonoff_d1 {
49
50static const char *const TAG = "sonoff_d1";
51
52// Protocol constants
53static constexpr size_t SONOFF_D1_ACK_SIZE = 7;
54static constexpr size_t SONOFF_D1_MAX_CMD_SIZE = 17;
55
56uint8_t SonoffD1Output::calc_checksum_(const uint8_t *cmd, const size_t len) {
57 uint8_t crc = 0;
58 for (size_t i = 2; i < len - 1; i++) {
59 crc += cmd[i];
60 }
61 return crc;
62}
63
64void SonoffD1Output::populate_checksum_(uint8_t *cmd, const size_t len) {
65 // Update the checksum
66 cmd[len - 1] = this->calc_checksum_(cmd, len);
67}
68
70 size_t garbage = 0;
71 // Read out everything from the UART FIFO
72 while (this->available()) {
73 uint8_t value = this->read();
74 ESP_LOGW(TAG, "[%04d] Skip %02d: 0x%02x from the dimmer", this->write_count_, garbage, value);
75 garbage++;
76 }
77
78 // Warn about unexpected bytes in the protocol with UART dimmer
79 if (garbage) {
80 ESP_LOGW(TAG, "[%04d] Skip %d bytes from the dimmer", this->write_count_, garbage);
81 }
82}
83
84// This assumes some data is already available
85bool SonoffD1Output::read_command_(uint8_t *cmd, size_t &len) {
86 // Do consistency check
87 if (cmd == nullptr || len < 7) {
88 ESP_LOGW(TAG, "[%04d] Too short command buffer (actual len is %d bytes, minimal is 7)", this->write_count_, len);
89 return false;
90 }
91
92 // Read a minimal packet
93 if (this->read_array(cmd, 6)) {
94#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
95 char hex_buf[format_hex_pretty_size(6)];
96 ESP_LOGV(TAG,
97 "[%04d] Reading from dimmer:\n"
98 "[%04d] %s",
99 this->write_count_, this->write_count_, format_hex_pretty_to(hex_buf, cmd, 6));
100#endif
101
102 if (cmd[0] != 0xAA || cmd[1] != 0x55) {
103 ESP_LOGW(TAG, "[%04d] RX: wrong header (%x%x, must be AA55)", this->write_count_, cmd[0], cmd[1]);
104 this->skip_command_();
105 return false;
106 }
107 if ((cmd[5] + 7 /*mandatory header + crc suffix length*/) > len) {
108 ESP_LOGW(TAG, "[%04d] RX: Payload length is unexpected (%d, max expected %d)", this->write_count_, cmd[5],
109 len - 7);
110 this->skip_command_();
111 return false;
112 }
113 if (this->read_array(&cmd[6], cmd[5] + 1 /*checksum suffix*/)) {
114#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
115 char hex_buf2[format_hex_pretty_size(SONOFF_D1_MAX_CMD_SIZE)];
116 ESP_LOGV(TAG, "[%04d] %s", this->write_count_, format_hex_pretty_to(hex_buf2, &cmd[6], cmd[5] + 1));
117#endif
118
119 // Check the checksum
120 uint8_t valid_checksum = this->calc_checksum_(cmd, cmd[5] + 7);
121 if (valid_checksum != cmd[cmd[5] + 7 - 1]) {
122 ESP_LOGW(TAG, "[%04d] RX: checksum mismatch (%d, expected %d)", this->write_count_, cmd[cmd[5] + 7 - 1],
123 valid_checksum);
124 this->skip_command_();
125 return false;
126 }
127 len = cmd[5] + 7 /*mandatory header + suffix length*/;
128
129 // Read remaining gardbled data (just in case, I don't see where this can appear now)
130 this->skip_command_();
131 return true;
132 }
133 } else {
134 ESP_LOGW(TAG, "[%04d] RX: feedback timeout", this->write_count_);
135 this->skip_command_();
136 }
137 return false;
138}
139
140bool SonoffD1Output::read_ack_(const uint8_t *cmd, const size_t len) {
141 // Expected acknowledgement from rf chip
142 uint8_t ref_buffer[7] = {0xAA, 0x55, cmd[2], cmd[3], 0x00, 0x00, 0x00};
143 uint8_t buffer[sizeof(ref_buffer)] = {0};
144 uint32_t pos = 0;
145 size_t buf_len = sizeof(ref_buffer);
146
147 // Update the reference checksum
148 this->populate_checksum_(ref_buffer, sizeof(ref_buffer));
149
150 // Read ack code, this either reads 7 bytes or exits with a timeout
151 this->read_command_(buffer, buf_len);
152
153 // Compare response with expected response
154 while (pos < sizeof(ref_buffer) && ref_buffer[pos] == buffer[pos]) {
155 pos++;
156 }
157 if (pos == sizeof(ref_buffer)) {
158 ESP_LOGD(TAG, "[%04d] Acknowledge received", this->write_count_);
159 return true;
160 } else {
161 char hex_buf[format_hex_pretty_size(SONOFF_D1_ACK_SIZE)];
162 ESP_LOGW(TAG, "[%04d] Unexpected acknowledge received (possible clash of RF/HA commands), expected ack was:",
163 this->write_count_);
164 ESP_LOGW(TAG, "[%04d] %s", this->write_count_, format_hex_pretty_to(hex_buf, ref_buffer, sizeof(ref_buffer)));
165 }
166 return false;
167}
168
169bool SonoffD1Output::write_command_(uint8_t *cmd, const size_t len, bool needs_ack) {
170 // Do some consistency checks
171 if (len < 7) {
172 ESP_LOGW(TAG, "[%04d] Too short command (actual len is %d bytes, minimal is 7)", this->write_count_, len);
173 return false;
174 }
175 if (cmd[0] != 0xAA || cmd[1] != 0x55) {
176 ESP_LOGW(TAG, "[%04d] Wrong header (%x%x, must be AA55)", this->write_count_, cmd[0], cmd[1]);
177 return false;
178 }
179 if ((cmd[5] + 7 /*mandatory header + suffix length*/) != len) {
180 ESP_LOGW(TAG, "[%04d] Payload length field does not match packet length (%d, expected %d)", this->write_count_,
181 cmd[5], len - 7);
182 return false;
183 }
184 this->populate_checksum_(cmd, len);
185
186 // Need retries here to handle the following cases:
187 // 1. On power up companion MCU starts to respond with a delay, so few first commands are ignored
188 // 2. UART command initiated by this component can clash with a command initiated by RF
189 uint32_t retries = 10;
190 do {
191#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
192 char hex_buf[format_hex_pretty_size(SONOFF_D1_MAX_CMD_SIZE)];
193 ESP_LOGV(TAG,
194 "[%04d] Writing to the dimmer:\n"
195 "[%04d] %s",
196 this->write_count_, this->write_count_, format_hex_pretty_to(hex_buf, cmd, len));
197#endif
198 this->write_array(cmd, len);
199 this->write_count_++;
200 if (!needs_ack)
201 return true;
202 retries--;
203 } while (!this->read_ack_(cmd, len) && retries > 0);
204
205 if (retries) {
206 return true;
207 } else {
208 ESP_LOGE(TAG, "[%04d] Unable to write to the dimmer", this->write_count_);
209 }
210 return false;
211}
212
213bool SonoffD1Output::control_dimmer_(const bool binary, const uint8_t brightness) {
214 // Include our basic code from the Tasmota project, thank you again!
215 // 0 1 2 3 4 5 6 7 8
216 uint8_t cmd[17] = {0xAA, 0x55, 0x01, 0x04, 0x00, 0x0A, 0x00, 0x00, 0xFF,
217 // 9 10 11 12 13 14 15 16
218 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x00};
219
220 cmd[6] = binary;
221 cmd[7] = remap<uint8_t, uint8_t>(brightness, 0, 100, this->min_value_, this->max_value_);
222 ESP_LOGI(TAG, "[%04d] Setting dimmer state to %s, raw brightness=%d", this->write_count_, ONOFF(binary), cmd[7]);
223 return this->write_command_(cmd, sizeof(cmd));
224}
225
226void SonoffD1Output::process_command_(const uint8_t *cmd, const size_t len) {
227 if (cmd[2] == 0x01 && cmd[3] == 0x04 && cmd[4] == 0x00 && cmd[5] == 0x0A) {
228 uint8_t ack_buffer[7] = {0xAA, 0x55, cmd[2], cmd[3], 0x00, 0x00, 0x00};
229 // Ack a command from RF to ESP to prevent repeating commands
230 this->write_command_(ack_buffer, sizeof(ack_buffer), false);
231 ESP_LOGI(TAG, "[%04d] RF sets dimmer state to %s, raw brightness=%d", this->write_count_, ONOFF(cmd[6]), cmd[7]);
232 const uint8_t new_brightness = remap<uint8_t, uint8_t>(cmd[7], this->min_value_, this->max_value_, 0, 100);
233 const bool new_state = cmd[6];
234
235 // Got light change state command. In all cases we revert the command immediately
236 // since we want to rely on ESP controlled transitions
237 if (new_state != this->last_binary_ || new_brightness != this->last_brightness_) {
239 }
240
241 if (!this->use_rm433_remote_) {
242 // If RF remote is not used, this is a known ghost RF command
243 ESP_LOGI(TAG, "[%04d] Ghost command from RF detected, reverted", this->write_count_);
244 } else {
245 // If remote is used, initiate transition to the new state
246 this->publish_state_(new_state, new_brightness);
247 }
248 } else {
249 ESP_LOGW(TAG, "[%04d] Unexpected command received", this->write_count_);
250 }
251}
252
253void SonoffD1Output::publish_state_(const bool is_on, const uint8_t brightness) {
254 if (light_state_) {
255 ESP_LOGV(TAG, "Publishing new state: %s, brightness=%d", ONOFF(is_on), brightness);
256 auto call = light_state_->make_call();
257 call.set_state(is_on);
258 if (brightness != 0) {
259 // Brightness equal to 0 has a special meaning.
260 // D1 uses 0 as "previously set brightness".
261 // Usually zero brightness comes inside light ON command triggered by RF remote.
262 // Since we unconditionally override commands coming from RF remote in process_command_(),
263 // here we mimic the original behavior but with LightCall functionality
264 call.set_brightness((float) brightness / 100.0f);
265 }
266 call.perform();
267 }
268}
269
270// Set the device's traits
272 auto traits = light::LightTraits();
273 traits.set_supported_color_modes({light::ColorMode::BRIGHTNESS});
274 return traits;
275}
276
278 bool binary;
279 float brightness;
280
281 // Fill our variables with the device's current state
282 state->current_values_as_binary(&binary);
283 state->current_values_as_brightness(&brightness);
284
285 // Convert ESPHome's brightness (0-1) to the device's internal brightness (0-100)
286 const uint8_t calculated_brightness = (uint8_t) roundf(brightness * 100);
287
288 if (calculated_brightness == 0) {
289 // if(binary) ESP_LOGD(TAG, "current_values_as_binary() returns true for zero brightness");
290 binary = false;
291 }
292
293 // If a new value, write to the dimmer
294 if (binary != this->last_binary_ || calculated_brightness != this->last_brightness_) {
295 if (this->control_dimmer_(binary, calculated_brightness)) {
296 this->last_brightness_ = calculated_brightness;
297 this->last_binary_ = binary;
298 } else {
299 // Return to original value if failed to write to the dimmer
300 // TODO: Test me, can be tested if high-voltage part is not connected
301 ESP_LOGW(TAG, "Failed to update the dimmer, publishing the previous state");
302 this->publish_state_(this->last_binary_, this->last_brightness_);
303 }
304 }
305}
306
308 ESP_LOGCONFIG(TAG,
309 "Sonoff D1 Dimmer: '%s'\n"
310 " Use RM433 Remote: %s\n"
311 " Minimal brightness: %d\n"
312 " Maximal brightness: %d",
313 this->light_state_ ? this->light_state_->get_name().c_str() : "", ONOFF(this->use_rm433_remote_),
314 this->min_value_, this->max_value_);
315}
316
318 // Read commands from the dimmer
319 // RF chip notifies ESP about remotely changed state with the same commands as we send
320 if (this->available()) {
321 ESP_LOGV(TAG, "Have some UART data in loop()");
322 uint8_t buffer[17] = {0};
323 size_t len = sizeof(buffer);
324 if (this->read_command_(buffer, len)) {
325 this->process_command_(buffer, len);
326 }
327 }
328}
329
330} // namespace sonoff_d1
331} // namespace esphome
const StringRef & get_name() const
constexpr const char * c_str() const
Definition string_ref.h:73
LightCall & set_state(optional< bool > state)
Set the binary ON/OFF state of the light.
This class represents the communication layer between the front-end MQTT layer and the hardware outpu...
Definition light_state.h:91
This class is used to represent the capabilities of a light.
bool read_command_(uint8_t *cmd, size_t &len)
Definition sonoff_d1.cpp:85
void process_command_(const uint8_t *cmd, size_t len)
uint8_t calc_checksum_(const uint8_t *cmd, size_t len)
Definition sonoff_d1.cpp:56
bool control_dimmer_(bool binary, uint8_t brightness)
void populate_checksum_(uint8_t *cmd, size_t len)
Definition sonoff_d1.cpp:64
light::LightState * light_state_
Definition sonoff_d1.h:70
void write_state(light::LightState *state) override
void publish_state_(bool is_on, uint8_t brightness)
bool read_ack_(const uint8_t *cmd, size_t len)
light::LightTraits get_traits() override
bool write_command_(uint8_t *cmd, size_t len, bool needs_ack=true)
optional< std::array< uint8_t, N > > read_array()
Definition uart.h:38
void write_array(const uint8_t *data, size_t len)
Definition uart.h:26
bool state
Definition fan.h:2
@ BRIGHTNESS
Dimmable light.
Providing packet encoding functions for exchanging data with a remote host.
Definition a01nyub.cpp:7
std::string size_t len
Definition helpers.h:692
char * format_hex_pretty_to(char *buffer, size_t buffer_size, const uint8_t *data, size_t length, char separator)
Format byte array as uppercase hex to buffer (base implementation).
Definition helpers.cpp:353
size_t size_t pos
Definition helpers.h:729
constexpr size_t format_hex_pretty_size(size_t byte_count)
Calculate buffer size needed for format_hex_pretty_to with separator: "XX:XX:...:XX\0".
Definition helpers.h:978
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:443