ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
toshiba.cpp
Go to the documentation of this file.
1#include "toshiba.h"
3#include "esphome/core/hal.h"
5
6namespace esphome::toshiba {
7
8struct RacPt1411hwruFanSpeed {
9 uint8_t code1;
10 uint8_t code2;
11};
12
13ESPHOME_LOG_TAG(TAG, "toshiba.climate");
14// Timings for IR bits/data
15const uint16_t TOSHIBA_PACKET_SPACE = 10500;
16const uint16_t TOSHIBA_ZERO_SPACE = 540;
17const uint16_t TOSHIBA_ONE_SPACE = 1620;
18const uint16_t TOSHIBA_CARRIER_FREQUENCY = 38000;
19// Generic Toshiba commands/flags
20const uint8_t TOSHIBA_COMMAND_MOTION = 0x02;
21
22const uint8_t TOSHIBA_MODE_AUTO = 0x00;
23const uint8_t TOSHIBA_MODE_COOL = 0x01;
24const uint8_t TOSHIBA_MODE_DRY = 0x02;
25const uint8_t TOSHIBA_MODE_HEAT = 0x03;
26const uint8_t TOSHIBA_MODE_FAN_ONLY = 0x04;
27const uint8_t TOSHIBA_MODE_OFF = 0x07;
28
29// RAC-PT1411HWRU temperature code flag bits
30const uint8_t RAC_PT1411HWRU_FLAG_FAH = 0x01;
31const uint8_t RAC_PT1411HWRU_FLAG_FRAC = 0x20;
32const uint8_t RAC_PT1411HWRU_FLAG_NEG = 0x10;
33// RAC-PT1411HWRU temperature short code flags mask
34const uint8_t RAC_PT1411HWRU_FLAG_MASK = 0x0F;
35// RAC-PT1411HWRU Headers, Footers and such
36const uint8_t RAC_PT1411HWRU_MESSAGE_HEADER0 = 0xB2;
37const uint8_t RAC_PT1411HWRU_MESSAGE_HEADER1 = 0xD5;
39// RAC-PT1411HWRU "Comfort Sense" feature bits
40const uint8_t RAC_PT1411HWRU_CS_ENABLED = 0x40;
41const uint8_t RAC_PT1411HWRU_CS_DATA = 0x80;
42const uint8_t RAC_PT1411HWRU_CS_HEADER = 0xBA;
43const uint8_t RAC_PT1411HWRU_CS_FOOTER_AUTO = 0x7A;
44const uint8_t RAC_PT1411HWRU_CS_FOOTER_COOL = 0x72;
45const uint8_t RAC_PT1411HWRU_CS_FOOTER_HEAT = 0x7E;
46// RAC-PT1411HWRU Swing
47const uint8_t RAC_PT1411HWRU_SWING_HEADER = 0xB9;
48static constexpr uint8_t RAC_PT1411HWRU_SWING_VERTICAL[] PROGMEM = {0xB9, 0x46, 0xF5, 0x0A, 0x04, 0xFB};
49static constexpr uint8_t RAC_PT1411HWRU_SWING_OFF[] PROGMEM = {0xB9, 0x46, 0xF5, 0x0A, 0x05, 0xFA};
50// RAC-PT1411HWRU Fan speeds
51const uint8_t RAC_PT1411HWRU_FAN_OFF = 0x7B;
52constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_AUTO{0xBF, 0x66};
53constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_LOW{0x9F, 0x28};
54constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_MED{0x5F, 0x3C};
55constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_HIGH{0x3F, 0x64};
56// RAC-PT1411HWRU Fan speed for Auto and Dry climate modes
57const RacPt1411hwruFanSpeed RAC_PT1411HWRU_NO_FAN{0x1F, 0x65};
58// RAC-PT1411HWRU Modes
59const uint8_t RAC_PT1411HWRU_MODE_AUTO = 0x08;
60const uint8_t RAC_PT1411HWRU_MODE_COOL = 0x00;
61const uint8_t RAC_PT1411HWRU_MODE_DRY = 0x04;
62const uint8_t RAC_PT1411HWRU_MODE_FAN = 0x04;
63const uint8_t RAC_PT1411HWRU_MODE_HEAT = 0x0C;
64const uint8_t RAC_PT1411HWRU_MODE_OFF = 0x00;
65// RAC-PT1411HWRU Fan-only "temperature"/system off
67// RAC-PT1411HWRU temperature codes are not sequential; they instead follow a modified Gray code.
68// Hence these look-up tables. In addition, the upper nibble is used here for additional
69// "negative" and "fractional value" flags as required for some temperatures.
70// RAC-PT1411HWRU °C Temperatures (short codes)
71static constexpr uint8_t RAC_PT1411HWRU_TEMPERATURE_C[] PROGMEM = {0x10, 0x00, 0x01, 0x03, 0x02, 0x06, 0x07, 0x05,
72 0x04, 0x0C, 0x0D, 0x09, 0x08, 0x0A, 0x0B};
73// RAC-PT1411HWRU °F Temperatures (short codes)
74static constexpr uint8_t RAC_PT1411HWRU_TEMPERATURE_F[] PROGMEM = {
75 0x10, 0x30, 0x00, 0x20, 0x01, 0x21, 0x03, 0x23, 0x02, 0x22, 0x06, 0x26, 0x07, 0x05,
76 0x25, 0x04, 0x24, 0x0C, 0x2C, 0x0D, 0x2D, 0x09, 0x08, 0x28, 0x0A, 0x2A, 0x0B};
77
78// RAS-2819T protocol constants
79const uint16_t RAS_2819T_HEADER1 = 0xC23D;
80const uint8_t RAS_2819T_HEADER2 = 0xD5;
81const uint8_t RAS_2819T_MESSAGE_LENGTH = 6;
82
83// RAS-2819T fan speed codes for rc_code_1 (bytes 2-3)
84const uint16_t RAS_2819T_FAN_AUTO = 0xBF40;
85const uint16_t RAS_2819T_FAN_QUIET = 0xFF00;
86const uint16_t RAS_2819T_FAN_LOW = 0x9F60;
87const uint16_t RAS_2819T_FAN_MEDIUM = 0x5FA0;
88const uint16_t RAS_2819T_FAN_HIGH = 0x3FC0;
89
90// RAS-2819T fan speed codes for rc_code_2 (byte 1)
91const uint8_t RAS_2819T_FAN2_AUTO = 0x66;
92const uint8_t RAS_2819T_FAN2_QUIET = 0x01;
93const uint8_t RAS_2819T_FAN2_LOW = 0x28;
94const uint8_t RAS_2819T_FAN2_MEDIUM = 0x3C;
95const uint8_t RAS_2819T_FAN2_HIGH = 0x50;
96
97// RAS-2819T second packet suffix bytes for rc_code_2 (bytes 3-5)
98// These are fixed patterns, not actual checksums
99struct Ras2819tPacketSuffix {
100 uint8_t byte3;
101 uint8_t byte4;
102 uint8_t byte5;
103};
104const Ras2819tPacketSuffix RAS_2819T_SUFFIX_AUTO{0x00, 0x02, 0x3D};
105const Ras2819tPacketSuffix RAS_2819T_SUFFIX_QUIET{0x00, 0x02, 0xD8};
106const Ras2819tPacketSuffix RAS_2819T_SUFFIX_LOW{0x00, 0x02, 0xFF};
107const Ras2819tPacketSuffix RAS_2819T_SUFFIX_MEDIUM{0x00, 0x02, 0x13};
108const Ras2819tPacketSuffix RAS_2819T_SUFFIX_HIGH{0x00, 0x02, 0x27};
109
110// RAS-2819T swing toggle command
111const uint64_t RAS_2819T_SWING_TOGGLE = 0xC23D6B94E01F;
112
113// RAS-2819T single-packet commands
114const uint64_t RAS_2819T_POWER_OFF_COMMAND = 0xC23D7B84E01F;
115
116// RAS-2819T known valid command patterns for validation
117const std::array<uint64_t, 2> RAS_2819T_VALID_SINGLE_COMMANDS = {
118 RAS_2819T_POWER_OFF_COMMAND, // Power off
119 RAS_2819T_SWING_TOGGLE, // Swing toggle
120};
121
122const uint16_t RAS_2819T_VALID_HEADER1 = 0xC23D;
123const uint8_t RAS_2819T_VALID_HEADER2 = 0xD5;
124
125const uint8_t RAS_2819T_DRY_BYTE2 = 0x1F;
126const uint8_t RAS_2819T_DRY_BYTE3 = 0xE0;
127const uint8_t RAS_2819T_DRY_TEMP_OFFSET = 0x24;
128
129const uint8_t RAS_2819T_AUTO_BYTE2 = 0x1F;
130const uint8_t RAS_2819T_AUTO_BYTE3 = 0xE0;
131const uint8_t RAS_2819T_AUTO_TEMP_OFFSET = 0x08;
132
133const uint8_t RAS_2819T_FAN_ONLY_TEMP = 0xE4;
134const uint8_t RAS_2819T_FAN_ONLY_TEMP_INV = 0x1B;
135
136const uint8_t RAS_2819T_HEAT_TEMP_OFFSET = 0x0C;
137
138// RAS-2819T second packet fixed values
139const uint8_t RAS_2819T_AUTO_DRY_FAN_BYTE = 0x65;
140const uint8_t RAS_2819T_AUTO_DRY_SUFFIX = 0x3A;
141const uint8_t RAS_2819T_HEAT_SUFFIX = 0x3B;
142
143// Bit timings differ per protocol family; the frame layout is shared
144struct ToshibaTimings {
145 uint16_t header_mark;
146 uint16_t header_space;
147 uint16_t bit_mark;
148 uint16_t zero_space;
149 uint16_t one_space;
150 uint16_t gap_space;
151 bool gap_after_last; // Seiya ends on a bare mark and only puts the gap between repeats
152};
153// Kept in flash on ESP8266, where plain const objects referenced through a function would land in RAM
154static const ToshibaTimings TOSHIBA_TIMINGS PROGMEM = {TOSHIBA_HEADER_MARK,
155 TOSHIBA_HEADER_SPACE,
156 TOSHIBA_BIT_MARK,
159 TOSHIBA_GAP_SPACE,
160 true};
161static const ToshibaTimings SEIYA_TIMINGS PROGMEM = {4630, 4450, 625, 490, 1570, 5830, false};
162// The RAS-B13 remote family does not accept the generic timings; these are IRremoteESP8266's TOSHIBA_AC values
163static const ToshibaTimings RAS_B10N3KV2_TIMINGS PROGMEM = {4400, 4300, 580, 490, 1600, 7400, true};
164
165static ToshibaTimings timings_for(Model model) {
166 const ToshibaTimings *source = &TOSHIBA_TIMINGS;
167 switch (model) {
168 case MODEL_SEIYA:
169 source = &SEIYA_TIMINGS;
170 break;
172 source = &RAS_B10N3KV2_TIMINGS;
173 break;
174 default:
175 break;
176 }
177 ToshibaTimings timings;
178 progmem_memcpy(&timings, source, sizeof(timings));
179 return timings;
180}
181
182// Seiya is the generic frame plus two bytes: a feature marker and a one-shot swing command
183static constexpr uint8_t SEIYA_MESSAGE_LENGTH = 11;
184static constexpr uint8_t SEIYA_FEATURE_MARKER = 0x21;
185static constexpr uint8_t SEIYA_SWING_BOTH = 0x01;
186static constexpr uint8_t SEIYA_SWING_OMNI = 0x02; // no ESPHome equivalent; treated as BOTH
187static constexpr uint8_t SEIYA_SWING_VERTICAL = 0x08;
188static constexpr uint8_t SEIYA_SWING_HORIZONTAL = 0x09;
189static constexpr uint8_t SEIYA_SWING_NEUTRAL = 0x17; // no swing change in this frame
190
191// RAS-2819T temperature codes for 18-30°C
192static constexpr uint8_t RAS_2819T_TEMP_CODES[] PROGMEM = {
193 0x10, // 18°C
194 0x30, // 19°C
195 0x20, // 20°C
196 0x60, // 21°C
197 0x70, // 22°C
198 0x50, // 23°C
199 0x40, // 24°C
200 0xC0, // 25°C
201 0xD0, // 26°C
202 0x90, // 27°C
203 0x80, // 28°C
204 0xA0, // 29°C
205 0xB0 // 30°C
206};
207
208// Helper functions for RAS-2819T protocol
209//
210// ===== RAS-2819T PROTOCOL DOCUMENTATION =====
211//
212// The RAS-2819T uses a two-packet IR protocol with some exceptions for simple commands.
213//
214// PACKET STRUCTURE:
215// All packets are 6 bytes (48 bits) transmitted with standard Toshiba timing.
216//
217// TWO-PACKET COMMANDS (Mode/Temperature/Fan changes):
218//
219// First Packet (rc_code_1): [C2 3D] [FAN_HI FAN_LO] [TEMP] [~TEMP]
220// Byte 0-1: Header (always 0xC23D)
221// Byte 2-3: Fan speed encoding (varies by mode, see fan tables below)
222// Byte 4: Temperature + mode encoding
223// Byte 5: Bitwise complement of temperature byte
224//
225// Second Packet (rc_code_2): [D5] [FAN2] [00] [SUF1] [SUF2] [SUF3]
226// Byte 0: Header (always 0xD5)
227// Byte 1: Fan speed secondary encoding
228// Byte 2: Always 0x00
229// Byte 3-5: Fixed suffix pattern (depends on fan speed and mode)
230//
231// TEMPERATURE ENCODING:
232// Base temp codes: 18°C=0x10, 19°C=0x30, 20°C=0x20, 21°C=0x60, 22°C=0x70,
233// 23°C=0x50, 24°C=0x40, 25°C=0xC0, 26°C=0xD0, 27°C=0x90,
234// 28°C=0x80, 29°C=0xA0, 30°C=0xB0
235// Mode offsets added to base temp:
236// COOL: No offset
237// HEAT: +0x0C (e.g., 24°C heat = 0x40 | 0x0C = 0x4C)
238// AUTO: +0x08 (e.g., 24°C auto = 0x40 | 0x08 = 0x48)
239// DRY: +0x24 (e.g., 24°C dry = 0x40 | 0x24 = 0x64)
240//
241// FAN SPEED ENCODING (First packet bytes 2-3):
242// AUTO: 0xBF40, QUIET: 0xFF00, LOW: 0x9F60, MEDIUM: 0x5FA0, HIGH: 0x3FC0
243// Special cases: AUTO/DRY modes use 0x1FE0 instead
244//
245// SINGLE-PACKET COMMANDS:
246// Power Off: 0xC23D7B84E01F (6 bytes, no second packet)
247// Swing Toggle: 0xC23D6B94E01F (6 bytes, no second packet)
248//
249// MODE DETECTION (from first packet):
250// - Check bytes 2-3: if 0x7B84 → OFF mode
251// - Check bytes 2-3: if 0x1FE0 → AUTO/DRY/low-temp-COOL (distinguish by temp code)
252// - Otherwise: COOL/HEAT/FAN_ONLY (distinguish by temp code and byte 5)
253
257static uint16_t get_ras_2819t_fan_code(climate::ClimateFanMode fan_mode) {
258 switch (fan_mode) {
260 return RAS_2819T_FAN_QUIET;
262 return RAS_2819T_FAN_LOW;
266 return RAS_2819T_FAN_HIGH;
268 default:
269 return RAS_2819T_FAN_AUTO;
270 }
271}
272
276struct Ras2819tSecondPacketCodes {
277 uint8_t fan_byte;
278 Ras2819tPacketSuffix suffix;
279};
280
281static Ras2819tSecondPacketCodes get_ras_2819t_second_packet_codes(climate::ClimateFanMode fan_mode) {
282 switch (fan_mode) {
292 default:
294 }
295}
296
300static uint8_t get_ras_2819t_temp_code(float temperature) {
301 int temp_index = static_cast<int>(temperature) - 18;
302 if (temp_index < 0 || static_cast<size_t>(temp_index) >= sizeof(RAS_2819T_TEMP_CODES)) {
303 ESP_LOGW(TAG, "Temperature %.1f°C out of range [18-30°C], defaulting to 24°C", temperature);
304 return 0x40; // Default to 24°C
305 }
306
307 return progmem_read_byte(&RAS_2819T_TEMP_CODES[temp_index]);
308}
309
313static float decode_ras_2819t_temperature(uint8_t temp_code) {
314 uint8_t base_temp_code = temp_code & 0xF0;
315
316 // Find the code in the temperature array
317 for (size_t temp_index = 0; temp_index < sizeof(RAS_2819T_TEMP_CODES); temp_index++) {
318 if (progmem_read_byte(&RAS_2819T_TEMP_CODES[temp_index]) == base_temp_code) {
319 return static_cast<float>(temp_index + 18); // 18°C is the minimum
320 }
321 }
322
323 ESP_LOGW(TAG, "Unknown temp code: 0x%02X, defaulting to 24°C", base_temp_code);
324 return 24.0f; // Default to 24°C
325}
326
330static climate::ClimateFanMode decode_ras_2819t_fan_mode(uint16_t fan_code) {
331 switch (fan_code) {
341 default:
343 }
344}
345
349static bool is_valid_ras_2819t_command(uint64_t rc_code_1, uint64_t rc_code_2 = 0) {
350 // Check header of first packet
351 uint16_t header1 = (rc_code_1 >> 32) & 0xFFFF;
352 if (header1 != RAS_2819T_VALID_HEADER1) {
353 return false;
354 }
355
356 // Single packet commands
357 if (rc_code_2 == 0) {
358 for (uint64_t valid_cmd : RAS_2819T_VALID_SINGLE_COMMANDS) {
359 if (rc_code_1 == valid_cmd) {
360 return true;
361 }
362 }
363 // Additional validation for unknown single packets
364 return false;
365 }
366
367 // Two-packet commands - validate second packet header
368 uint8_t header2 = (rc_code_2 >> 40) & 0xFF;
369 if (header2 != RAS_2819T_VALID_HEADER2) {
370 return false;
371 }
372
373 // Validate temperature complement in first packet (byte 4 should be ~byte 5)
374 uint8_t temp_byte = (rc_code_1 >> 8) & 0xFF;
375 uint8_t temp_complement = rc_code_1 & 0xFF;
376 if (temp_byte != static_cast<uint8_t>(~temp_complement)) {
377 return false;
378 }
379
380 // Validate fan speed combinations make sense
381 uint16_t fan_code = (rc_code_1 >> 16) & 0xFFFF;
382 uint8_t fan2_byte = (rc_code_2 >> 32) & 0xFF;
383
384 // Check if fan codes are from known valid patterns
385 bool valid_fan_combo = false;
386 if (fan_code == RAS_2819T_FAN_AUTO && fan2_byte == RAS_2819T_FAN2_AUTO)
387 valid_fan_combo = true;
388 if (fan_code == RAS_2819T_FAN_QUIET && fan2_byte == RAS_2819T_FAN2_QUIET)
389 valid_fan_combo = true;
390 if (fan_code == RAS_2819T_FAN_LOW && fan2_byte == RAS_2819T_FAN2_LOW)
391 valid_fan_combo = true;
392 if (fan_code == RAS_2819T_FAN_MEDIUM && fan2_byte == RAS_2819T_FAN2_MEDIUM)
393 valid_fan_combo = true;
394 if (fan_code == RAS_2819T_FAN_HIGH && fan2_byte == RAS_2819T_FAN2_HIGH)
395 valid_fan_combo = true;
396 if (fan_code == 0x1FE0 && fan2_byte == RAS_2819T_AUTO_DRY_FAN_BYTE)
397 valid_fan_combo = true; // AUTO/DRY
398
399 return valid_fan_combo;
400}
401
404 this->maximum_temperature_ = this->temperature_max_();
405 this->swing_modes_ = this->toshiba_swing_modes_();
406 if (this->sensor_) {
407 this->sensor_->add_on_state_callback([this](float state) {
410 // current temperature changed, publish state
411 this->publish_state();
412 });
413 this->current_temperature = this->sensor_->state;
414 } else {
415 this->current_temperature = NAN;
416 }
417 // restore set points
418 auto restore = this->restore_state_();
419 if (restore.has_value()) {
420 this->sanitize_restore_state(*restore);
421 // apply() publishes synchronously, so on_state automations need the swing cache ready. apply() skips an
422 // unsupported swing mode, leaving OFF, which also stands for "not yet commanded" on models without a stop.
423 if (this->swing_modes_.count(restore->swing_mode))
424 this->last_swing_mode_ = restore->swing_mode;
425 restore->apply(this);
426 } else {
427 // restore from defaults
429 // initialize target temperature to some value so that it's not NAN
430 this->target_temperature =
431 roundf(clamp<float>(this->current_temperature, this->minimum_temperature_, this->maximum_temperature_));
432 this->fan_mode = climate::CLIMATE_FAN_AUTO;
434 }
435 // Ensure mode is valid - ESPHome should only use standard climate modes
439 ESP_LOGW(TAG, "Invalid mode detected during setup, resetting to OFF");
441 }
442
443 // Ensure fan mode is valid
444 if (!this->fan_mode.has_value() && !this->has_custom_fan_mode()) {
445 ESP_LOGW(TAG, "Fan mode not set during setup, defaulting to AUTO");
446 this->fan_mode = climate::CLIMATE_FAN_AUTO;
447 }
448
449 // Never send nan to HA
450 if (std::isnan(this->target_temperature))
451 this->target_temperature = 24;
452#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
453 // Log final state for debugging HA errors
454 const char *fan_mode_str = "NONE";
455 char fan_mode_buf[4]; // max 3 digits for fan mode enum + null
456 if (this->fan_mode.has_value()) {
457 buf_append_printf(fan_mode_buf, sizeof(fan_mode_buf), 0, "%d", static_cast<int>(this->fan_mode.value()));
458 fan_mode_str = fan_mode_buf;
459 }
460 ESP_LOGV(TAG, "Setup complete - Mode: %d, Fan: %s, Swing: %d, Temp: %.1f", static_cast<int>(this->mode), fan_mode_str,
461 static_cast<int>(this->swing_mode), this->target_temperature);
462#endif
463}
464
468 } else if (this->model_ == MODEL_RAS_2819T) {
469 this->transmit_ras_2819t_();
470 } else {
471 this->transmit_generic_();
472 }
473}
474
476 const bool seiya = this->model_ == MODEL_SEIYA;
477 uint8_t message[16] = {0};
478 const uint8_t message_length = seiya ? SEIYA_MESSAGE_LENGTH : 9;
479
480 // Command
481 message[4] = TOSHIBA_COMMAND_DEFAULT;
482
483 // Temperature
484 uint8_t temperature = static_cast<uint8_t>(
486 message[5] = (temperature - static_cast<uint8_t>(TOSHIBA_GENERIC_TEMP_C_MIN)) << 4;
487
488 // Mode and fan
489 message[6] = this->encode_mode_fan_();
490
491 // Zero
492 message[7] = 0x00;
493
494 if (seiya) {
495 // Captured OFF frames carry no fan bits
496 if (this->mode == climate::CLIMATE_MODE_OFF) {
497 message[6] = TOSHIBA_MODE_OFF;
498 }
499 message[8] = SEIYA_FEATURE_MARKER;
500 // Swing is a momentary command, sent only when the mode changes, like the remote does
501 message[9] = SEIYA_SWING_NEUTRAL;
502 if (this->swing_mode != this->last_swing_mode_) {
503 message[9] = this->seiya_swing_code_();
504 this->last_swing_mode_ = this->swing_mode;
505 }
506 }
507
508 finalize_frame(message, message_length);
509
510 // Transmit
511 auto transmit = this->transmitter_->transmit();
512 auto *data = transmit.get_data();
513
514 this->encode_(data, message, message_length, 1);
515
516 transmit.perform();
517}
518
520 switch (this->mode) {
522 return TOSHIBA_MODE_OFF;
524 return TOSHIBA_MODE_HEAT;
526 return TOSHIBA_MODE_COOL;
528 return TOSHIBA_MODE_DRY;
532 default:
533 return TOSHIBA_MODE_AUTO;
534 }
535}
536
538 switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
540 return TOSHIBA_FAN_SPEED_QUIET;
542 return TOSHIBA_FAN_SPEED_1;
544 return TOSHIBA_FAN_SPEED_3;
546 return TOSHIBA_FAN_SPEED_5;
548 default:
549 return TOSHIBA_FAN_SPEED_AUTO;
550 }
551}
552
553uint8_t ToshibaClimate::seiya_swing_code_() const {
554 switch (this->swing_mode) {
556 return SEIYA_SWING_VERTICAL;
558 return SEIYA_SWING_HORIZONTAL;
560 return SEIYA_SWING_BOTH;
561 default:
562 return SEIYA_SWING_NEUTRAL;
563 }
564}
565
567 uint8_t code = 0, index = 0, message[RAC_PT1411HWRU_MESSAGE_LENGTH * 2] = {0};
568 float temperature =
571 auto transmit = this->transmitter_->transmit();
572 auto *data = transmit.get_data();
573
574 // Byte 0: Header upper (0xB2)
576 // Byte 1: Header lower (0x4D)
577 message[1] = ~message[0];
578 // Byte 2u: Fan speed
579 // Byte 2l: 1111 (on) or 1011 (off)
580 if (this->mode == climate::CLIMATE_MODE_OFF) {
582 } else if ((this->mode == climate::CLIMATE_MODE_HEAT_COOL) || (this->mode == climate::CLIMATE_MODE_DRY)) {
585 } else {
586 switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
590 break;
591
595 break;
596
600 break;
601
603 default:
606 }
607 }
608 // Byte 3u: ~Fan speed
609 // Byte 3l: 0000 (on) or 0100 (off)
610 message[3] = ~message[2];
611 // Byte 4u: Temp
612 if (this->model_ == MODEL_RAC_PT1411HWRU_F) {
613 temperature = (temperature * 1.8f) + 32;
615 }
616
617 index = static_cast<uint8_t>(roundf(temp_adjd));
618
619 if (this->model_ == MODEL_RAC_PT1411HWRU_F) {
620 code = progmem_read_byte(&RAC_PT1411HWRU_TEMPERATURE_F[index]);
622 } else {
623 code = progmem_read_byte(&RAC_PT1411HWRU_TEMPERATURE_C[index]);
624 }
627 }
628
629 if (code & RAC_PT1411HWRU_FLAG_FRAC) {
631 }
632 if (code & RAC_PT1411HWRU_FLAG_NEG) {
634 }
635 message[4] = (code & RAC_PT1411HWRU_FLAG_MASK) << 4;
636 // Byte 4l: Mode
637 switch (this->mode) {
639 // zerooooo
640 break;
641
644 break;
645
648 break;
649
652 break;
653
656 break;
657
659 default:
661 }
662
663 // Byte 5u: ~Temp
664 // Byte 5l: ~Mode
665 message[5] = ~message[4];
666
667 if (this->mode != climate::CLIMATE_MODE_OFF) {
668 // Byte 6: Header (0xD5)
670 // Byte 7: Fan speed part 2 (done above)
671 // Byte 8: 0x20 for °F frac, else 0 (done above)
672 // Byte 9: 0x10=NEG, 0x01=°F (done above)
673 // Byte 10: 0
674 // Byte 11: Checksum (bytes 6 through 10)
675 for (index = 6; index <= 10; index++) {
676 message[11] += message[index];
677 }
678 }
679
680 // load first block of IR code and repeat it once
681 this->encode_(data, &message[0], RAC_PT1411HWRU_MESSAGE_LENGTH, 1);
682 // load second block of IR code, if present
683 if (message[6] != 0) {
684 this->encode_(data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH, 0);
685 }
686
687 transmit.perform();
688
689 // Swing Mode
690 data->reset();
691 data->space(TOSHIBA_PACKET_SPACE);
692 const uint8_t *swing_frame =
693 this->swing_mode == climate::CLIMATE_SWING_VERTICAL ? RAC_PT1411HWRU_SWING_VERTICAL : RAC_PT1411HWRU_SWING_OFF;
694 uint8_t swing[RAC_PT1411HWRU_MESSAGE_LENGTH];
695 progmem_memcpy(swing, swing_frame, sizeof(swing));
696 this->encode_(data, swing, sizeof(swing), 1);
697
698 data->space(TOSHIBA_PACKET_SPACE);
699 transmit.perform();
700
701 if (this->sensor_) {
702 this->transmit_rac_pt1411hwru_temp_(true, false);
703 }
704}
705
706void ToshibaClimate::transmit_rac_pt1411hwru_temp_(const bool cs_state, const bool cs_send_update) {
707 if ((this->mode == climate::CLIMATE_MODE_HEAT) || (this->mode == climate::CLIMATE_MODE_COOL) ||
710 float temperature = clamp<float>(this->current_temperature, 0.0, TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX + 1);
711 auto transmit = this->transmitter_->transmit();
712 auto *data = transmit.get_data();
713 // "Comfort Sense" feature notes
714 // IR Code: 0xBA45 xxXX yyYY
715 // xx: Temperature in °C
716 // Bit 6: feature state (on/off)
717 // Bit 7: message contains temperature data for feature (bit 6 must also be set)
718 // XX: Bitwise complement of xx
719 // yy: Mode: Auto=0x7A, Cool=0x72, Heat=0x7E
720 // YY: Bitwise complement of yy
721 //
722 // Byte 0: Header upper (0xBA)
724 // Byte 1: Header lower (0x45)
725 message[1] = ~message[0];
726 // Byte 2: Temperature in °C
727 message[2] = static_cast<uint8_t>(roundf(temperature));
728 if (cs_send_update) {
730 } else if (cs_state) {
732 }
733 // Byte 3: Bitwise complement of byte 2
734 message[3] = ~message[2];
735 // Byte 4: Footer upper
736 switch (this->mode) {
739 break;
740
743 break;
744
747
748 default:
749 break;
750 }
751 // Byte 5: Footer lower/bitwise complement of byte 4
752 message[5] = ~message[4];
753
754 // load IR code and repeat it once
756
757 transmit.perform();
758 }
759}
760
762 // Handle swing mode transmission for RAS-2819T
763 // Note: RAS-2819T uses a toggle command, so we need to track state changes
764
765 // Check if ONLY swing mode changed (and no other climate parameters)
766 bool swing_changed = (this->swing_mode != this->last_swing_mode_);
767 bool mode_changed = (this->mode != this->last_mode_);
768 bool fan_changed = (this->fan_mode != this->last_fan_mode_);
769 bool temp_changed = (abs(this->target_temperature - this->last_target_temperature_) > 0.1f);
770
771 bool only_swing_changed = swing_changed && !mode_changed && !fan_changed && !temp_changed;
772
773 if (only_swing_changed) {
774 // Send ONLY swing toggle command (like the physical remote does)
775 auto swing_transmit = this->transmitter_->transmit();
776 auto *swing_data = swing_transmit.get_data();
777
778 // Convert toggle command to bytes for transmission
779 uint8_t swing_message[RAS_2819T_MESSAGE_LENGTH];
780 swing_message[0] = (RAS_2819T_SWING_TOGGLE >> 40) & 0xFF;
781 swing_message[1] = (RAS_2819T_SWING_TOGGLE >> 32) & 0xFF;
782 swing_message[2] = (RAS_2819T_SWING_TOGGLE >> 24) & 0xFF;
783 swing_message[3] = (RAS_2819T_SWING_TOGGLE >> 16) & 0xFF;
784 swing_message[4] = (RAS_2819T_SWING_TOGGLE >> 8) & 0xFF;
785 swing_message[5] = RAS_2819T_SWING_TOGGLE & 0xFF;
786
787 // Use single packet transmission WITH repeat (like regular commands)
788 this->encode_(swing_data, swing_message, RAS_2819T_MESSAGE_LENGTH, 1);
789 swing_transmit.perform();
790
791 // Update all state tracking
792 this->last_swing_mode_ = this->swing_mode;
793 this->last_mode_ = this->mode;
794 this->last_fan_mode_ = this->fan_mode;
795 this->last_target_temperature_ = this->target_temperature;
796
797 // Immediately publish the state change to Home Assistant
798 this->publish_state();
799
800 return; // Exit early - don't send climate command
801 }
802
803 // If we get here, send the regular climate command (temperature/mode/fan)
804 uint8_t message1[RAS_2819T_MESSAGE_LENGTH] = {0};
805 uint8_t message2[RAS_2819T_MESSAGE_LENGTH] = {0};
806 float temperature =
808
809 // Build first packet (RAS_2819T_HEADER1 + 4 bytes)
810 message1[0] = (RAS_2819T_HEADER1 >> 8) & 0xFF;
811 message1[1] = RAS_2819T_HEADER1 & 0xFF;
812
813 // Handle OFF mode
814 if (this->mode == climate::CLIMATE_MODE_OFF) {
815 // Extract bytes from power off command constant
816 message1[2] = (RAS_2819T_POWER_OFF_COMMAND >> 24) & 0xFF;
817 message1[3] = (RAS_2819T_POWER_OFF_COMMAND >> 16) & 0xFF;
818 message1[4] = (RAS_2819T_POWER_OFF_COMMAND >> 8) & 0xFF;
819 message1[5] = RAS_2819T_POWER_OFF_COMMAND & 0xFF;
820 // No second packet for OFF
821 } else {
822 // Get temperature and fan encoding
823 uint8_t temp_code = get_ras_2819t_temp_code(temperature);
824
825 // Get fan speed encoding for rc_code_1
826 climate::ClimateFanMode effective_fan_mode = this->fan_mode.value_or(climate::CLIMATE_FAN_ON);
827
828 // Dry mode only supports AUTO fan speed
829 if (this->mode == climate::CLIMATE_MODE_DRY) {
830 effective_fan_mode = climate::CLIMATE_FAN_AUTO;
831 if (this->fan_mode.value_or(climate::CLIMATE_FAN_ON) != climate::CLIMATE_FAN_AUTO) {
832 ESP_LOGW(TAG, "Dry mode only supports AUTO fan speed, forcing AUTO");
833 }
834 }
835
836 uint16_t fan_code = get_ras_2819t_fan_code(effective_fan_mode);
837
838 // Mode and temperature encoding
839 switch (this->mode) {
841 // All cooling temperatures support fan speed control
842 message1[2] = (fan_code >> 8) & 0xFF;
843 message1[3] = fan_code & 0xFF;
844 message1[4] = temp_code;
845 message1[5] = ~temp_code;
846 break;
847
849 // Heating supports fan speed control
850 message1[2] = (fan_code >> 8) & 0xFF;
851 message1[3] = fan_code & 0xFF;
852 // Heat mode adds offset to temperature code
853 message1[4] = temp_code | RAS_2819T_HEAT_TEMP_OFFSET;
854 message1[5] = ~(temp_code | RAS_2819T_HEAT_TEMP_OFFSET);
855 break;
856
858 // Auto mode uses fixed encoding
859 message1[2] = RAS_2819T_AUTO_BYTE2;
860 message1[3] = RAS_2819T_AUTO_BYTE3;
861 message1[4] = temp_code | RAS_2819T_AUTO_TEMP_OFFSET;
862 message1[5] = ~(temp_code | RAS_2819T_AUTO_TEMP_OFFSET);
863 break;
864
866 // Dry mode uses fixed encoding and forces AUTO fan
867 message1[2] = RAS_2819T_DRY_BYTE2;
868 message1[3] = RAS_2819T_DRY_BYTE3;
869 message1[4] = temp_code | RAS_2819T_DRY_TEMP_OFFSET;
870 message1[5] = ~message1[4];
871 break;
872
874 // Fan only mode supports fan speed control
875 message1[2] = (fan_code >> 8) & 0xFF;
876 message1[3] = fan_code & 0xFF;
877 message1[4] = RAS_2819T_FAN_ONLY_TEMP;
878 message1[5] = RAS_2819T_FAN_ONLY_TEMP_INV;
879 break;
880
881 default:
882 // Default case supports fan speed control
883 message1[2] = (fan_code >> 8) & 0xFF;
884 message1[3] = fan_code & 0xFF;
885 message1[4] = temp_code;
886 message1[5] = ~temp_code;
887 break;
888 }
889
890 // Build second packet (RAS_2819T_HEADER2 + 4 bytes)
891 message2[0] = RAS_2819T_HEADER2;
892
893 // Get fan speed encoding for rc_code_2
894 Ras2819tSecondPacketCodes second_packet_codes = get_ras_2819t_second_packet_codes(effective_fan_mode);
895
896 // Determine header byte 2 and fan encoding based on mode
897 switch (this->mode) {
899 message2[1] = second_packet_codes.fan_byte;
900 message2[2] = 0x00;
901 message2[3] = second_packet_codes.suffix.byte3;
902 message2[4] = second_packet_codes.suffix.byte4;
903 message2[5] = second_packet_codes.suffix.byte5;
904 break;
905
907 message2[1] = second_packet_codes.fan_byte;
908 message2[2] = 0x00;
909 message2[3] = second_packet_codes.suffix.byte3;
910 message2[4] = 0x00;
911 message2[5] = RAS_2819T_HEAT_SUFFIX;
912 break;
913
916 // Auto/Dry modes use fixed values regardless of fan setting
917 message2[1] = RAS_2819T_AUTO_DRY_FAN_BYTE;
918 message2[2] = 0x00;
919 message2[3] = 0x00;
920 message2[4] = 0x00;
921 message2[5] = RAS_2819T_AUTO_DRY_SUFFIX;
922 break;
923
925 message2[1] = second_packet_codes.fan_byte;
926 message2[2] = 0x00;
927 message2[3] = second_packet_codes.suffix.byte3;
928 message2[4] = 0x00;
929 message2[5] = RAS_2819T_HEAT_SUFFIX;
930 break;
931
932 default:
933 message2[1] = second_packet_codes.fan_byte;
934 message2[2] = 0x00;
935 message2[3] = second_packet_codes.suffix.byte3;
936 message2[4] = second_packet_codes.suffix.byte4;
937 message2[5] = second_packet_codes.suffix.byte5;
938 break;
939 }
940 }
941
942 // Log final messages being transmitted
943
944 // Transmit using proper Toshiba protocol timing
945 auto transmit = this->transmitter_->transmit();
946 auto *data = transmit.get_data();
947
948 // Use existing Toshiba encode function for proper timing
949 this->encode_(data, message1, RAS_2819T_MESSAGE_LENGTH, 1);
950
951 if (this->mode != climate::CLIMATE_MODE_OFF) {
952 // Send second packet with gap
953 this->encode_(data, message2, RAS_2819T_MESSAGE_LENGTH, 0);
954 }
955
956 transmit.perform();
957
958 // Update all state tracking after successful transmission
959 this->last_swing_mode_ = this->swing_mode;
960 this->last_mode_ = this->mode;
961 this->last_fan_mode_ = this->fan_mode;
962 this->last_target_temperature_ = this->target_temperature;
963}
964
966 static constexpr uint8_t HEADERS[] = {RAC_PT1411HWRU_MESSAGE_HEADER0, RAC_PT1411HWRU_CS_HEADER,
968
969 for (auto i : HEADERS) {
970 if ((message[0] == i) && (message[1] == static_cast<uint8_t>(~i)))
971 return i;
972 }
975
976 return 0;
977}
978
979bool ToshibaClimate::compare_rac_pt1411hwru_packets_(const uint8_t *message1, const uint8_t *message2) {
980 for (uint8_t i = 0; i < RAC_PT1411HWRU_MESSAGE_LENGTH; i++) {
981 if (message1[i] != message2[i])
982 return false;
983 }
984 return true;
985}
986
988 switch (this->is_valid_rac_pt1411hwru_header_(message)) {
992 if (this->is_valid_rac_pt1411hwru_header_(message) && (message[2] == static_cast<uint8_t>(~message[3])) &&
993 (message[4] == static_cast<uint8_t>(~message[5]))) {
994 return true;
995 }
996 break;
997
1000 return true;
1001 }
1002 break;
1003
1004 default:
1005 return false;
1006 }
1007
1008 return false;
1009}
1010
1012 // Check for power-off command (single packet)
1013 if (toshiba_data.rc_code_2 == 0 && toshiba_data.rc_code_1 == RAS_2819T_POWER_OFF_COMMAND) {
1015 ESP_LOGI(TAG, "Mode: OFF");
1016 this->publish_state();
1017 return true;
1018 }
1019
1020 // Check for swing toggle command (single packet)
1021 if (toshiba_data.rc_code_2 == 0 && toshiba_data.rc_code_1 == RAS_2819T_SWING_TOGGLE) {
1022 // Toggle swing mode
1025 ESP_LOGI(TAG, "Swing: OFF");
1026 } else {
1028 ESP_LOGI(TAG, "Swing: VERTICAL");
1029 }
1030 this->publish_state();
1031 return true;
1032 }
1033
1034 // Handle regular two-packet commands (mode/temperature/fan changes)
1035 if (toshiba_data.rc_code_2 != 0) {
1036 // Convert to byte array for easier processing
1037 uint8_t message1[6], message2[6];
1038 for (uint8_t i = 0; i < 6; i++) {
1039 message1[i] = (toshiba_data.rc_code_1 >> (40 - i * 8)) & 0xFF;
1040 message2[i] = (toshiba_data.rc_code_2 >> (40 - i * 8)) & 0xFF;
1041 }
1042
1043 // Decode the protocol using message1 (rc_code_1)
1044 uint8_t temp_code = message1[4];
1045
1046 // Decode mode - check bytes 2-3 pattern and temperature code
1047 if ((message1[2] == 0x7B) && (message1[3] == 0x84)) {
1048 // OFF mode has specific pattern
1050 ESP_LOGI(TAG, "Mode: OFF");
1051 } else if ((message1[2] == 0x1F) && (message1[3] == 0xE0)) {
1052 // 0x1FE0 pattern is used for AUTO, DRY, and low-temp COOL
1053 if ((temp_code & 0x0F) == 0x08) {
1055 ESP_LOGI(TAG, "Mode: AUTO");
1056 } else if ((temp_code & 0x0F) == 0x04) {
1058 ESP_LOGI(TAG, "Mode: DRY");
1059 } else {
1061 ESP_LOGI(TAG, "Mode: COOL (low temp)");
1062 }
1063 } else {
1064 // Variable fan speed patterns - decode by temperature code
1065 if ((temp_code & 0x0F) == 0x0C) {
1067 ESP_LOGI(TAG, "Mode: HEAT");
1068 } else if (message1[5] == 0x1B) {
1070 ESP_LOGI(TAG, "Mode: FAN_ONLY");
1071 } else {
1073 ESP_LOGI(TAG, "Mode: COOL");
1074 }
1075 }
1076
1077 // Decode fan speed from rc_code_1
1078 uint16_t fan_code = (message1[2] << 8) | message1[3];
1079 this->fan_mode = decode_ras_2819t_fan_mode(fan_code);
1080
1081 // Decode temperature
1083 this->target_temperature = decode_ras_2819t_temperature(temp_code);
1084 }
1085
1086 this->publish_state();
1087 return true;
1088 } else {
1089 ESP_LOGD(TAG, "Unknown single-packet RAS-2819T command: 0x%" PRIX64, toshiba_data.rc_code_1);
1090 return false;
1091 }
1092}
1093
1095 // Try modern ToshibaAcProtocol decoder first (handles RAS-2819T and potentially others)
1096 if (this->model_ != MODEL_SEIYA) {
1097 remote_base::ToshibaAcProtocol toshiba_protocol;
1098 auto decode_result = toshiba_protocol.decode(data);
1099
1100 if (decode_result.has_value()) {
1101 auto toshiba_data = decode_result.value();
1102 // Validate and process RAS-2819T commands
1103 if (is_valid_ras_2819t_command(toshiba_data.rc_code_1, toshiba_data.rc_code_2)) {
1104 return this->process_ras_2819t_command_(toshiba_data);
1105 }
1106 }
1107 }
1108
1109 // Fall back to generic processing for older protocols
1110 uint8_t message[18] = {0};
1111 uint8_t message_length = TOSHIBA_HEADER_LENGTH, temperature_code = 0;
1112
1113 // Validate header
1114 const ToshibaTimings timings = timings_for(this->model_);
1115 if (!data.expect_item(timings.header_mark, timings.header_space)) {
1116 return false;
1117 }
1118 // Read incoming bits into buffer
1119 if (!this->decode_(&data, message, message_length)) {
1120 return false;
1121 }
1122 // Determine incoming message protocol version and/or length
1123 if (this->is_valid_rac_pt1411hwru_header_(message)) {
1124 // We already received four bytes
1125 message_length = RAC_PT1411HWRU_MESSAGE_LENGTH - 4;
1126 } else if (!frame_header_valid(message)) {
1127 return false;
1128 } else {
1129 // First checksum was valid so continue receiving the remaining bits
1130 // The shortest frame is 9 bytes (length byte 3); longer ones must still fit the buffer
1131 if (message[2] < 3 || message[2] > sizeof(message) - TOSHIBA_HEADER_LENGTH - 2) {
1132 return false;
1133 }
1134 message_length = message[2] + 2;
1135 }
1136 // Decode the remaining bytes
1137 if (!this->decode_(&data, &message[4], message_length)) {
1138 return false;
1139 }
1140 // If this is a RAC-PT1411HWRU message, we expect the first packet a second time and also possibly a third packet
1141 if (this->is_valid_rac_pt1411hwru_header_(message)) {
1142 // There is always a space between packets
1143 if (!data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) {
1144 return false;
1145 }
1146 // Validate header 2
1147 if (!data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE)) {
1148 return false;
1149 }
1150 if (!this->decode_(&data, &message[6], RAC_PT1411HWRU_MESSAGE_LENGTH)) {
1151 return false;
1152 }
1153 // If this is a RAC-PT1411HWRU message, there may also be a third packet.
1154 // We do not fail the receive if we don't get this; it isn't always present
1155 if (data.expect_item(TOSHIBA_BIT_MARK, TOSHIBA_GAP_SPACE)) {
1156 // Validate header 3
1157 data.expect_item(TOSHIBA_HEADER_MARK, TOSHIBA_HEADER_SPACE);
1158 if (this->decode_(&data, &message[12], RAC_PT1411HWRU_MESSAGE_LENGTH)) {
1159 if (!this->is_valid_rac_pt1411hwru_message_(&message[12])) {
1160 // If a third packet was received but the checksum is not valid, fail
1161 return false;
1162 }
1163 }
1164 }
1165 if (!this->compare_rac_pt1411hwru_packets_(&message[0], &message[6])) {
1166 // If the first two packets don't match each other, fail
1167 return false;
1168 }
1169 if (!this->is_valid_rac_pt1411hwru_message_(&message[0])) {
1170 // If the first packet isn't valid, fail
1171 return false;
1172 }
1173 }
1174
1175 // Header has been verified, now determine protocol version and set the climate component properties
1176 switch (this->is_valid_rac_pt1411hwru_header_(message)) {
1177 // Power, temperature, mode, fan speed
1179 // Get the mode
1180 switch (message[4] & 0x0F) {
1183 break;
1184
1185 // case RAC_PT1411HWRU_MODE_OFF:
1187 if (((message[4] >> 4) == RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY) && (message[2] == RAC_PT1411HWRU_FAN_OFF)) {
1189 } else {
1191 }
1192 break;
1193
1194 // case RAC_PT1411HWRU_MODE_DRY:
1196 if ((message[4] >> 4) == RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY) {
1198 } else {
1200 }
1201 break;
1202
1205 break;
1206
1207 default:
1209 break;
1210 }
1211 // Get the fan speed/mode
1212 switch (message[2]) {
1213 case RAC_PT1411HWRU_FAN_LOW.code1:
1214 this->fan_mode = climate::CLIMATE_FAN_LOW;
1215 break;
1216
1217 case RAC_PT1411HWRU_FAN_MED.code1:
1218 this->fan_mode = climate::CLIMATE_FAN_MEDIUM;
1219 break;
1220
1221 case RAC_PT1411HWRU_FAN_HIGH.code1:
1222 this->fan_mode = climate::CLIMATE_FAN_HIGH;
1223 break;
1224
1225 case RAC_PT1411HWRU_FAN_AUTO.code1:
1226 default:
1227 this->fan_mode = climate::CLIMATE_FAN_AUTO;
1228 break;
1229 }
1230 // Get the target temperature
1231 if (this->is_valid_rac_pt1411hwru_message_(&message[12])) {
1232 temperature_code =
1234 if (message[15] & RAC_PT1411HWRU_FLAG_FAH) {
1235 for (size_t i = 0; i < sizeof(RAC_PT1411HWRU_TEMPERATURE_F); i++) {
1236 if (progmem_read_byte(&RAC_PT1411HWRU_TEMPERATURE_F[i]) == temperature_code) {
1237 this->target_temperature = static_cast<float>((i + TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN - 32) * 5) / 9;
1238 }
1239 }
1240 } else {
1241 for (size_t i = 0; i < sizeof(RAC_PT1411HWRU_TEMPERATURE_C); i++) {
1242 if (progmem_read_byte(&RAC_PT1411HWRU_TEMPERATURE_C[i]) == temperature_code) {
1244 }
1245 }
1246 }
1247 }
1248 break;
1249 // "Comfort Sense" temperature packet
1251 // "Comfort Sense" feature notes
1252 // IR Code: 0xBA45 xxXX yyYY
1253 // xx: Temperature in °C
1254 // Bit 6: feature state (on/off)
1255 // Bit 7: message contains temperature data for feature (bit 6 must also be set)
1256 // XX: Bitwise complement of xx
1257 // yy: Mode: Auto: 7A
1258 // Cool: 72
1259 // Heat: 7E
1260 // YY: Bitwise complement of yy
1262 // Setting current_temperature this way allows the unit's remote to provide the temperature to HA
1264 }
1265 break;
1266 // Swing mode
1268 if (message[4] == progmem_read_byte(&RAC_PT1411HWRU_SWING_VERTICAL[4])) {
1270 } else {
1272 }
1273 break;
1274 // Generic (old) Toshiba packet
1275 default:
1276 // Add back the length of the header (we pruned it above)
1277 message_length += TOSHIBA_HEADER_LENGTH;
1278 // Validate the second checksum before trusting any more of the message
1279 if (!frame_checksum_valid(message, message_length)) {
1280 return false;
1281 }
1282 // Check if this is a short swing/fix message
1284 // Not supported yet
1285 return false;
1286 }
1287
1288 this->decode_mode_fan_temperature_(message);
1289 // The marker byte tells a Seiya frame from another checksum-valid 11 byte frame within receiver tolerance
1290 if (this->model_ == MODEL_SEIYA && message_length == SEIYA_MESSAGE_LENGTH && message[8] == SEIYA_FEATURE_MARKER) {
1291 this->seiya_decode_swing_(message[9]);
1292 }
1293 }
1294
1295 this->publish_state();
1296 return true;
1297}
1298
1300 message[0] = TOSHIBA_HEADER_BYTE_0;
1301 message[1] = TOSHIBA_HEADER_BYTE_1;
1302 message[2] = length - 6;
1303 message[3] = ~message[2];
1304 message[length - 1] = xor_bytes(message + 4, length - 5);
1305}
1306
1307void ToshibaClimate::encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, const uint8_t nbytes,
1308 const uint8_t repeat) {
1309 const ToshibaTimings timings = timings_for(this->model_);
1311
1312 for (uint8_t copy = 0; copy <= repeat; copy++) {
1313 data->item(timings.header_mark, timings.header_space);
1314
1315 for (uint8_t byte = 0; byte < nbytes; byte++) {
1316 for (uint8_t bit = 0; bit < 8; bit++) {
1317 data->mark(timings.bit_mark);
1318 if (message[byte] & (1 << (7 - bit))) {
1319 data->space(timings.one_space);
1320 } else {
1321 data->space(timings.zero_space);
1322 }
1323 }
1324 }
1325 data->mark(timings.bit_mark);
1326 if (copy < repeat || timings.gap_after_last) {
1327 data->space(timings.gap_space);
1328 }
1329 }
1330}
1331
1332bool ToshibaClimate::decode_(remote_base::RemoteReceiveData *data, uint8_t *message, const uint8_t nbytes) {
1333 const ToshibaTimings timings = timings_for(this->model_);
1334 for (uint8_t byte = 0; byte < nbytes; byte++) {
1335 for (uint8_t bit = 0; bit < 8; bit++) {
1336 if (data->expect_item(timings.bit_mark, timings.one_space)) {
1337 message[byte] |= 1 << (7 - bit);
1338 } else if (data->expect_item(timings.bit_mark, timings.zero_space)) {
1339 message[byte] &= static_cast<uint8_t>(~(1 << (7 - bit)));
1340 } else {
1341 return false;
1342 }
1343 }
1344 }
1345 return true;
1346}
1347
1348void ToshibaClimate::decode_mode_fan_temperature_(const uint8_t *message) {
1349 this->decode_mode_(message);
1350 this->fan_mode = decode_fan(message[6] & 0xF0);
1351 this->target_temperature = (message[5] >> 4) + TOSHIBA_GENERIC_TEMP_C_MIN;
1352}
1353
1355 switch (message[6] & 0x0F) {
1356 case TOSHIBA_MODE_OFF:
1358 break;
1359
1360 case TOSHIBA_MODE_COOL:
1362 break;
1363
1364 case TOSHIBA_MODE_DRY:
1366 break;
1367
1370 break;
1371
1372 case TOSHIBA_MODE_HEAT:
1374 break;
1375
1376 case TOSHIBA_MODE_AUTO:
1377 default:
1379 }
1380}
1381
1383 // Speeds 2 and 4 are not exposed; report the nearest mode
1384 switch (code) {
1385 case TOSHIBA_FAN_SPEED_QUIET:
1387 case TOSHIBA_FAN_SPEED_1:
1389 case TOSHIBA_FAN_SPEED_2:
1390 case TOSHIBA_FAN_SPEED_3:
1392 case TOSHIBA_FAN_SPEED_4:
1393 case TOSHIBA_FAN_SPEED_5:
1395 case TOSHIBA_FAN_SPEED_AUTO:
1396 default:
1398 }
1399}
1400
1401void ToshibaClimate::seiya_decode_swing_(uint8_t code) {
1402 // A momentary command: the neutral value on an ordinary update means no change, not swing off
1403 switch (code) {
1404 case SEIYA_SWING_BOTH:
1405 case SEIYA_SWING_OMNI:
1407 break;
1408 case SEIYA_SWING_VERTICAL:
1410 break;
1411 case SEIYA_SWING_HORIZONTAL:
1413 break;
1414 default:
1415 return;
1416 }
1417 // Keep the tracker in sync so the received command is not sent back out
1418 this->last_swing_mode_ = this->swing_mode;
1419}
1420
1421} // namespace esphome::toshiba
constexpr size_t count(ValueType value) const
Check if the set contains a specific value (std::set compatibility) Returns 1 if present,...
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
float current_temperature
The current temperature of the climate device, as reported from the integration.
Definition climate.h:291
void publish_state()
Publish the state of the climate device, to be called from integrations.
Definition climate.cpp:432
optional< ClimateDeviceRestoreState > restore_state_()
Restore the state of the climate device, call this from your setup() method.
Definition climate.cpp:365
climate::ClimateSwingModeMask swing_modes_
Definition climate_ir.h:83
bool expect_item(uint32_t mark, uint32_t space)
void set_carrier_frequency(uint32_t carrier_frequency)
Definition remote_base.h:64
optional< ToshibaAcData > decode(RemoteReceiveData src)
void add_on_state_callback(F &&callback)
Add a callback that will be called every time a filtered value arrives.
Definition sensor.h:121
float state
This member variable stores the last state that has passed through all filters.
Definition sensor.h:140
static bool frame_header_valid(const uint8_t *message)
Definition toshiba.h:97
void encode_(remote_base::RemoteTransmitData *data, const uint8_t *message, uint8_t nbytes, uint8_t repeat)
Definition toshiba.cpp:1307
virtual void transmit_current_temperature()
Definition toshiba.h:89
static void finalize_frame(uint8_t *message, uint8_t length)
Definition toshiba.cpp:1299
bool compare_rac_pt1411hwru_packets_(const uint8_t *message1, const uint8_t *message2)
Definition toshiba.cpp:979
virtual float temperature_min()
Definition toshiba.h:81
virtual void sanitize_restore_state(climate::ClimateDeviceRestoreState &restore)
Definition toshiba.h:91
bool is_valid_rac_pt1411hwru_message_(const uint8_t *message)
Definition toshiba.cpp:987
static climate::ClimateFanMode decode_fan(uint8_t code)
Definition toshiba.cpp:1382
uint8_t is_valid_rac_pt1411hwru_header_(const uint8_t *message)
Definition toshiba.cpp:965
bool process_ras_2819t_command_(const remote_base::ToshibaAcData &toshiba_data)
Definition toshiba.cpp:1011
bool decode_(remote_base::RemoteReceiveData *data, uint8_t *message, uint8_t nbytes)
Definition toshiba.cpp:1332
void decode_mode_(const uint8_t *message)
Definition toshiba.cpp:1354
climate::ClimateSwingMode last_swing_mode_
Definition toshiba.h:112
void transmit_rac_pt1411hwru_temp_(bool cs_state=true, bool cs_send_update=true)
Definition toshiba.cpp:706
bool on_receive(remote_base::RemoteReceiveData data) override
Definition toshiba.cpp:1094
static bool frame_checksum_valid(const uint8_t *message, uint8_t length)
Definition toshiba.h:101
ClimateFanMode fan_mode
Definition climate.h:3
const LogString * message
Definition component.cpp:35
bool state
Definition fan.h:2
constexpr float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM
Definition bme680.cpp:27
@ CLIMATE_SWING_OFF
The swing mode is set to Off.
@ CLIMATE_SWING_HORIZONTAL
The fan mode is set to Horizontal.
@ CLIMATE_SWING_VERTICAL
The fan mode is set to Vertical.
@ CLIMATE_SWING_BOTH
The fan mode is set to Both.
@ 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.
ClimateFanMode
NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value.
@ 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_QUIET
The fan mode is set to Quiet.
@ CLIMATE_FAN_HIGH
The fan mode is set to High.
const uint8_t RAS_2819T_FAN2_MEDIUM
Definition toshiba.cpp:94
const uint8_t RAC_PT1411HWRU_MODE_HEAT
Definition toshiba.cpp:63
const uint16_t RAS_2819T_VALID_HEADER1
Definition toshiba.cpp:122
const uint8_t TOSHIBA_MODE_HEAT
Definition toshiba.cpp:25
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_AUTO
Definition toshiba.cpp:104
const uint8_t RAC_PT1411HWRU_MODE_FAN
Definition toshiba.cpp:62
const uint8_t RAC_PT1411HWRU_CS_ENABLED
Definition toshiba.cpp:40
const uint16_t TOSHIBA_CARRIER_FREQUENCY
Definition toshiba.cpp:18
@ MODEL_RAC_PT1411HWRU_C
Definition toshiba.h:36
@ MODEL_RAC_PT1411HWRU_F
Definition toshiba.h:37
const uint8_t RAC_PT1411HWRU_MODE_AUTO
Definition toshiba.cpp:59
const uint16_t RAS_2819T_FAN_AUTO
Definition toshiba.cpp:84
const RacPt1411hwruFanSpeed RAC_PT1411HWRU_NO_FAN
Definition toshiba.cpp:57
const uint8_t RAC_PT1411HWRU_MODE_COOL
Definition toshiba.cpp:60
const uint8_t RAS_2819T_DRY_BYTE3
Definition toshiba.cpp:126
const uint64_t RAS_2819T_POWER_OFF_COMMAND
Definition toshiba.cpp:114
const uint8_t RAC_PT1411HWRU_TEMPERATURE_FAN_ONLY
Definition toshiba.cpp:66
const float TOSHIBA_RAS_2819T_TEMP_C_MAX
Definition toshiba.h:52
const uint8_t TOSHIBA_COMMAND_MOTION
Definition toshiba.cpp:20
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_LOW
Definition toshiba.cpp:106
const uint8_t RAS_2819T_FAN2_QUIET
Definition toshiba.cpp:92
const uint8_t RAS_2819T_FAN2_AUTO
Definition toshiba.cpp:91
const uint8_t RAC_PT1411HWRU_FLAG_FAH
Definition toshiba.cpp:30
const uint8_t RAS_2819T_HEAT_SUFFIX
Definition toshiba.cpp:141
const uint8_t RAS_2819T_AUTO_DRY_SUFFIX
Definition toshiba.cpp:140
const uint64_t RAS_2819T_SWING_TOGGLE
Definition toshiba.cpp:111
const uint8_t RAC_PT1411HWRU_CS_FOOTER_COOL
Definition toshiba.cpp:44
const uint8_t RAC_PT1411HWRU_CS_DATA
Definition toshiba.cpp:41
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_MEDIUM
Definition toshiba.cpp:107
const uint8_t TOSHIBA_MODE_DRY
Definition toshiba.cpp:24
const uint8_t RAC_PT1411HWRU_FAN_OFF
Definition toshiba.cpp:51
const uint8_t RAC_PT1411HWRU_FLAG_NEG
Definition toshiba.cpp:32
const uint8_t RAS_2819T_VALID_HEADER2
Definition toshiba.cpp:123
const uint8_t RAS_2819T_HEADER2
Definition toshiba.cpp:80
const float TOSHIBA_RAC_PT1411HWRU_TEMP_F_MIN
Definition toshiba.h:49
const uint8_t RAS_2819T_FAN2_LOW
Definition toshiba.cpp:93
const uint8_t TOSHIBA_MODE_FAN_ONLY
Definition toshiba.cpp:26
const float TOSHIBA_GENERIC_TEMP_C_MAX
Definition toshiba.h:46
const uint8_t TOSHIBA_MODE_COOL
Definition toshiba.cpp:23
const float TOSHIBA_GENERIC_TEMP_C_MIN
Definition toshiba.h:45
const uint16_t TOSHIBA_ZERO_SPACE
Definition toshiba.cpp:16
constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_LOW
Definition toshiba.cpp:53
ESPHOME_LOG_TAG(TAG, "toshiba.climate")
const uint8_t RAC_PT1411HWRU_MESSAGE_HEADER1
Definition toshiba.cpp:37
const uint8_t RAS_2819T_DRY_BYTE2
Definition toshiba.cpp:125
const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MAX
Definition toshiba.h:48
const uint8_t RAC_PT1411HWRU_CS_HEADER
Definition toshiba.cpp:42
const uint16_t RAS_2819T_FAN_MEDIUM
Definition toshiba.cpp:87
constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_AUTO
Definition toshiba.cpp:52
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_QUIET
Definition toshiba.cpp:105
const uint8_t RAC_PT1411HWRU_CS_FOOTER_AUTO
Definition toshiba.cpp:43
const uint16_t RAS_2819T_HEADER1
Definition toshiba.cpp:79
const uint8_t TOSHIBA_MODE_AUTO
Definition toshiba.cpp:22
const uint8_t RAS_2819T_AUTO_TEMP_OFFSET
Definition toshiba.cpp:131
const uint8_t RAC_PT1411HWRU_MODE_DRY
Definition toshiba.cpp:61
const uint8_t RAS_2819T_FAN_ONLY_TEMP
Definition toshiba.cpp:133
const uint8_t RAS_2819T_AUTO_BYTE3
Definition toshiba.cpp:130
const uint16_t TOSHIBA_ONE_SPACE
Definition toshiba.cpp:17
constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_HIGH
Definition toshiba.cpp:55
const uint8_t RAC_PT1411HWRU_MESSAGE_HEADER0
Definition toshiba.cpp:36
const float TOSHIBA_RAS_2819T_TEMP_C_MIN
Definition toshiba.h:51
constexpr RacPt1411hwruFanSpeed RAC_PT1411HWRU_FAN_MED
Definition toshiba.cpp:54
const uint8_t RAC_PT1411HWRU_FLAG_FRAC
Definition toshiba.cpp:31
const uint8_t RAS_2819T_AUTO_BYTE2
Definition toshiba.cpp:129
const uint8_t RAS_2819T_HEAT_TEMP_OFFSET
Definition toshiba.cpp:136
const uint8_t TOSHIBA_MODE_OFF
Definition toshiba.cpp:27
const Ras2819tPacketSuffix RAS_2819T_SUFFIX_HIGH
Definition toshiba.cpp:108
const uint8_t RAC_PT1411HWRU_CS_FOOTER_HEAT
Definition toshiba.cpp:45
const uint8_t RAS_2819T_FAN2_HIGH
Definition toshiba.cpp:95
const uint16_t TOSHIBA_PACKET_SPACE
Definition toshiba.cpp:15
const uint8_t RAS_2819T_FAN_ONLY_TEMP_INV
Definition toshiba.cpp:134
const uint8_t RAC_PT1411HWRU_MODE_OFF
Definition toshiba.cpp:64
const float TOSHIBA_RAC_PT1411HWRU_TEMP_C_MIN
Definition toshiba.h:47
const uint8_t RAC_PT1411HWRU_MESSAGE_LENGTH
Definition toshiba.cpp:38
const uint8_t RAC_PT1411HWRU_FLAG_MASK
Definition toshiba.cpp:34
const uint8_t RAS_2819T_AUTO_DRY_FAN_BYTE
Definition toshiba.cpp:139
const uint16_t RAS_2819T_FAN_LOW
Definition toshiba.cpp:86
const std::array< uint64_t, 2 > RAS_2819T_VALID_SINGLE_COMMANDS
Definition toshiba.cpp:117
const uint16_t RAS_2819T_FAN_QUIET
Definition toshiba.cpp:85
const uint16_t RAS_2819T_FAN_HIGH
Definition toshiba.cpp:88
const uint8_t RAC_PT1411HWRU_SWING_HEADER
Definition toshiba.cpp:47
const uint8_t RAS_2819T_MESSAGE_LENGTH
Definition toshiba.cpp:81
const uint8_t RAS_2819T_DRY_TEMP_OFFSET
Definition toshiba.cpp:127
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
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
void progmem_memcpy(void *dst, const void *src, size_t len)
Definition hal.h:56
uint8_t progmem_read_byte(const uint8_t *addr)
Definition hal.h:51
uint16_t temperature
Definition sun_gtil2.cpp:12
uint16_t length
Definition tt21100.cpp:0