#include "hitachi_rar6ne1.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" #include #include namespace esphome { namespace hitachi_rar6ne1 { static const char *const TAG = "hitachi_rar6ne1.climate"; // Stato di riferimento reale del RAR-6NE1 (ON 26C Cool). Fornisce tutti i byte // "fissi" (intestazione/costanti); i campi variabili vengono sovrascritti in // build_state_(). Indici: 0..27. static const uint8_t BASE_STATE[kStateLength] = { 0x80, 0x08, 0x0C, 0x02, 0xFD, 0x80, 0x7F, 0x88, 0x48, 0xC0, 0x20, 0x2C, 0x00, 0x20, 0xE0, 0xE0, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x30, 0x00, 0x41}; // Frame OFF realmente catturato dal RAR-6NE1 (checksum 0x40 verificato): usato // dal test self-loopback per avere un riferimento deterministico e sicuro. static const uint8_t REF_OFF[kStateLength] = { 0x80, 0x08, 0x0C, 0x02, 0xFD, 0x80, 0x7F, 0x88, 0x48, 0xC0, 0x20, 0x2C, 0x00, 0x20, 0xE0, 0xE0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, 0x80, 0x30, 0x00, 0x40}; // Inverte l'ordine dei bit di un byte (MSB<->LSB). static inline uint8_t reverse_bits8(uint8_t x) { uint8_t r = 0; for (uint8_t i = 0; i < 8; i++) r |= static_cast(((x >> i) & 0x01) << (7 - i)); return r; } uint8_t HitachiRar6ne1Climate::calc_checksum_(const uint8_t state[kStateLength]) { uint8_t sum = 62; for (uint8_t i = 0; i < kStateLength - 1; i++) sum -= reverse_bits8(state[i]); return reverse_bits8(sum); } void HitachiRar6ne1Climate::build_state_(uint8_t state[kStateLength]) { std::memcpy(state, BASE_STATE, kStateLength); // byte[9]: codice tasto (vedi nota in .h). Variabile #1 da testare. state[9] = kButtonCode; const bool power_on = this->mode != climate::CLIMATE_MODE_OFF; // Mode (byte[10]). In OFF teniamo Cool come modalita' di base nel frame. switch (this->mode) { case climate::CLIMATE_MODE_COOL: state[10] = 0x20; break; case climate::CLIMATE_MODE_HEAT: state[10] = 0xC0; break; case climate::CLIMATE_MODE_DRY: state[10] = 0xA0; break; case climate::CLIMATE_MODE_FAN_ONLY: state[10] = 0x30; break; case climate::CLIMATE_MODE_HEAT_COOL: state[10] = 0x40; break; case climate::CLIMATE_MODE_OFF: default: state[10] = 0x20; break; } // Temp (byte[11]) = reverseBits(C*2). In Fan-only non si applica -> 0x01. if (this->mode == climate::CLIMATE_MODE_FAN_ONLY) { state[11] = 0x01; } else { float tc = clamp(this->target_temperature, kMinTempC, kMaxTempC); uint8_t t = static_cast(lroundf(tc)); state[11] = reverse_bits8(static_cast(t * 2)); } // Fan speed (byte[13]). In Dry l'unita' forza 2/4 (0xC0). if (this->mode == climate::CLIMATE_MODE_DRY) { state[13] = 0xC0; } else { switch (this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)) { case climate::CLIMATE_FAN_QUIET: state[13] = 0x40; break; // 1/4 case climate::CLIMATE_FAN_LOW: state[13] = 0xC0; break; // 2/4 case climate::CLIMATE_FAN_MEDIUM: state[13] = 0x20; break; // 3/4 case climate::CLIMATE_FAN_HIGH: state[13] = 0xA0; break; // 4/4 case climate::CLIMATE_FAN_AUTO: default: state[13] = 0x80; break; } } // Swing V (byte[14]) / H (byte[15]): 0xE0 = on, 0x60 = off. const bool swing_v = this->swing_mode == climate::CLIMATE_SWING_VERTICAL || this->swing_mode == climate::CLIMATE_SWING_BOTH; const bool swing_h = this->swing_mode == climate::CLIMATE_SWING_HORIZONTAL || this->swing_mode == climate::CLIMATE_SWING_BOTH; state[14] = swing_v ? 0xE0 : 0x60; state[15] = swing_h ? 0xE0 : 0x60; // Power (byte[17] bit0). if (power_on) state[17] |= 0x01; else state[17] &= ~0x01; // Preset: Eco -> Silent (byte[26] bit4), Boost -> Powerful (byte[25] bit2). const climate::ClimatePreset preset = this->preset.value_or(climate::CLIMATE_PRESET_NONE); if (preset == climate::CLIMATE_PRESET_ECO) state[26] |= 0x10; else state[26] &= ~0x10; if (preset == climate::CLIMATE_PRESET_BOOST) state[25] |= 0x04; else state[25] &= ~0x04; // Checksum (byte[27]). state[27] = calc_checksum_(state); } void HitachiRar6ne1Climate::transmit_frame_(const uint8_t state[kStateLength]) { ESP_LOGD(TAG, "TX: mode[10]=0x%02X temp[11]=0x%02X fan[13]=0x%02X " "pwr[17]=0x%02X tasto[9]=0x%02X ck[27]=0x%02X", state[10], state[11], state[13], state[17], state[9], state[27]); auto transmit = this->transmitter_->transmit(); auto *data = transmit.get_data(); data->set_carrier_frequency(kCarrierFrequency); data->reserve(2 + kStateLength * 8 * 2 + 2); data->mark(kHeaderMark); data->space(kHeaderSpace); for (uint8_t i = 0; i < kStateLength; i++) { for (int8_t bit = 7; bit >= 0; bit--) { // MSB-first data->mark(kBitMark); data->space((state[i] & (1 << bit)) ? kOneSpace : kZeroSpace); } } data->mark(kBitMark); // footer mark data->space(0); transmit.perform(); } void HitachiRar6ne1Climate::transmit_state() { uint8_t state[kStateLength]; this->build_state_(state); this->transmit_frame_(state); } void HitachiRar6ne1Climate::send_test_frame() { ESP_LOGI(TAG, "Test self-loopback: invio frame OFF di riferimento"); this->transmit_frame_(REF_OFF); } void HitachiRar6ne1Climate::control(const climate::ClimateCall &call) { // Sul telecomando reale Silent e Powerful sono accoppiati alla ventola // (Silent = minima, Powerful = massima) e mutuamente esclusivi. Teniamo lo // stato coerente: attivando un preset forziamo la ventola corrispondente; // se l'utente cambia la ventola mentre un preset e' attivo, usciamo dal preset. climate::ClimateCall modified = call; if (call.get_preset().has_value()) { switch (*call.get_preset()) { case climate::CLIMATE_PRESET_ECO: modified.set_fan_mode(climate::CLIMATE_FAN_QUIET); break; case climate::CLIMATE_PRESET_BOOST: modified.set_fan_mode(climate::CLIMATE_FAN_HIGH); break; default: break; } } else if (call.get_fan_mode().has_value() && this->preset.value_or(climate::CLIMATE_PRESET_NONE) != climate::CLIMATE_PRESET_NONE) { modified.set_preset(climate::CLIMATE_PRESET_NONE); } climate_ir::ClimateIR::control(modified); } bool HitachiRar6ne1Climate::on_receive(remote_base::RemoteReceiveData data) { if (!data.expect_item(kHeaderMark, kHeaderSpace)) return false; uint8_t state[kStateLength] = {0}; for (uint8_t i = 0; i < kStateLength; i++) { for (int8_t bit = 7; bit >= 0; bit--) { // MSB-first if (!data.expect_mark(kBitMark)) return false; if (data.expect_space(kOneSpace)) { state[i] |= static_cast(1 << bit); } else if (data.expect_space(kZeroSpace)) { // bit a 0 } else { return false; } } } if (state[27] != calc_checksum_(state)) { ESP_LOGW(TAG, "RX: checksum errato, frame ignorato"); return false; } // Power / Mode (byte[17] bit0, byte[10]). if ((state[17] & 0x01) == 0) { this->mode = climate::CLIMATE_MODE_OFF; } else { switch (state[10]) { case 0x20: this->mode = climate::CLIMATE_MODE_COOL; break; case 0xC0: this->mode = climate::CLIMATE_MODE_HEAT; break; case 0xA0: this->mode = climate::CLIMATE_MODE_DRY; break; case 0x30: this->mode = climate::CLIMATE_MODE_FAN_ONLY; break; case 0x40: this->mode = climate::CLIMATE_MODE_HEAT_COOL; break; default: break; } } // Temp (byte[11]); non applicabile in Fan-only. if (state[10] != 0x30) { this->target_temperature = reverse_bits8(state[11]) / 2.0f; } // Fan (byte[13]). switch (state[13]) { case 0x40: this->fan_mode = climate::CLIMATE_FAN_QUIET; break; case 0xC0: this->fan_mode = climate::CLIMATE_FAN_LOW; break; case 0x20: this->fan_mode = climate::CLIMATE_FAN_MEDIUM; break; case 0xA0: this->fan_mode = climate::CLIMATE_FAN_HIGH; break; case 0x80: this->fan_mode = climate::CLIMATE_FAN_AUTO; break; default: break; } // Swing (byte[14]/[15]). const bool swing_v = state[14] == 0xE0; const bool swing_h = state[15] == 0xE0; if (swing_v && swing_h) this->swing_mode = climate::CLIMATE_SWING_BOTH; else if (swing_v) this->swing_mode = climate::CLIMATE_SWING_VERTICAL; else if (swing_h) this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; else this->swing_mode = climate::CLIMATE_SWING_OFF; // Preset (byte[26] Silent / byte[25] Powerful). if (state[26] & 0x10) this->preset = climate::CLIMATE_PRESET_ECO; else if (state[25] & 0x04) this->preset = climate::CLIMATE_PRESET_BOOST; else this->preset = climate::CLIMATE_PRESET_NONE; ESP_LOGD(TAG, "RX ok (tasto byte[9]=0x%02X)", state[9]); this->publish_state(); return true; } } // namespace hitachi_rar6ne1 } // namespace esphome