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