ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
whirlpool.cpp
Go to the documentation of this file.
1#include "whirlpool.h"
3#include "esphome/core/log.h"
4
6
7ESPHOME_LOG_TAG(TAG, "whirlpool.climate");
8
9const uint16_t WHIRLPOOL_HEADER_MARK = 9000;
10const uint16_t WHIRLPOOL_HEADER_SPACE = 4494;
11const uint16_t WHIRLPOOL_BIT_MARK = 572;
12static constexpr remote_base::BitTimings WHIRLPOOL_BIT_TIMINGS{
13 .bit_mark = WHIRLPOOL_BIT_MARK, .one_space = 1659, .zero_space = 553};
15
17
18const uint8_t WHIRLPOOL_STATE_LENGTH = 21;
19static constexpr uint8_t WHIRLPOOL_FIRST_DIVIDER = 6;
20static constexpr uint8_t WHIRLPOOL_SECOND_DIVIDER = 14;
21
22const uint8_t WHIRLPOOL_HEAT = 0;
23const uint8_t WHIRLPOOL_DRY = 3;
24const uint8_t WHIRLPOOL_COOL = 2;
25const uint8_t WHIRLPOOL_FAN = 4;
26const uint8_t WHIRLPOOL_AUTO = 1;
27
28const uint8_t WHIRLPOOL_FAN_AUTO = 0;
29const uint8_t WHIRLPOOL_FAN_HIGH = 1;
30const uint8_t WHIRLPOOL_FAN_MED = 2;
31const uint8_t WHIRLPOOL_FAN_LOW = 3;
32
33const uint8_t WHIRLPOOL_FAN_MASK = 0x03;
34
35const uint8_t WHIRLPOOL_SWING_MASK = 128; // byte 2
36const uint8_t WHIRLPOOL_SWING_BYTE8 = 0x40; // byte 8
37
38const uint8_t WHIRLPOOL_POWER = 0x04;
39
40// Hisense ARG model; unused constexpr values cost nothing in DG11J1-only builds
41static constexpr uint8_t HISENSE_ARG_DISPLAY_ON = 0x80; // byte 11
42static constexpr uint8_t HISENSE_ARG_AMBIENT_TEMP = 0x18; // byte 12, 24 C
43static constexpr uint8_t HISENSE_ARG_POWER_TOGGLE = 0x01; // byte 15
44static constexpr uint8_t HISENSE_ARG_SETTINGS_ONLY = 0x02; // byte 15
45static constexpr uint8_t HISENSE_ARG_FIXED_TEMP_STEP = 7; // auto and fan-only send 23 C
46
53
55 this->last_transmit_time_ = millis(); // setting the time of the last transmission.
56 uint8_t remote_state[WHIRLPOOL_STATE_LENGTH] = {0};
57 remote_state[0] = 0x83;
58 remote_state[1] = 0x06;
59 remote_state[6] = 0x80;
60 remote_state[18] = 0x08; // the same on every supported model
61
62 auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
63 const bool power_toggle = powered_on != this->powered_on_assumed;
64 if (power_toggle) {
65 // Set power toggle command
66 remote_state[2] = 4;
67 remote_state[15] = 1;
68 this->powered_on_assumed = powered_on;
69 }
70 switch (this->mode) {
72 // set fan auto
73 // set temp auto temp
74 // set sleep false
75 remote_state[3] = WHIRLPOOL_AUTO;
76 remote_state[15] = 0x17;
77 break;
79 remote_state[3] = WHIRLPOOL_HEAT;
80 remote_state[15] = 6;
81 break;
83 remote_state[3] = WHIRLPOOL_COOL;
84 remote_state[15] = 6;
85 break;
87 remote_state[3] = WHIRLPOOL_DRY;
88 remote_state[15] = 6;
89 break;
91 remote_state[3] = WHIRLPOOL_FAN;
92 remote_state[15] = 6;
93 break;
95 default:
96 break;
97 }
98
99 const bool hisense_arg = this->is_hisense_arg_();
100
101 // Temperature
102 auto temp = (uint8_t) roundf(clamp(this->target_temperature, this->temperature_min_(), this->temperature_max_()));
103 const auto temp_step = (uint8_t) (temp - this->temperature_min_());
104 if (hisense_arg && this->fixed_temp_mode_()) {
105 remote_state[3] |= HISENSE_ARG_FIXED_TEMP_STEP << 4;
106 } else {
107 remote_state[3] |= temp_step << 4;
108 }
109
110 // Fan speed
111 switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
113 remote_state[2] |= WHIRLPOOL_FAN_HIGH;
114 break;
116 remote_state[2] |= WHIRLPOOL_FAN_MED;
117 break;
119 remote_state[2] |= WHIRLPOOL_FAN_LOW;
120 break;
121 default:
122 break;
123 }
124
125 // Swing
126 ESP_LOGV(TAG, "send swing %s", this->send_swing_cmd_ ? LOG_STR_LITERAL("true") : LOG_STR_LITERAL("false"));
127 if (hisense_arg) {
128 // This model sends swing as a state on every frame; the DG11J1 remotes send a toggle
130 remote_state[2] |= WHIRLPOOL_SWING_MASK;
131 remote_state[8] = WHIRLPOOL_SWING_BYTE8;
132 }
133 this->encode_hisense_arg_(remote_state, power_toggle, temp_step);
134 } else if (this->send_swing_cmd_) {
136 remote_state[2] |= WHIRLPOOL_SWING_MASK;
137 remote_state[8] |= WHIRLPOOL_SWING_BYTE8;
138 }
139 }
140
141 // Checksum
142 remote_state[13] = xor_bytes(&remote_state[2], 11, remote_state[13]);
143 remote_state[20] = xor_bytes(&remote_state[14], 6, remote_state[20]);
144
145 ESP_LOGV(TAG,
146 "Sending: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X "
147 "%02X %02X %02X",
148 remote_state[0], remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5],
149 remote_state[6], remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11],
150 remote_state[12], remote_state[13], remote_state[14], remote_state[15], remote_state[16], remote_state[17],
151 remote_state[18], remote_state[19], remote_state[20]);
152
153 // Send code
154 auto transmit = this->transmitter_->transmit();
155 auto *data = transmit.get_data();
156
157 data->set_carrier_frequency(38000);
158
159 // Header
160 data->mark(WHIRLPOOL_HEADER_MARK);
161 data->space(WHIRLPOOL_HEADER_SPACE);
162 // Data
163 for (uint8_t i = 0; i < WHIRLPOOL_STATE_LENGTH; i++) {
164 if (i == WHIRLPOOL_FIRST_DIVIDER || i == WHIRLPOOL_SECOND_DIVIDER) {
165 // Divider
166 data->mark(WHIRLPOOL_BIT_MARK);
167 data->space(WHIRLPOOL_GAP);
168 }
169 data->item_bytes<WHIRLPOOL_BIT_TIMINGS>(remote_state + i, 1);
170 }
171 // Footer
172 data->mark(WHIRLPOOL_BIT_MARK);
173
174 transmit.perform();
175}
176
180
181void WhirlpoolClimate::encode_hisense_arg_(uint8_t *remote_state, bool power_toggle, uint8_t temp_step) const {
182#ifdef USE_WHIRLPOOL_HISENSE_ARG
183 remote_state[11] = HISENSE_ARG_DISPLAY_ON;
184 remote_state[12] = HISENSE_ARG_AMBIENT_TEMP;
185 remote_state[15] = power_toggle ? HISENSE_ARG_POWER_TOGGLE : HISENSE_ARG_SETTINGS_ONLY;
186
187 switch (this->mode) {
189 // The remote keeps the cool code in the power-off frame
190 remote_state[3] = (remote_state[3] & 0xF0) | WHIRLPOOL_COOL;
191 break;
193 // The unit runs these fan speeds itself; the user's choice is kept for the next mode change
194 remote_state[2] &= ~WHIRLPOOL_FAN_MASK;
195 break;
197 if ((remote_state[2] & WHIRLPOOL_FAN_MASK) == WHIRLPOOL_FAN_AUTO) {
198 remote_state[2] |= WHIRLPOOL_FAN_MED;
199 }
200 break;
202 // Auto sends the target as one of four range codes in byte 2 bits 6:4 instead of the temperature nibble.
203 // Taken from the HPA-4911 firmware: 16 to 21 C, 22 C, 24 C, and everything else (23 C and 25 C up) share a code.
204 if (temp_step < 6) {
205 remote_state[2] |= 0x30;
206 } else if (temp_step == 6) {
207 remote_state[2] |= 0x50;
208 } else if (temp_step == 8) {
209 remote_state[2] |= 0x40;
210 } else {
211 remote_state[2] |= 0x20;
212 }
213 break;
214 default:
215 break;
216 }
217#endif
218}
219
221 // Check if the esp isn't currently transmitting.
222 if (millis() - this->last_transmit_time_ < 500) {
223 ESP_LOGV(TAG, "Blocked receive because of current trasmittion");
224 return false;
225 }
226
227 // Validate header
228 if (!data.expect_item(WHIRLPOOL_HEADER_MARK, WHIRLPOOL_HEADER_SPACE)) {
229 ESP_LOGV(TAG, "Header fail");
230 return false;
231 }
232
233 uint8_t remote_state[WHIRLPOOL_STATE_LENGTH] = {0};
234 bool skip_footer = false;
235 // Read all bytes.
236 for (int i = 0; i < WHIRLPOOL_STATE_LENGTH; i++) {
237 if (i == WHIRLPOOL_FIRST_DIVIDER || i == WHIRLPOOL_SECOND_DIVIDER) {
238 if (!data.expect_item(WHIRLPOOL_BIT_MARK, WHIRLPOOL_GAP))
239 return false;
240 }
241 if (i == WHIRLPOOL_SECOND_DIVIDER && !data.is_valid()) {
242 // Remote control only sent 14 bytes, nothing more to read, not even the footer
243 ESP_LOGV(TAG, "Remote control only sent %d bytes", i);
244 skip_footer = true;
245 break;
246 }
247 if (!data.expect_bytes<WHIRLPOOL_BIT_TIMINGS>(remote_state + i, 1))
248 return false;
249 }
250 // Validate footer
251 if (!data.expect_mark(WHIRLPOOL_BIT_MARK) && !skip_footer) {
252 ESP_LOGV(TAG, "Footer fail");
253 return false;
254 }
255
256 // Calculate checksum and compare with signal value.
257 uint8_t checksum13 = xor_bytes(&remote_state[2], 11);
258 uint8_t checksum20 = xor_bytes(&remote_state[14], 6);
259
260 if (checksum13 != remote_state[13] || (!skip_footer && checksum20 != remote_state[20])) {
261 ESP_LOGVV(TAG, "Checksum fail");
262 return false;
263 }
264
265 ESP_LOGV(
266 TAG,
267 "Received: %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X %02X "
268 "%02X %02X %02X",
269 remote_state[0], remote_state[1], remote_state[2], remote_state[3], remote_state[4], remote_state[5],
270 remote_state[6], remote_state[7], remote_state[8], remote_state[9], remote_state[10], remote_state[11],
271 remote_state[12], remote_state[13], remote_state[14], remote_state[15], remote_state[16], remote_state[17],
272 remote_state[18], remote_state[19], remote_state[20]);
273
274 // verify header remote code
275 if (remote_state[0] != 0x83 || remote_state[1] != 0x06)
276 return false;
277
278 // powr on/off button
279 ESP_LOGV(TAG, "Power: %02X", (remote_state[2] & WHIRLPOOL_POWER));
280
281 if ((remote_state[2] & WHIRLPOOL_POWER) == WHIRLPOOL_POWER) {
282 auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
283
284 if (powered_on) {
286 this->powered_on_assumed = false;
287 } else {
288 this->powered_on_assumed = true;
289 }
290 }
291
292 // Set received mode
293 if (powered_on_assumed) {
294 auto mode = remote_state[3] & 0x7;
295 ESP_LOGV(TAG, "Mode: %02X", mode);
296 switch (mode) {
297 case WHIRLPOOL_HEAT:
299 break;
300 case WHIRLPOOL_COOL:
302 break;
303 case WHIRLPOOL_DRY:
305 break;
306 case WHIRLPOOL_FAN:
308 break;
309 case WHIRLPOOL_AUTO:
311 break;
312 default:
313 break;
314 }
315 }
316
317 // Set received temp; Hisense ARG sends fixed values in auto and fan-only
318 const bool hisense_arg = this->is_hisense_arg_();
319 if (!hisense_arg || !this->fixed_temp_mode_()) {
320 int temp = remote_state[3] & 0xF0;
321 ESP_LOGVV(TAG, "Temperature Raw: %02X", temp);
322 temp = (uint8_t) temp >> 4;
323 temp += static_cast<int>(this->temperature_min_());
324 ESP_LOGVV(TAG, "Temperature Climate: %u", temp);
325 this->target_temperature = temp;
326 }
327
328 // Set received fan speed
329 auto fan = remote_state[2] & WHIRLPOOL_FAN_MASK;
330 ESP_LOGVV(TAG, "Fan: %02X", fan);
331 switch (fan) {
334 break;
337 break;
340 break;
342 default:
344 break;
345 }
346
347 // Set received swing status
348 if (hisense_arg) {
349 this->swing_mode =
351 } else if ((remote_state[2] & WHIRLPOOL_SWING_MASK) == WHIRLPOOL_SWING_MASK &&
352 remote_state[8] == WHIRLPOOL_SWING_BYTE8) {
353 ESP_LOGVV(TAG, "Swing toggle pressed ");
356 } else {
358 }
359 }
360
361 this->publish_state();
362 return true;
363}
364
365} // namespace esphome::whirlpool
ClimateMode mode
The active mode of the climate device.
Definition climate.h:317
optional< ClimateFanMode > fan_mode
The active fan mode of the climate device.
Definition climate.h:311
float target_temperature
The target temperature of the climate device.
Definition climate.h:298
ClimateSwingMode swing_mode
The active swing mode of the climate device.
Definition climate.h:323
void publish_state()
Publish the state of the climate device, to be called from integrations.
Definition climate.cpp:432
bool is_hisense_arg_() const
False when the model is compiled out, so the compiler drops the Hisense ARG branches.
Definition whirlpool.h:51
uint32_t last_transmit_time_
Set the time of the last transmission.
Definition whirlpool.h:63
bool fixed_temp_mode_() const
Modes in which the Hisense ARG model sends a fixed temperature.
void transmit_state() override
Transmit via IR the state of this climate controller.
Definition whirlpool.cpp:54
void encode_hisense_arg_(uint8_t *remote_state, bool power_toggle, uint8_t temp_step) const
Add the Hisense ARG bytes on top of the common whirlpool frame.
bool on_receive(remote_base::RemoteReceiveData data) override
Handle received IR Buffer.
@ CLIMATE_SWING_OFF
The swing mode is set to Off.
@ CLIMATE_SWING_VERTICAL
The fan mode is set to Vertical.
@ CLIMATE_MODE_DRY
The climate device is set to dry/humidity mode.
@ CLIMATE_MODE_FAN_ONLY
The climate device only has the fan enabled, no heating or cooling is taking place.
@ CLIMATE_MODE_HEAT
The climate device is set to heat to reach the target temperature.
@ CLIMATE_MODE_COOL
The climate device is set to cool to reach the target temperature.
@ CLIMATE_MODE_HEAT_COOL
The climate device is set to heat/cool to reach the target temperature.
@ CLIMATE_MODE_OFF
The climate device is off.
@ CLIMATE_FAN_MEDIUM
The fan mode is set to Medium.
@ CLIMATE_FAN_ON
The fan mode is set to On.
@ CLIMATE_FAN_AUTO
The fan mode is set to Auto.
@ CLIMATE_FAN_LOW
The fan mode is set to Low.
@ CLIMATE_FAN_HIGH
The fan mode is set to High.
const uint8_t WHIRLPOOL_COOL
Definition whirlpool.cpp:24
ESPHOME_LOG_TAG(TAG, "whirlpool.climate")
const uint16_t WHIRLPOOL_BIT_MARK
Definition whirlpool.cpp:11
const uint8_t WHIRLPOOL_FAN_MASK
Definition whirlpool.cpp:33
const float WHIRLPOOL_DG11J1_3A_TEMP_MAX
Definition whirlpool.h:15
const uint16_t WHIRLPOOL_HEADER_SPACE
Definition whirlpool.cpp:10
const uint8_t WHIRLPOOL_FAN_MED
Definition whirlpool.cpp:30
const uint8_t WHIRLPOOL_FAN_AUTO
Definition whirlpool.cpp:28
const uint8_t WHIRLPOOL_SWING_MASK
Definition whirlpool.cpp:35
const uint8_t WHIRLPOOL_STATE_LENGTH
Definition whirlpool.cpp:18
const uint32_t WHIRLPOOL_CARRIER_FREQUENCY
Definition whirlpool.cpp:16
const uint8_t WHIRLPOOL_FAN_LOW
Definition whirlpool.cpp:31
const uint8_t WHIRLPOOL_SWING_BYTE8
Definition whirlpool.cpp:36
const uint8_t WHIRLPOOL_DRY
Definition whirlpool.cpp:23
const uint16_t WHIRLPOOL_HEADER_MARK
Definition whirlpool.cpp:9
const float WHIRLPOOL_DG11J1_3A_TEMP_MIN
Definition whirlpool.h:16
const uint8_t WHIRLPOOL_HEAT
Definition whirlpool.cpp:22
const uint8_t WHIRLPOOL_AUTO
Definition whirlpool.cpp:26
const uint8_t WHIRLPOOL_POWER
Definition whirlpool.cpp:38
const uint8_t WHIRLPOOL_FAN
Definition whirlpool.cpp:25
const uint8_t WHIRLPOOL_FAN_HIGH
Definition whirlpool.cpp:29
const uint32_t WHIRLPOOL_GAP
Definition whirlpool.cpp:14
constexpr uint8_t xor_bytes(const uint8_t *data, size_t len, uint8_t init=0)
XOR the bytes of data with size len, starting from init.
Definition helpers.h:904
uint32_t IRAM_ATTR HOT millis()
Definition hal.cpp:25
static void uint32_t
A bit is a mark (one_mark or zero_mark, both bit_mark unless set) and a space (one_space or zero_spac...
Definition remote_base.h:29