// Copyright 2018 David Conran // // Code to emulate Hitachi protocol compatible devices. // Should be compatible with: // * Hitachi RAS-35THA6 remote // #include "ir_Hitachi.h" #include #ifndef ARDUINO #include #endif #include "IRrecv.h" #include "IRremoteESP8266.h" #include "IRsend.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. using irutils::addBoolToString; using irutils::addIntToString; using irutils::addLabeledString; using irutils::addModeToString; using irutils::addFanToString; using irutils::addTempToString; #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: ALPHA / Untested. // // 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: BETA / Appears to work. // // 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, 38, true, repeat, 50); } #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: BETA / Appears 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) { int8_t sum = 62; for (uint16_t i = 0; i < length - 1; i++) sum -= reverseBits(state[i], 8); return reverseBits((uint8_t)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) { for (uint8_t i = 0; i < length && i < kHitachiAcStateLength; i++) remote_state[i] = new_code[i]; } #if SEND_HITACHI_AC void IRHitachiAc::send(const uint16_t repeat) { checksum(); _irsend.sendHitachiAC(remote_state, kHitachiAcStateLength, repeat); } #endif // SEND_HITACHI_AC bool IRHitachiAc::getPower(void) { return (remote_state[17] & 0x01); } void IRHitachiAc::setPower(const bool on) { if (on) remote_state[17] |= 0x01; else remote_state[17] &= 0xFE; } 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) { case kHitachiAcFan: // Fan mode sets a special temp. 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 remote_state[14] & 0x80; } void IRHitachiAc::setSwingVertical(const bool on) { if (on) remote_state[14] |= 0x80; else remote_state[14] &= 0x7F; } bool IRHitachiAc::getSwingHorizontal(void) { return remote_state[15] & 0x80; } void IRHitachiAc::setSwingHorizontal(const bool on) { if (on) remote_state[15] |= 0x80; else remote_state[15] &= 0x7F; } // 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(), F("Power"), false); result += addModeToString(getMode(), kHitachiAcAuto, kHitachiAcCool, kHitachiAcHeat, kHitachiAcDry, kHitachiAcFan); result += addTempToString(getTemp()); result += addFanToString(getFan(), kHitachiAcFanHigh, kHitachiAcFanLow, kHitachiAcFanAuto, kHitachiAcFanAuto, kHitachiAcFanMed); result += addBoolToString(getSwingVertical(), F("Swing (Vertical)")); result += addBoolToString(getSwingHorizontal(), F("Swing (Horizontal)")); 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. // 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: ALPHA / Untested. // // 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, const uint16_t nbits, const bool strict) { const uint8_t kTolerance = 30; if (results->rawlen < 2 * nbits + kHeader + kFooter - 1) return false; // Can't possibly be a valid HitachiAC message. if (strict) { switch (nbits) { case kHitachiAcBits: case kHitachiAc1Bits: case kHitachiAc2Bits: break; // Okay to continue. default: return false; // Not strictly a Hitachi message. } } uint16_t offset = kStartOffset; 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, kTolerance)) return false; // Compliance if (strict) { if (nbits / 8 == kHitachiAcStateLength && !IRHitachiAc::validChecksum(results->state, kHitachiAcStateLength)) 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)