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