ESPHome 2025.5.2
All Data Structures Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Modules Pages
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
46namespace esphome {
47namespace sonoff_d1 {
48
49static const char *const TAG = "sonoff_d1";
50
51uint8_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
59void 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
80bool 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
127bool 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
155bool 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
194bool 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
207void 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_) {
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
234void 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
const StringRef & get_name() const
constexpr const char * c_str() const
Definition string_ref.h:69
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:63
This class is used to represent the capabilities of a light.
bool read_command_(uint8_t *cmd, size_t &len)
Definition sonoff_d1.cpp:80
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:51
bool control_dimmer_(bool binary, uint8_t brightness)
void populate_checksum_(uint8_t *cmd, size_t len)
Definition sonoff_d1.cpp:59
light::LightState * light_state_
Definition sonoff_d1.h:71
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:33
void write_array(const uint8_t *data, size_t len)
Definition uart.h:21
bool state
Definition fan.h:0
@ 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:302
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:163
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:372