// Copyright 2018-2019 David Conran // // Code to emulate Hitachi protocol compatible devices. // Should be compatible with: // * Hitachi RAS-35THA6 remote // #include "ir_Hitachi.h" #include #include #ifndef ARDUINO #include #endif #include "IRrecv.h" #include "IRremoteESP8266.h" #include "IRsend.h" #include "IRtext.h" #include "IRutils.h" // Constants // Ref: https://github.com/crankyoldgit/IRremoteESP8266/issues/417 const uint16_t kHitachiAcHdrMark = 3300; const uint16_t kHitachiAcHdrSpace = 1700; const uint16_t kHitachiAc1HdrMark = 3400; const uint16_t kHitachiAc1HdrSpace = 3400; const uint16_t kHitachiAcBitMark = 400; const uint16_t kHitachiAcOneSpace = 1250; const uint16_t kHitachiAcZeroSpace = 500; const uint32_t kHitachiAcMinGap = kDefaultMessageGap; // Just a guess. // Support for HitachiAc424 protocol // Ref: https://github.com/crankyoldgit/IRremoteESP8266/issues/973 const uint16_t kHitachiAc424LdrMark = 29784; // Leader const uint16_t kHitachiAc424LdrSpace = 49290; // Leader const uint16_t kHitachiAc424HdrMark = 3416; // Header const uint16_t kHitachiAc424HdrSpace = 1604; // Header const uint16_t kHitachiAc424BitMark = 463; const uint16_t kHitachiAc424OneSpace = 1208; const uint16_t kHitachiAc424ZeroSpace = 372; // Support for HitachiAc3 protocol // Ref: https://github.com/crankyoldgit/IRremoteESP8266/issues/1060 const uint16_t kHitachiAc3HdrMark = 3400; // Header const uint16_t kHitachiAc3HdrSpace = 1660; // Header const uint16_t kHitachiAc3BitMark = 460; const uint16_t kHitachiAc3OneSpace = 1250; const uint16_t kHitachiAc3ZeroSpace = 410; using irutils::addBoolToString; using irutils::addIntToString; using irutils::addLabeledString; using irutils::addModeToString; using irutils::addModelToString; using irutils::addFanToString; using irutils::addTempToString; using irutils::minsToString; using irutils::setBit; using irutils::setBits; #if (SEND_HITACHI_AC || SEND_HITACHI_AC2) // Send a Hitachi A/C message. // // Args: // data: An array of bytes containing the IR command. // nbytes: Nr. of bytes of data in the array. (>=kHitachiAcStateLength) // repeat: Nr. of times the message is to be repeated. (Default = 0). // // Status: STABLE / Working. // // Ref: // https://github.com/crankyoldgit/IRremoteESP8266/issues/417 void IRsend::sendHitachiAC(const unsigned char data[], const uint16_t nbytes, const uint16_t repeat) { if (nbytes < kHitachiAcStateLength) return; // Not enough bytes to send a proper message. sendGeneric(kHitachiAcHdrMark, kHitachiAcHdrSpace, kHitachiAcBitMark, kHitachiAcOneSpace, kHitachiAcBitMark, kHitachiAcZeroSpace, kHitachiAcBitMark, kHitachiAcMinGap, data, nbytes, 38, true, repeat, 50); } #endif // (SEND_HITACHI_AC || SEND_HITACHI_AC2) #if SEND_HITACHI_AC1 // Send a Hitachi A/C 13-byte message. // // For devices: // Hitachi A/C Series VI (Circa 2007) / Remote: LT0541-HTA // // Args: // data: An array of bytes containing the IR command. // nbytes: Nr. of bytes of data in the array. (>=kHitachiAc1StateLength) // repeat: Nr. of times the message is to be repeated. (Default = 0). // // Status: STABLE / Confirmed Working. // // Ref: // https://github.com/crankyoldgit/IRremoteESP8266/issues/453 // Basically the same as sendHitatchiAC() except different size and header. void IRsend::sendHitachiAC1(const unsigned char data[], const uint16_t nbytes, const uint16_t repeat) { if (nbytes < kHitachiAc1StateLength) return; // Not enough bytes to send a proper message. sendGeneric(kHitachiAc1HdrMark, kHitachiAc1HdrSpace, kHitachiAcBitMark, kHitachiAcOneSpace, kHitachiAcBitMark, kHitachiAcZeroSpace, kHitachiAcBitMark, kHitachiAcMinGap, data, nbytes, kHitachiAcFreq, true, repeat, kDutyDefault); } #endif // SEND_HITACHI_AC1 #if SEND_HITACHI_AC2 // Send a Hitachi A/C 53-byte message. // // For devices: // Hitachi A/C Series VI (Circa 2007) / Remote: LT0541-HTA // // Args: // data: An array of bytes containing the IR command. // nbytes: Nr. of bytes of data in the array. (>=kHitachiAc2StateLength) // repeat: Nr. of times the message is to be repeated. (Default = 0). // // Status: STABLE / Expected to work. // // Ref: // https://github.com/crankyoldgit/IRremoteESP8266/issues/417 // Basically the same as sendHitatchiAC() except different size. void IRsend::sendHitachiAC2(const unsigned char data[], const uint16_t nbytes, const uint16_t repeat) { if (nbytes < kHitachiAc2StateLength) return; // Not enough bytes to send a proper message. sendHitachiAC(data, nbytes, repeat); } #endif // SEND_HITACHI_AC2 // Class for handling the remote control on a Hitachi 28 byte A/C message. // Inspired by: // https://github.com/ToniA/arduino-heatpumpir/blob/master/HitachiHeatpumpIR.cpp IRHitachiAc::IRHitachiAc(const uint16_t pin, const bool inverted, const bool use_modulation) : _irsend(pin, inverted, use_modulation) { stateReset(); } void IRHitachiAc::stateReset(void) { remote_state[0] = 0x80; remote_state[1] = 0x08; remote_state[2] = 0x0C; remote_state[3] = 0x02; remote_state[4] = 0xFD; remote_state[5] = 0x80; remote_state[6] = 0x7F; remote_state[7] = 0x88; remote_state[8] = 0x48; remote_state[9] = 0x10; for (uint8_t i = 10; i < kHitachiAcStateLength; i++) remote_state[i] = 0x00; remote_state[14] = 0x60; remote_state[15] = 0x60; remote_state[24] = 0x80; setTemp(23); } void IRHitachiAc::begin(void) { _irsend.begin(); } uint8_t IRHitachiAc::calcChecksum(const uint8_t state[], const uint16_t length) { uint8_t sum = 62; for (uint16_t i = 0; i < length - 1; i++) sum -= reverseBits(state[i], 8); return reverseBits(sum, 8); } void IRHitachiAc::checksum(const uint16_t length) { remote_state[length - 1] = calcChecksum(remote_state, length); } bool IRHitachiAc::validChecksum(const uint8_t state[], const uint16_t length) { if (length < 2) return true; // Assume true for lengths that are too short. return (state[length - 1] == calcChecksum(state, length)); } uint8_t *IRHitachiAc::getRaw(void) { checksum(); return remote_state; } void IRHitachiAc::setRaw(const uint8_t new_code[], const uint16_t length) { memcpy(remote_state, new_code, std::min(length, kHitachiAcStateLength)); } #if SEND_HITACHI_AC void IRHitachiAc::send(const uint16_t repeat) { _irsend.sendHitachiAC(getRaw(), kHitachiAcStateLength, repeat); } #endif // SEND_HITACHI_AC bool IRHitachiAc::getPower(void) { return GETBIT8(remote_state[17], kHitachiAcPowerOffset); } void IRHitachiAc::setPower(const bool on) { setBit(&remote_state[17], kHitachiAcPowerOffset, on); } void IRHitachiAc::on(void) { setPower(true); } void IRHitachiAc::off(void) { setPower(false); } uint8_t IRHitachiAc::getMode(void) { return reverseBits(remote_state[10], 8); } void IRHitachiAc::setMode(const uint8_t mode) { uint8_t newmode = mode; switch (mode) { // Fan mode sets a special temp. case kHitachiAcFan: setTemp(64); break; case kHitachiAcAuto: case kHitachiAcHeat: case kHitachiAcCool: case kHitachiAcDry: break; default: newmode = kHitachiAcAuto; } remote_state[10] = reverseBits(newmode, 8); if (mode != kHitachiAcFan) setTemp(_previoustemp); setFan(getFan()); // Reset the fan speed after the mode change. } uint8_t IRHitachiAc::getTemp(void) { return reverseBits(remote_state[11], 8) >> 1; } void IRHitachiAc::setTemp(const uint8_t celsius) { uint8_t temp; if (celsius != 64) _previoustemp = celsius; switch (celsius) { case 64: temp = celsius; break; default: temp = std::min(celsius, kHitachiAcMaxTemp); temp = std::max(temp, kHitachiAcMinTemp); } remote_state[11] = reverseBits(temp << 1, 8); if (temp == kHitachiAcMinTemp) remote_state[9] = 0x90; else remote_state[9] = 0x10; } uint8_t IRHitachiAc::getFan(void) { return reverseBits(remote_state[13], 8); } void IRHitachiAc::setFan(const uint8_t speed) { uint8_t fanmin = kHitachiAcFanAuto; uint8_t fanmax = kHitachiAcFanHigh; switch (getMode()) { case kHitachiAcDry: // Only 2 x low speeds in Dry mode. fanmin = kHitachiAcFanLow; fanmax = kHitachiAcFanLow + 1; break; case kHitachiAcFan: fanmin = kHitachiAcFanLow; // No Auto in Fan mode. break; } uint8_t newspeed = std::max(speed, fanmin); newspeed = std::min(newspeed, fanmax); remote_state[13] = reverseBits(newspeed, 8); } bool IRHitachiAc::getSwingVertical(void) { return GETBIT8(remote_state[14], kHitachiAcSwingOffset); } void IRHitachiAc::setSwingVertical(const bool on) { setBit(&remote_state[14], kHitachiAcSwingOffset, on); } bool IRHitachiAc::getSwingHorizontal(void) { return GETBIT8(remote_state[15], kHitachiAcSwingOffset); } void IRHitachiAc::setSwingHorizontal(const bool on) { setBit(&remote_state[15], kHitachiAcSwingOffset, on); } // Convert a standard A/C mode into its native mode. uint8_t IRHitachiAc::convertMode(const stdAc::opmode_t mode) { switch (mode) { case stdAc::opmode_t::kCool: return kHitachiAcCool; case stdAc::opmode_t::kHeat: return kHitachiAcHeat; case stdAc::opmode_t::kDry: return kHitachiAcDry; case stdAc::opmode_t::kFan: return kHitachiAcFan; default: return kHitachiAcAuto; } } // Convert a standard A/C Fan speed into its native fan speed. uint8_t IRHitachiAc::convertFan(const stdAc::fanspeed_t speed) { switch (speed) { case stdAc::fanspeed_t::kMin: case stdAc::fanspeed_t::kLow: return kHitachiAcFanLow; case stdAc::fanspeed_t::kMedium: return kHitachiAcFanLow + 1; case stdAc::fanspeed_t::kHigh: return kHitachiAcFanHigh - 1; case stdAc::fanspeed_t::kMax: return kHitachiAcFanHigh; default: return kHitachiAcFanAuto; } } // Convert a native mode to it's common equivalent. stdAc::opmode_t IRHitachiAc::toCommonMode(const uint8_t mode) { switch (mode) { case kHitachiAcCool: return stdAc::opmode_t::kCool; case kHitachiAcHeat: return stdAc::opmode_t::kHeat; case kHitachiAcDry: return stdAc::opmode_t::kDry; case kHitachiAcFan: return stdAc::opmode_t::kFan; default: return stdAc::opmode_t::kAuto; } } // Convert a native fan speed to it's common equivalent. stdAc::fanspeed_t IRHitachiAc::toCommonFanSpeed(const uint8_t speed) { switch (speed) { case kHitachiAcFanHigh: return stdAc::fanspeed_t::kMax; case kHitachiAcFanHigh - 1: return stdAc::fanspeed_t::kHigh; case kHitachiAcFanLow + 1: return stdAc::fanspeed_t::kMedium; case kHitachiAcFanLow: return stdAc::fanspeed_t::kLow; default: return stdAc::fanspeed_t::kAuto; } } // Convert the A/C state to it's common equivalent. stdAc::state_t IRHitachiAc::toCommon(void) { stdAc::state_t result; result.protocol = decode_type_t::HITACHI_AC; result.model = -1; // No models used. result.power = this->getPower(); result.mode = this->toCommonMode(this->getMode()); result.celsius = true; result.degrees = this->getTemp(); result.fanspeed = this->toCommonFanSpeed(this->getFan()); result.swingv = this->getSwingVertical() ? stdAc::swingv_t::kAuto : stdAc::swingv_t::kOff; result.swingh = this->getSwingHorizontal() ? stdAc::swingh_t::kAuto : stdAc::swingh_t::kOff; // Not supported. result.quiet = false; result.turbo = false; result.clean = false; result.econo = false; result.filter = false; result.light = false; result.beep = false; result.sleep = -1; result.clock = -1; return result; } // Convert the internal state into a human readable string. String IRHitachiAc::toString(void) { String result = ""; result.reserve(110); // Reserve some heap for the string to reduce fragging. result += addBoolToString(getPower(), kPowerStr, false); result += addModeToString(getMode(), kHitachiAcAuto, kHitachiAcCool, kHitachiAcHeat, kHitachiAcDry, kHitachiAcFan); result += addTempToString(getTemp()); result += addFanToString(getFan(), kHitachiAcFanHigh, kHitachiAcFanLow, kHitachiAcFanAuto, kHitachiAcFanAuto, kHitachiAcFanMed); result += addBoolToString(getSwingVertical(), kSwingVStr); result += addBoolToString(getSwingHorizontal(), kSwingHStr); return result; } // Class for handling the remote control on a Hitachi 13 byte A/C message. // Ref: // https://github.com/crankyoldgit/IRremoteESP8266/issues/1056 IRHitachiAc1::IRHitachiAc1(const uint16_t pin, const bool inverted, const bool use_modulation) : _irsend(pin, inverted, use_modulation) { stateReset(); } void IRHitachiAc1::stateReset(void) { for (uint8_t i = 0; i < kHitachiAc1StateLength; i++) remote_state[i] = 0x00; // Copy in a known good state. remote_state[0] = 0xB2; remote_state[1] = 0xAE; remote_state[2] = 0x4D; remote_state[3] = 0x91; remote_state[4] = 0xF0; remote_state[5] = 0xE1; remote_state[6] = 0xA4; remote_state[11] = 0x61; remote_state[12] = 0x24; } void IRHitachiAc1::begin(void) { _irsend.begin(); } uint8_t IRHitachiAc1::calcChecksum(const uint8_t state[], const uint16_t length) { uint8_t sum = 0; for (uint16_t i = kHitachiAc1ChecksumStartByte; i < length - 1; i++) { sum += reverseBits(GETBITS8(state[i], kLowNibble, kNibbleSize), kNibbleSize); sum += reverseBits(GETBITS8(state[i], kHighNibble, kNibbleSize), kNibbleSize); } return reverseBits(sum, 8); } void IRHitachiAc1::checksum(const uint16_t length) { remote_state[length - 1] = calcChecksum(remote_state, length); } bool IRHitachiAc1::validChecksum(const uint8_t state[], const uint16_t length) { if (length < 2) return true; // Assume true for lengths that are too short. return (state[length - 1] == calcChecksum(state, length)); } uint8_t *IRHitachiAc1::getRaw(void) { checksum(); return remote_state; } void IRHitachiAc1::setRaw(const uint8_t new_code[], const uint16_t length) { memcpy(remote_state, new_code, std::min(length, kHitachiAc1StateLength)); } #if SEND_HITACHI_AC void IRHitachiAc1::send(const uint16_t repeat) { _irsend.sendHitachiAC1(getRaw(), kHitachiAc1StateLength, repeat); // Clear the toggle bits as we have actioned them by sending them. setPowerToggle(false); setSwingToggle(false); } #endif // SEND_HITACHI_AC hitachi_ac1_remote_model_t IRHitachiAc1::getModel(void) { switch (GETBITS8(remote_state[kHitachiAc1ModelByte], kHitachiAc1ModelOffset, kHitachiAc1ModelSize)) { case kHitachiAc1Model_B: return hitachi_ac1_remote_model_t::R_LT0541_HTA_B; default: return hitachi_ac1_remote_model_t::R_LT0541_HTA_A; } } void IRHitachiAc1::setModel(const hitachi_ac1_remote_model_t model) { uint8_t value = 0; switch (model) { case hitachi_ac1_remote_model_t::R_LT0541_HTA_B: value = kHitachiAc1Model_B; break; default: value = kHitachiAc1Model_A; // i.e. 'A' mode. } setBits(&remote_state[kHitachiAc1ModelByte], kHitachiAc1ModelOffset, kHitachiAc1ModelSize, value); } bool IRHitachiAc1::getPower(void) { return GETBIT8(remote_state[kHitachiAc1PowerByte], kHitachiAc1PowerOffset); } void IRHitachiAc1::setPower(const bool on) { // If the power changes, set the power toggle bit. if (on != getPower()) setPowerToggle(true); setBit(&remote_state[kHitachiAc1PowerByte], kHitachiAc1PowerOffset, on); } bool IRHitachiAc1::getPowerToggle(void) { return GETBIT8(remote_state[kHitachiAc1PowerByte], kHitachiAc1PowerToggleOffset); } void IRHitachiAc1::setPowerToggle(const bool on) { setBit(&remote_state[kHitachiAc1PowerByte], kHitachiAc1PowerToggleOffset, on); } void IRHitachiAc1::on(void) { setPower(true); } void IRHitachiAc1::off(void) { setPower(false); } uint8_t IRHitachiAc1::getMode(void) { return GETBITS8(remote_state[kHitachiAc1ModeByte], kHitachiAc1ModeOffset, kHitachiAc1ModeSize); } void IRHitachiAc1::setMode(const uint8_t mode) { switch (mode) { case kHitachiAc1Auto: setTemp(kHitachiAc1TempAuto); // FALL THRU case kHitachiAc1Fan: case kHitachiAc1Heat: case kHitachiAc1Cool: case kHitachiAc1Dry: setBits(&remote_state[kHitachiAc1ModeByte], kHitachiAc1ModeOffset, kHitachiAc1ModeSize, mode); setSleep(getSleep()); // Correct the sleep mode if required. setFan(getFan()); // Correct the fan speed if required. break; default: setMode(kHitachiAc1Auto); } switch (mode) { case kHitachiAc1Fan: case kHitachiAc1Heat: // Auto fan speed not available in these modes, change if needed. if (getFan() == kHitachiAc1FanAuto) setFan(kHitachiAc1FanLow); } } uint8_t IRHitachiAc1::getTemp(void) { return reverseBits(GETBITS8(remote_state[kHitachiAc1TempByte], kHitachiAc1TempOffset, kHitachiAc1TempSize), kHitachiAc1TempSize) + kHitachiAc1TempDelta; } void IRHitachiAc1::setTemp(const uint8_t celsius) { if (getMode() == kHitachiAc1Auto) return; // Can't change temp in Auto mode. uint8_t temp = std::min(celsius, kHitachiAcMaxTemp); temp = std::max(temp, kHitachiAcMinTemp); temp -= kHitachiAc1TempDelta; temp = reverseBits(temp, kHitachiAc1TempSize); setBits(&remote_state[kHitachiAc1TempByte], kHitachiAc1TempOffset, kHitachiAc1TempSize, temp); } uint8_t IRHitachiAc1::getFan(void) { return GETBITS8(remote_state[kHitachiAc1FanByte], kHitachiAc1FanOffset, kHitachiAc1FanSize); } void IRHitachiAc1::setFan(const uint8_t speed, const bool force) { if (!force) { switch (getMode()) { case kHitachiAc1Dry: setFan(kHitachiAc1FanLow, true); // Dry is locked to Low speed. return; case kHitachiAc1Auto: setFan(kHitachiAc1FanAuto, true); // Auto is locked to Auto speed. return; } } switch (speed) { case kHitachiAc1FanAuto: switch (getMode()) { case kHitachiAc1Heat: case kHitachiAc1Fan: return; // Auto speed not allowed in these modes. } // FALL THRU case kHitachiAc1FanHigh: case kHitachiAc1FanMed: case kHitachiAc1FanLow: setBits(&remote_state[kHitachiAc1FanByte], kHitachiAc1FanOffset, kHitachiAc1FanSize, speed); break; default: setFan(kHitachiAc1FanAuto); } } bool IRHitachiAc1::getSwingToggle(void) { return GETBIT8(remote_state[kHitachiAc1SwingByte], kHitachiAc1SwingToggleOffset); } void IRHitachiAc1::setSwingToggle(const bool toggle) { setBit(&remote_state[kHitachiAc1SwingByte], kHitachiAc1SwingToggleOffset, toggle); } bool IRHitachiAc1::getSwingV(void) { return GETBIT8(remote_state[kHitachiAc1SwingByte], kHitachiAc1SwingVOffset); } void IRHitachiAc1::setSwingV(const bool on) { setBit(&remote_state[kHitachiAc1SwingByte], kHitachiAc1SwingVOffset, on); } bool IRHitachiAc1::getSwingH(void) { return GETBIT8(remote_state[kHitachiAc1SwingByte], kHitachiAc1SwingHOffset); } void IRHitachiAc1::setSwingH(const bool on) { setBit(&remote_state[kHitachiAc1SwingByte], kHitachiAc1SwingHOffset, on); } uint8_t IRHitachiAc1::getSleep(void) { return GETBITS8(remote_state[kHitachiAc1SleepByte], kHitachiAc1SleepOffset, kHitachiAc1SleepSize); } void IRHitachiAc1::setSleep(const uint8_t mode) { // Sleep modes only available in Auto & Cool modes, otherwise it's off. switch (getMode()) { case kHitachiAc1Auto: case kHitachiAc1Cool: setBits(&remote_state[kHitachiAc1SleepByte], kHitachiAc1SleepOffset, kHitachiAc1SleepSize, std::min(mode, kHitachiAc1Sleep4)); break; default: setBits(&remote_state[kHitachiAc1SleepByte], kHitachiAc1SleepOffset, kHitachiAc1SleepSize, kHitachiAc1SleepOff); } } void IRHitachiAc1::setOnTimer(const uint16_t mins) { const uint16_t mins_lsb = reverseBits(mins, kHitachiAc1TimerSize); remote_state[kHitachiAc1OnTimerLowByte] = GETBITS16(mins_lsb, 8, 8); remote_state[kHitachiAc1OnTimerHighByte] = GETBITS16(mins_lsb, 0, 8); } uint16_t IRHitachiAc1::getOnTimer(void) { return reverseBits( (remote_state[kHitachiAc1OnTimerLowByte] << 8) | remote_state[kHitachiAc1OnTimerHighByte], kHitachiAc1TimerSize); } void IRHitachiAc1::setOffTimer(const uint16_t mins) { const uint16_t mins_lsb = reverseBits(mins, kHitachiAc1TimerSize); remote_state[kHitachiAc1OffTimerLowByte] = GETBITS16(mins_lsb, 8, 8); remote_state[kHitachiAc1OffTimerHighByte] = GETBITS16(mins_lsb, 0, 8); } uint16_t IRHitachiAc1::getOffTimer(void) { return reverseBits( (remote_state[kHitachiAc1OffTimerLowByte] << 8) | remote_state[kHitachiAc1OffTimerHighByte], kHitachiAc1TimerSize); } // Convert a standard A/C mode into its native mode. uint8_t IRHitachiAc1::convertMode(const stdAc::opmode_t mode) { switch (mode) { case stdAc::opmode_t::kCool: return kHitachiAc1Cool; case stdAc::opmode_t::kHeat: return kHitachiAc1Heat; case stdAc::opmode_t::kDry: return kHitachiAc1Dry; case stdAc::opmode_t::kFan: return kHitachiAc1Fan; default: return kHitachiAc1Auto; } } // Convert a standard A/C Fan speed into its native fan speed. uint8_t IRHitachiAc1::convertFan(const stdAc::fanspeed_t speed) { switch (speed) { case stdAc::fanspeed_t::kMin: case stdAc::fanspeed_t::kLow: return kHitachiAc1FanLow; case stdAc::fanspeed_t::kMedium: return kHitachiAc1FanMed; case stdAc::fanspeed_t::kHigh: case stdAc::fanspeed_t::kMax: return kHitachiAc1FanHigh; default: return kHitachiAc1FanAuto; } } // Convert a native mode to it's common equivalent. stdAc::opmode_t IRHitachiAc1::toCommonMode(const uint8_t mode) { switch (mode) { case kHitachiAc1Cool: return stdAc::opmode_t::kCool; case kHitachiAc1Heat: return stdAc::opmode_t::kHeat; case kHitachiAc1Dry: return stdAc::opmode_t::kDry; case kHitachiAc1Fan: return stdAc::opmode_t::kFan; default: return stdAc::opmode_t::kAuto; } } // Convert a native fan speed to it's common equivalent. stdAc::fanspeed_t IRHitachiAc1::toCommonFanSpeed(const uint8_t speed) { switch (speed) { case kHitachiAc1FanHigh: return stdAc::fanspeed_t::kMax; case kHitachiAc1FanMed: return stdAc::fanspeed_t::kMedium; case kHitachiAc1FanLow: return stdAc::fanspeed_t::kLow; default: return stdAc::fanspeed_t::kAuto; } } // Convert the A/C state to it's common equivalent. stdAc::state_t IRHitachiAc1::toCommon(void) { stdAc::state_t result; result.protocol = decode_type_t::HITACHI_AC1; result.model = this->getModel(); result.power = this->getPower(); result.mode = this->toCommonMode(this->getMode()); result.celsius = true; result.degrees = this->getTemp(); result.fanspeed = this->toCommonFanSpeed(this->getFan()); result.swingv = this->getSwingV() ? stdAc::swingv_t::kAuto : stdAc::swingv_t::kOff; result.swingh = this->getSwingH() ? stdAc::swingh_t::kAuto : stdAc::swingh_t::kOff; result.sleep = this->getSleep() ? 0 : -1; // Not supported. result.quiet = false; result.turbo = false; result.clean = false; result.econo = false; result.filter = false; result.light = false; result.beep = false; result.clock = -1; return result; } // Convert the internal state into a human readable string. String IRHitachiAc1::toString(void) { String result = ""; result.reserve(170); // Reserve some heap for the string to reduce fragging. result += addModelToString(decode_type_t::HITACHI_AC1, getModel(), false); result += addBoolToString(getPower(), kPowerStr); result += addBoolToString(getPowerToggle(), kPowerToggleStr); result += addModeToString(getMode(), kHitachiAc1Auto, kHitachiAc1Cool, kHitachiAc1Heat, kHitachiAc1Dry, kHitachiAc1Fan); result += addTempToString(getTemp()); result += addFanToString(getFan(), kHitachiAc1FanHigh, kHitachiAc1FanLow, kHitachiAc1FanAuto, kHitachiAc1FanAuto, kHitachiAc1FanMed); result += addBoolToString(getSwingToggle(), kSwingVToggleStr); result += addBoolToString(getSwingV(), kSwingVStr); result += addBoolToString(getSwingH(), kSwingHStr); result += addLabeledString(getSleep() ? uint64ToString(getSleep()) : kOffStr, kSleepStr); result += addLabeledString(getOnTimer() ? minsToString(getOnTimer()) : kOffStr, kOnTimerStr); result += addLabeledString(getOffTimer() ? minsToString(getOffTimer()) : kOffStr, kOffTimerStr); return result; } #if (DECODE_HITACHI_AC || DECODE_HITACHI_AC1 || DECODE_HITACHI_AC2) // Decode the supplied Hitachi A/C message. // // Args: // results: Ptr to the data to decode and where to store the decode result. // offset: The starting index to use when attempting to decode the raw data. // Typically/Defaults to kStartOffset. // nbits: The number of data bits to expect. // Typically kHitachiAcBits, kHitachiAc1Bits, kHitachiAc2Bits // strict: Flag indicating if we should perform strict matching. // Returns: // boolean: True if it can decode it, false if it can't. // // Status: STABLE / Expected to work. // // Supported devices: // Hitachi A/C Series VI (Circa 2007) / Remote: LT0541-HTA // // Ref: // https://github.com/crankyoldgit/IRremoteESP8266/issues/417 // https://github.com/crankyoldgit/IRremoteESP8266/issues/453 bool IRrecv::decodeHitachiAC(decode_results *results, uint16_t offset, const uint16_t nbits, const bool strict) { const uint8_t k_tolerance = _tolerance + 5; if (strict) { switch (nbits) { case kHitachiAcBits: case kHitachiAc1Bits: case kHitachiAc2Bits: break; // Okay to continue. default: return false; // Not strictly a Hitachi message. } } uint16_t hmark; uint32_t hspace; if (nbits == kHitachiAc1Bits) { hmark = kHitachiAc1HdrMark; hspace = kHitachiAc1HdrSpace; } else { hmark = kHitachiAcHdrMark; hspace = kHitachiAcHdrSpace; } // Match Header + Data + Footer if (!matchGeneric(results->rawbuf + offset, results->state, results->rawlen - offset, nbits, hmark, hspace, kHitachiAcBitMark, kHitachiAcOneSpace, kHitachiAcBitMark, kHitachiAcZeroSpace, kHitachiAcBitMark, kHitachiAcMinGap, true, k_tolerance)) return false; // Compliance if (strict) { if (nbits / 8 == kHitachiAcStateLength && !IRHitachiAc::validChecksum(results->state, kHitachiAcStateLength)) return false; if (nbits / 8 == kHitachiAc1StateLength && !IRHitachiAc1::validChecksum(results->state, kHitachiAc1StateLength)) return false; } // Success switch (nbits) { case kHitachiAc1Bits: results->decode_type = HITACHI_AC1; break; case kHitachiAc2Bits: results->decode_type = HITACHI_AC2; break; case kHitachiAcBits: default: results->decode_type = HITACHI_AC; } results->bits = nbits; // No need to record the state as we stored it as we decoded it. // As we use result->state, we don't record value, address, or command as it // is a union data type. return true; } #endif // (DECODE_HITACHI_AC || DECODE_HITACHI_AC1 || DECODE_HITACHI_AC2) #if SEND_HITACHI_AC424 // Send HITACHI_AC424 messages // // Note: This protocol is almost exactly the same as HitachiAC2 except this // variant has a leader section as well, and subtle timing differences. // It is also in LSBF order (per byte), rather than MSBF order. // // Args: // data: An array of bytes containing the IR command. // It is assumed to be in LSBF order for this code. // nbytes: Nr. of bytes of data in the array. (>=kHitachiAc424StateLength) // repeat: Nr. of times the message is to be repeated. // // Status: STABLE / Reported as working. void IRsend::sendHitachiAc424(const uint8_t data[], const uint16_t nbytes, const uint16_t repeat) { enableIROut(kHitachiAcFreq); for (uint16_t r = 0; r <= repeat; r++) { // Leader mark(kHitachiAc424LdrMark); space(kHitachiAc424LdrSpace); // Header + Data + Footer sendGeneric(kHitachiAc424HdrMark, kHitachiAc424HdrSpace, kHitachiAc424BitMark, kHitachiAc424OneSpace, kHitachiAc424BitMark, kHitachiAc424ZeroSpace, kHitachiAc424BitMark, kHitachiAcMinGap, data, nbytes, // Bytes kHitachiAcFreq, false, kNoRepeat, kDutyDefault); } } #endif // SEND_HITACHI_AC424 #if DECODE_HITACHI_AC424 // Decode the supplied Hitachi 424 bit A/C message. // // Note: This protocol is almost exactly the same as HitachiAC2 except this // variant has a leader section as well, and subtle timing differences. // It is also in LSBF order (per byte), rather than MSBF order. // // Args: // results: Ptr to the data to decode and where to store the decode result. // offset: The starting index to use when attempting to decode the raw data. // Typically/Defaults to kStartOffset. // nbits: The number of data bits to expect. Typically kHitachiAc424Bits. // strict: Flag indicating if we should perform strict matching. // Returns: // boolean: True if it can decode it, false if it can't. // // Status: STABLE / Reported as working. // // Supported devices: // Hitachi Shirokumakun / AC Model: RAS-AJ25H / AC Remote Model: RAR-8P2 // Manual (Japanese): // https://kadenfan.hitachi.co.jp/support/raj/item/docs/ras_aj22h_a_tori.pdf // // Ref: // https://github.com/crankyoldgit/IRremoteESP8266/issues/973 bool IRrecv::decodeHitachiAc424(decode_results *results, uint16_t offset, const uint16_t nbits, const bool strict) { if (results->rawlen < 2 * nbits + kHeader + kHeader + kFooter - 1 + offset) return false; // Too short a message to match. if (strict && nbits != kHitachiAc424Bits) return false; uint16_t used; // Leader if (!matchMark(results->rawbuf[offset++], kHitachiAc424LdrMark)) return false; if (!matchSpace(results->rawbuf[offset++], kHitachiAc424LdrSpace)) return false; // Header + Data + Footer used = matchGeneric(results->rawbuf + offset, results->state, results->rawlen - offset, nbits, kHitachiAc424HdrMark, kHitachiAc424HdrSpace, kHitachiAc424BitMark, kHitachiAc424OneSpace, kHitachiAc424BitMark, kHitachiAc424ZeroSpace, kHitachiAc424BitMark, kHitachiAcMinGap, true, kUseDefTol, 0, false); if (used == 0) return false; // We failed to find any data. // Success results->decode_type = decode_type_t::HITACHI_AC424; results->bits = nbits; return true; } #endif // DECODE_HITACHI_AC424 // Class for handling the remote control on a Hitachi_AC424 53 byte A/C message IRHitachiAc424::IRHitachiAc424(const uint16_t pin, const bool inverted, const bool use_modulation) : _irsend(pin, inverted, use_modulation) { stateReset(); } // Reset to auto fan, cooling, 23° Celcius void IRHitachiAc424::stateReset(void) { for (uint8_t i = 0; i < kHitachiAc424StateLength; i++) remote_state[i] = 0x00; remote_state[0] = 0x01; remote_state[1] = 0x10; remote_state[3] = 0x40; remote_state[5] = 0xFF; remote_state[7] = 0xCC; remote_state[33] = 0x80; remote_state[35] = 0x03; remote_state[37] = 0x01; remote_state[39] = 0x88; remote_state[45] = 0xFF; remote_state[47] = 0xFF; remote_state[49] = 0xFF; remote_state[51] = 0xFF; setTemp(23); setPower(true); setMode(kHitachiAc424Cool); setFan(kHitachiAc424FanAuto); } void IRHitachiAc424::setInvertedStates(void) { for (uint8_t i = 3; i < kHitachiAc424StateLength - 1; i += 2) remote_state[i + 1] = ~remote_state[i]; } void IRHitachiAc424::begin(void) { _irsend.begin(); } uint8_t *IRHitachiAc424::getRaw(void) { setInvertedStates(); return remote_state; } void IRHitachiAc424::setRaw(const uint8_t new_code[], const uint16_t length) { memcpy(remote_state, new_code, std::min(length, kHitachiAc424StateLength)); } #if SEND_HITACHI_AC424 void IRHitachiAc424::send(const uint16_t repeat) { _irsend.sendHitachiAc424(getRaw(), kHitachiAc424StateLength, repeat); } #endif // SEND_HITACHI_AC424 bool IRHitachiAc424::getPower(void) { return remote_state[kHitachiAc424PowerByte] == kHitachiAc424PowerOn; } void IRHitachiAc424::setPower(const bool on) { setButton(kHitachiAc424ButtonPowerMode); remote_state[kHitachiAc424PowerByte] = on ? kHitachiAc424PowerOn : kHitachiAc424PowerOff; } void IRHitachiAc424::on(void) { setPower(true); } void IRHitachiAc424::off(void) { setPower(false); } uint8_t IRHitachiAc424::getMode(void) { return GETBITS8(remote_state[kHitachiAc424ModeByte], kLowNibble, kNibbleSize); } void IRHitachiAc424::setMode(const uint8_t mode) { uint8_t newMode = mode; switch (mode) { // Fan mode sets a special temp. case kHitachiAc424Fan: setTemp(kHitachiAc424FanTemp, false); break; case kHitachiAc424Heat: case kHitachiAc424Cool: case kHitachiAc424Dry: break; default: newMode = kHitachiAc424Cool; } setBits(&remote_state[kHitachiAc424ModeByte], kLowNibble, kNibbleSize, newMode); if (newMode != kHitachiAc424Fan) setTemp(_previoustemp); setFan(getFan()); // Reset the fan speed after the mode change. setButton(kHitachiAc424ButtonPowerMode); } uint8_t IRHitachiAc424::getTemp(void) { return GETBITS8(remote_state[kHitachiAc424TempByte], kHitachiAc424TempOffset, kHitachiAc424TempSize); } void IRHitachiAc424::setTemp(const uint8_t celsius, bool setPrevious) { uint8_t temp; temp = std::min(celsius, kHitachiAc424MaxTemp); temp = std::max(temp, kHitachiAc424MinTemp); setBits(&remote_state[kHitachiAc424TempByte], kHitachiAc424TempOffset, kHitachiAc424TempSize, temp); if (_previoustemp > temp) setButton(kHitachiAc424ButtonTempDown); else if (_previoustemp < temp) setButton(kHitachiAc424ButtonTempUp); if (setPrevious) _previoustemp = temp; } uint8_t IRHitachiAc424::getFan(void) { return GETBITS8(remote_state[kHitachiAc424FanByte], kHighNibble, kNibbleSize); } void IRHitachiAc424::setFan(const uint8_t speed) { uint8_t newSpeed = std::max(speed, kHitachiAc424FanMin); uint8_t fanMax = kHitachiAc424FanMax; // Only 2 x low speeds in Dry mode or Auto if (getMode() == kHitachiAc424Dry && speed == kHitachiAc424FanAuto) { fanMax = kHitachiAc424FanAuto; } else if (getMode() == kHitachiAc424Dry) { fanMax = kHitachiAc424FanMaxDry; } else if (getMode() == kHitachiAc424Fan && speed == kHitachiAc424FanAuto) { // Fan Mode does not have auto. Set to safe low newSpeed = kHitachiAc424FanMin; } newSpeed = std::min(newSpeed, fanMax); // Handle the setting the button value if we are going to change the value. if (newSpeed != getFan()) setButton(kHitachiAc424ButtonFan); // Set the values setBits(&remote_state[kHitachiAc424FanByte], kHighNibble, kNibbleSize, newSpeed); remote_state[9] = 0x92; remote_state[29] = 0x00; // When fan is at min/max, additional bytes seem to be set if (newSpeed == kHitachiAc424FanMin) remote_state[9] = 0x98; if (newSpeed == kHitachiAc424FanMax) { remote_state[9] = 0xA9; remote_state[29] = 0x30; } } uint8_t IRHitachiAc424::getButton(void) { return remote_state[kHitachiAc424ButtonByte]; } // The remote sends the type of button pressed on send void IRHitachiAc424::setButton(const uint8_t button) { remote_state[kHitachiAc424ButtonByte] = button; } // The remote does not keep state of the vertical swing. // A byte is sent indicating the swing button is pressed on the remote void IRHitachiAc424::setSwingVToggle(const bool on) { uint8_t button = getButton(); // Get the current button value. if (on) button = kHitachiAc424ButtonSwingV; // Set the button to SwingV. else if (button == kHitachiAc424ButtonSwingV) // Asked to unset it // It was set previous, so use Power as a default button = kHitachiAc424ButtonPowerMode; setButton(button); } bool IRHitachiAc424::getSwingVToggle(void) { return getButton() == kHitachiAc424ButtonSwingV; } // Convert a standard A/C mode into its native mode. uint8_t IRHitachiAc424::convertMode(const stdAc::opmode_t mode) { switch (mode) { case stdAc::opmode_t::kCool: return kHitachiAc424Cool; case stdAc::opmode_t::kHeat: return kHitachiAc424Heat; case stdAc::opmode_t::kDry: return kHitachiAc424Dry; case stdAc::opmode_t::kFan: return kHitachiAc424Fan; default: return kHitachiAc424Cool; } } // Convert a standard A/C Fan speed into its native fan speed. uint8_t IRHitachiAc424::convertFan(const stdAc::fanspeed_t speed) { switch (speed) { case stdAc::fanspeed_t::kMin: return kHitachiAc424FanMin; case stdAc::fanspeed_t::kLow: return kHitachiAc424FanLow; case stdAc::fanspeed_t::kMedium: return kHitachiAc424FanMedium; case stdAc::fanspeed_t::kHigh: return kHitachiAc424FanHigh; case stdAc::fanspeed_t::kMax: return kHitachiAc424FanMax; default: return kHitachiAc424FanAuto; } } // Convert a native mode to it's common equivalent. stdAc::opmode_t IRHitachiAc424::toCommonMode(const uint8_t mode) { switch (mode) { case kHitachiAc424Cool: return stdAc::opmode_t::kCool; case kHitachiAc424Heat: return stdAc::opmode_t::kHeat; case kHitachiAc424Dry: return stdAc::opmode_t::kDry; case kHitachiAc424Fan: return stdAc::opmode_t::kFan; default: return stdAc::opmode_t::kCool; } } // Convert a native fan speed to it's common equivalent. stdAc::fanspeed_t IRHitachiAc424::toCommonFanSpeed(const uint8_t speed) { switch (speed) { case kHitachiAc424FanMax: return stdAc::fanspeed_t::kMax; case kHitachiAc424FanHigh: return stdAc::fanspeed_t::kHigh; case kHitachiAc424FanMedium: return stdAc::fanspeed_t::kMedium; case kHitachiAc424FanLow: return stdAc::fanspeed_t::kLow; case kHitachiAc424FanMin: return stdAc::fanspeed_t::kMin; default: return stdAc::fanspeed_t::kAuto; } } // Convert the A/C state to it's common equivalent. stdAc::state_t IRHitachiAc424::toCommon(void) { stdAc::state_t result; result.protocol = decode_type_t::HITACHI_AC424; result.model = -1; // No models used. result.power = this->getPower(); result.mode = this->toCommonMode(this->getMode()); result.celsius = true; result.degrees = this->getTemp(); result.fanspeed = this->toCommonFanSpeed(this->getFan()); result.swingv = this->getSwingVToggle() ? stdAc::swingv_t::kAuto : stdAc::swingv_t::kOff; // Not supported. result.swingh = stdAc::swingh_t::kOff; result.quiet = false; result.turbo = false; result.clean = false; result.econo = false; result.filter = false; result.light = false; result.beep = false; result.sleep = -1; result.clock = -1; return result; } // Convert the internal state into a human readable string. String IRHitachiAc424::toString(void) { String result = ""; result.reserve(100); // Reserve some heap for the string to reduce fragging. result += addBoolToString(getPower(), kPowerStr, false); result += addModeToString(getMode(), 0, kHitachiAc424Cool, kHitachiAc424Heat, kHitachiAc424Dry, kHitachiAc424Fan); result += addTempToString(getTemp()); result += addIntToString(getFan(), kFanStr); result += kSpaceLBraceStr; switch (getFan()) { case kHitachiAc424FanAuto: result += kAutoStr; break; case kHitachiAc424FanMax: result += kMaxStr; break; case kHitachiAc424FanHigh: result += kHighStr; break; case kHitachiAc424FanMedium: result += kMedStr; break; case kHitachiAc424FanLow: result += kLowStr; break; case kHitachiAc424FanMin: result += kMinStr; break; default: result += kUnknownStr; } result += ')'; result += addBoolToString(getSwingVToggle(), kSwingVToggleStr); result += addIntToString(getButton(), kButtonStr); result += kSpaceLBraceStr; switch (getButton()) { case kHitachiAc424ButtonPowerMode: result += kPowerStr; result += '/'; result += kModeStr; break; case kHitachiAc424ButtonFan: result += kFanStr; break; case kHitachiAc424ButtonSwingV: result += kSwingVStr; break; case kHitachiAc424ButtonTempDown: result += kTempDownStr; break; case kHitachiAc424ButtonTempUp: result += kTempUpStr; break; default: result += kUnknownStr; } result += ')'; return result; } #if SEND_HITACHI_AC3 // Send HITACHI_AC3 messages // // Note: This protocol is almost exactly the same as HitachiAC424 except this // variant has subtle timing differences. // There are five(5) typical sizes: // * kHitachiAc3MinStateLength (Cancel Timer) // * kHitachiAc3MinStateLength + 2 (Change Temp) // * kHitachiAc3StateLength - 6 (Change Mode) // * kHitachiAc3StateLength- 4 (Normal) // * kHitachiAc3StateLength (Set Timer) // // Args: // data: An array of bytes containing the IR command. // It is assumed to be in LSBF order for this code. // nbytes: Nr. of bytes of data in the array. // repeat: Nr. of times the message is to be repeated. // // Status: STABLE / Working fine. void IRsend::sendHitachiAc3(const uint8_t data[], const uint16_t nbytes, const uint16_t repeat) { // Header + Data + Footer sendGeneric(kHitachiAc3HdrMark, kHitachiAc3HdrSpace, kHitachiAc3BitMark, kHitachiAc3OneSpace, kHitachiAc3BitMark, kHitachiAc3ZeroSpace, kHitachiAc3BitMark, kHitachiAcMinGap, data, nbytes, // Bytes kHitachiAcFreq, false, repeat, kDutyDefault); } #endif // SEND_HITACHI_AC3 // Class for handling the remote control on a Hitachi_AC3 53 A/C message IRHitachiAc3::IRHitachiAc3(const uint16_t pin, const bool inverted, const bool use_modulation) : _irsend(pin, inverted, use_modulation) { stateReset(); } // Reset to auto fan, cooling, 23° Celcius void IRHitachiAc3::stateReset(void) { for (uint8_t i = 0; i < kHitachiAc3StateLength; i++) remote_state[i] = 0x00; remote_state[0] = 0x01; remote_state[1] = 0x10; remote_state[3] = 0x40; remote_state[5] = 0xFF; remote_state[7] = 0xE8; remote_state[9] = 0x89; remote_state[11] = 0x0B; remote_state[13] = 0x3F; remote_state[15] = 0x15; remote_state[21] = 0x4B; remote_state[23] = 0x18; setInvertedStates(); } void IRHitachiAc3::setInvertedStates(const uint16_t length) { for (uint8_t i = 3; i < length - 1; i += 2) remote_state[i + 1] = ~remote_state[i]; } bool IRHitachiAc3::hasInvertedStates(const uint8_t state[], const uint16_t length) { for (uint8_t i = 3; i < length - 1; i += 2) if ((state[i + 1] ^ state[i]) != 0xFF) return false; return true; } void IRHitachiAc3::begin(void) { _irsend.begin(); } uint8_t *IRHitachiAc3::getRaw(void) { setInvertedStates(); return remote_state; } void IRHitachiAc3::setRaw(const uint8_t new_code[], const uint16_t length) { memcpy(remote_state, new_code, std::min(length, kHitachiAc3StateLength)); } #if DECODE_HITACHI_AC3 // Decode the supplied HitachiAc3 A/C message. // // Note: This protocol is almost exactly the same as HitachiAC424 except this // variant has subtle timing differences and multiple lengths. // // Args: // results: Ptr to the data to decode and where to store the decode result. // offset: The starting index to use when attempting to decode the raw data. // Typically/Defaults to kStartOffset. // nbits: The number of data bits to expect. Typically kHitachiAc3Bits. // strict: Flag indicating if we should perform strict matching. // Returns: // boolean: True if it can decode it, false if it can't. // // Status: STABLE / Works fine. // // Supported devices: // Hitachi PC-LH3B // // Ref: // https://github.com/crankyoldgit/IRremoteESP8266/issues/1060 bool IRrecv::decodeHitachiAc3(decode_results *results, uint16_t offset, const uint16_t nbits, const bool strict) { if (results->rawlen < 2 * nbits + kHeader + kFooter - 1 + offset) return false; // Too short a message to match. if (strict) { // Check the requested bit length. switch (nbits) { case kHitachiAc3MinBits: // Cancel Timer (Min Size) case kHitachiAc3MinBits + 2 * 8: // Change Temp case kHitachiAc3Bits - 6 * 8: // Change Mode case kHitachiAc3Bits - 4 * 8: // Normal case kHitachiAc3Bits: // Set Temp (Max Size) break; default: return false; } } // Header + Data + Footer if (!matchGeneric(results->rawbuf + offset, results->state, results->rawlen - offset, nbits, kHitachiAc3HdrMark, kHitachiAc3HdrSpace, kHitachiAc3BitMark, kHitachiAc3OneSpace, kHitachiAc3BitMark, kHitachiAc3ZeroSpace, kHitachiAc3BitMark, kHitachiAcMinGap, true, kUseDefTol, 0, false)) return false; // We failed to find any data. // Compliance if (strict && !IRHitachiAc3::hasInvertedStates(results->state, nbits / 8)) return false; // Success results->decode_type = decode_type_t::HITACHI_AC3; results->bits = nbits; return true; } #endif // DECODE_HITACHI_AC3