// Copyright 2017 Ville Skyttä (scop) // Copyright 2017, 2018 David Conran // // Code to emulate Gree protocol compatible HVAC devices. // Should be compatible with: // * Heat pumps carrying the "Ultimate" brand name. // * EKOKAI air conditioners. // #include "ir_Gree.h" #include #include #ifndef ARDUINO #include #endif #include "IRrecv.h" #include "IRremoteESP8266.h" #include "IRsend.h" #include "IRtext.h" #include "IRutils.h" #include "ir_Kelvinator.h" // Constants // Ref: https://github.com/ToniA/arduino-heatpumpir/blob/master/GreeHeatpumpIR.h const uint16_t kGreeHdrMark = 9000; const uint16_t kGreeHdrSpace = 4500; // See #684 and real example in unit tests const uint16_t kGreeBitMark = 620; const uint16_t kGreeOneSpace = 1600; const uint16_t kGreeZeroSpace = 540; const uint16_t kGreeMsgSpace = 19000; const uint8_t kGreeBlockFooter = 0b010; const uint8_t kGreeBlockFooterBits = 3; 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_GREE // Send a Gree Heat Pump message. // // Args: // data: An array of bytes containing the IR command. // nbytes: Nr. of bytes of data in the array. (>=kGreeStateLength) // repeat: Nr. of times the message is to be repeated. (Default = 0). // // Status: ALPHA / Untested. // // Ref: // https://github.com/ToniA/arduino-heatpumpir/blob/master/GreeHeatpumpIR.cpp void IRsend::sendGree(const unsigned char data[], const uint16_t nbytes, const uint16_t repeat) { if (nbytes < kGreeStateLength) return; // Not enough bytes to send a proper message. for (uint16_t r = 0; r <= repeat; r++) { // Block #1 sendGeneric(kGreeHdrMark, kGreeHdrSpace, kGreeBitMark, kGreeOneSpace, kGreeBitMark, kGreeZeroSpace, 0, 0, // No Footer. data, 4, 38, false, 0, 50); // Footer #1 sendGeneric(0, 0, // No Header kGreeBitMark, kGreeOneSpace, kGreeBitMark, kGreeZeroSpace, kGreeBitMark, kGreeMsgSpace, 0b010, 3, 38, false, 0, 50); // Block #2 sendGeneric(0, 0, // No Header for Block #2 kGreeBitMark, kGreeOneSpace, kGreeBitMark, kGreeZeroSpace, kGreeBitMark, kGreeMsgSpace, data + 4, nbytes - 4, 38, false, 0, 50); } } // Send a Gree Heat Pump message. // // Args: // data: The raw message to be sent. // nbits: Nr. of bits of data in the message. (Default is kGreeBits) // repeat: Nr. of times the message is to be repeated. (Default = 0). // // Status: ALPHA / Untested. // // Ref: // https://github.com/ToniA/arduino-heatpumpir/blob/master/GreeHeatpumpIR.cpp void IRsend::sendGree(const uint64_t data, const uint16_t nbits, const uint16_t repeat) { if (nbits != kGreeBits) return; // Wrong nr. of bits to send a proper message. // Set IR carrier frequency enableIROut(38); for (uint16_t r = 0; r <= repeat; r++) { // Header mark(kGreeHdrMark); space(kGreeHdrSpace); // Data for (int16_t i = 8; i <= nbits; i += 8) { sendData(kGreeBitMark, kGreeOneSpace, kGreeBitMark, kGreeZeroSpace, (data >> (nbits - i)) & 0xFF, 8, false); if (i == nbits / 2) { // Send the mid-message Footer. sendData(kGreeBitMark, kGreeOneSpace, kGreeBitMark, kGreeZeroSpace, 0b010, 3); mark(kGreeBitMark); space(kGreeMsgSpace); } } // Footer mark(kGreeBitMark); space(kGreeMsgSpace); } } #endif // SEND_GREE IRGreeAC::IRGreeAC(const uint16_t pin, const gree_ac_remote_model_t model, const bool inverted, const bool use_modulation) : _irsend(pin, inverted, use_modulation) { stateReset(); setModel(model); } void IRGreeAC::stateReset(void) { // This resets to a known-good state to Power Off, Fan Auto, Mode Auto, 25C. for (uint8_t i = 0; i < kGreeStateLength; i++) remote_state[i] = 0x0; remote_state[1] = 0x09; remote_state[2] = 0x20; remote_state[3] = 0x50; remote_state[5] = 0x20; remote_state[7] = 0x50; } void IRGreeAC::fixup(void) { setPower(getPower()); // Redo the power bits as they differ between models. checksum(); // Calculate the checksums } void IRGreeAC::begin(void) { _irsend.begin(); } #if SEND_GREE void IRGreeAC::send(const uint16_t repeat) { fixup(); // Ensure correct settings before sending. _irsend.sendGree(remote_state, kGreeStateLength, repeat); } #endif // SEND_GREE uint8_t* IRGreeAC::getRaw(void) { fixup(); // Ensure correct settings before sending. return remote_state; } void IRGreeAC::setRaw(const uint8_t new_code[]) { memcpy(remote_state, new_code, kGreeStateLength); // We can only detect the difference between models when the power is on. if (getPower()) { if (GETBIT8(remote_state[2], kGreePower2Offset)) _model = gree_ac_remote_model_t::YAW1F; else _model = gree_ac_remote_model_t::YBOFB; } } void IRGreeAC::checksum(const uint16_t length) { // Gree uses the same checksum alg. as Kelvinator's block checksum. setBits(&remote_state[length - 1], kHighNibble, kNibbleSize, IRKelvinatorAC::calcBlockChecksum(remote_state, length)); } // Verify the checksum is valid for a given state. // Args: // state: The array to verify the checksum of. // length: The size of the state. // Returns: // A boolean. bool IRGreeAC::validChecksum(const uint8_t state[], const uint16_t length) { // Top 4 bits of the last byte in the state is the state's checksum. return GETBITS8(state[length - 1], kHighNibble, kNibbleSize) == IRKelvinatorAC::calcBlockChecksum(state, length); } void IRGreeAC::setModel(const gree_ac_remote_model_t model) { switch (model) { case gree_ac_remote_model_t::YAW1F: case gree_ac_remote_model_t::YBOFB: _model = model; break; default: setModel(gree_ac_remote_model_t::YAW1F); } } gree_ac_remote_model_t IRGreeAC::getModel(void) { return _model; } void IRGreeAC::on(void) { setPower(true); } void IRGreeAC::off(void) { setPower(false); } void IRGreeAC::setPower(const bool on) { setBit(&remote_state[0], kGreePower1Offset, on); // May not be needed. See #814 setBit(&remote_state[2], kGreePower2Offset, on && _model != gree_ac_remote_model_t::YBOFB); } bool IRGreeAC::getPower(void) { // See #814. Not checking/requiring: (remote_state[2] & kGreePower2Mask) return GETBIT8(remote_state[0], kGreePower1Offset); } // Set the temp. in deg C void IRGreeAC::setTemp(const uint8_t temp) { uint8_t new_temp = std::max((uint8_t)kGreeMinTemp, temp); new_temp = std::min((uint8_t)kGreeMaxTemp, new_temp); if (getMode() == kGreeAuto) new_temp = 25; setBits(&remote_state[1], kLowNibble, kGreeTempSize, new_temp - kGreeMinTemp); } // Return the set temp. in deg C uint8_t IRGreeAC::getTemp(void) { return GETBITS8(remote_state[1], kLowNibble, kGreeTempSize) + kGreeMinTemp; } // Set the speed of the fan, 0-3, 0 is auto, 1-3 is the speed void IRGreeAC::setFan(const uint8_t speed) { uint8_t fan = std::min((uint8_t)kGreeFanMax, speed); // Bounds check if (getMode() == kGreeDry) fan = 1; // DRY mode is always locked to fan 1. // Set the basic fan values. setBits(&remote_state[0], kGreeFanOffset, kGreeFanSize, fan); } uint8_t IRGreeAC::getFan(void) { return GETBITS8(remote_state[0], kGreeFanOffset, kGreeFanSize); } void IRGreeAC::setMode(const uint8_t new_mode) { uint8_t mode = new_mode; switch (mode) { // AUTO is locked to 25C case kGreeAuto: setTemp(25); break; // DRY always sets the fan to 1. case kGreeDry: setFan(1); break; case kGreeCool: case kGreeFan: case kGreeHeat: break; // If we get an unexpected mode, default to AUTO. default: mode = kGreeAuto; } setBits(&remote_state[0], kLowNibble, kModeBitsSize, mode); } uint8_t IRGreeAC::getMode(void) { return GETBITS8(remote_state[0], kLowNibble, kModeBitsSize); } void IRGreeAC::setLight(const bool on) { setBit(&remote_state[2], kGreeLightOffset, on); } bool IRGreeAC::getLight(void) { return GETBIT8(remote_state[2], kGreeLightOffset); } void IRGreeAC::setIFeel(const bool on) { setBit(&remote_state[5], kGreeIFeelOffset, on); } bool IRGreeAC::getIFeel(void) { return GETBIT8(remote_state[5], kGreeIFeelOffset); } void IRGreeAC::setWiFi(const bool on) { setBit(&remote_state[5], kGreeWiFiOffset, on); } bool IRGreeAC::getWiFi(void) { return GETBIT8(remote_state[5], kGreeWiFiOffset); } void IRGreeAC::setXFan(const bool on) { setBit(&remote_state[2], kGreeXfanOffset, on); } bool IRGreeAC::getXFan(void) { return GETBIT8(remote_state[2], kGreeXfanOffset); } void IRGreeAC::setSleep(const bool on) { setBit(&remote_state[0], kGreeSleepOffset, on); } bool IRGreeAC::getSleep(void) { return GETBIT8(remote_state[0], kGreeSleepOffset); } void IRGreeAC::setTurbo(const bool on) { setBit(&remote_state[2], kGreeTurboOffset, on); } bool IRGreeAC::getTurbo(void) { return GETBIT8(remote_state[2], kGreeTurboOffset); } void IRGreeAC::setSwingVertical(const bool automatic, const uint8_t position) { setBit(&remote_state[0], kGreeSwingAutoOffset, automatic); uint8_t new_position = position; if (!automatic) { switch (position) { case kGreeSwingUp: case kGreeSwingMiddleUp: case kGreeSwingMiddle: case kGreeSwingMiddleDown: case kGreeSwingDown: break; default: new_position = kGreeSwingLastPos; } } else { switch (position) { case kGreeSwingAuto: case kGreeSwingDownAuto: case kGreeSwingMiddleAuto: case kGreeSwingUpAuto: break; default: new_position = kGreeSwingAuto; } } setBits(&remote_state[4], kLowNibble, kGreeSwingSize, new_position); } bool IRGreeAC::getSwingVerticalAuto(void) { return GETBIT8(remote_state[0], kGreeSwingAutoOffset); } uint8_t IRGreeAC::getSwingVerticalPosition(void) { return GETBITS8(remote_state[4], kLowNibble, kGreeSwingSize); } void IRGreeAC::setTimerEnabled(const bool on) { setBit(&remote_state[1], kGreeTimerEnabledOffset, on); } bool IRGreeAC::getTimerEnabled(void) { return GETBIT8(remote_state[1], kGreeTimerEnabledOffset); } // Returns the number of minutes the timer is set for. uint16_t IRGreeAC::getTimer(void) { uint16_t hrs = irutils::bcdToUint8( (GETBITS8(remote_state[1], kGreeTimerTensHrOffset, kGreeTimerTensHrSize) << kNibbleSize) | GETBITS8(remote_state[2], kGreeTimerHoursOffset, kGreeTimerHoursSize)); return hrs * 60 + (GETBIT8(remote_state[1], kGreeTimerHalfHrOffset) ? 30 : 0); } // Set the A/C's timer to turn off in X many minutes. // Stores time internally in 30 min units. // e.g. 5 mins means 0 (& Off), 95 mins is 90 mins (& On). Max is 24 hours. // // Args: // minutes: The number of minutes the timer should be set for. void IRGreeAC::setTimer(const uint16_t minutes) { uint16_t mins = std::min(kGreeTimerMax, minutes); // Bounds check. setTimerEnabled(mins >= 30); // Timer is enabled when >= 30 mins. uint8_t hours = mins / 60; // Set the half hour bit. setBit(&remote_state[1], kGreeTimerHalfHrOffset, !((mins % 60) < 30)); // Set the "tens" digit of hours. setBits(&remote_state[1], kGreeTimerTensHrOffset, kGreeTimerTensHrSize, hours / 10); // Set the "units" digit of hours. setBits(&remote_state[2], kGreeTimerHoursOffset, kGreeTimerHoursSize, hours % 10); } // Convert a standard A/C mode into its native mode. uint8_t IRGreeAC::convertMode(const stdAc::opmode_t mode) { switch (mode) { case stdAc::opmode_t::kCool: return kGreeCool; case stdAc::opmode_t::kHeat: return kGreeHeat; case stdAc::opmode_t::kDry: return kGreeDry; case stdAc::opmode_t::kFan: return kGreeFan; default: return kGreeAuto; } } // Convert a standard A/C Fan speed into its native fan speed. uint8_t IRGreeAC::convertFan(const stdAc::fanspeed_t speed) { switch (speed) { case stdAc::fanspeed_t::kMin: return kGreeFanMin; case stdAc::fanspeed_t::kLow: case stdAc::fanspeed_t::kMedium: return kGreeFanMax - 1; case stdAc::fanspeed_t::kHigh: case stdAc::fanspeed_t::kMax: return kGreeFanMax; default: return kGreeFanAuto; } } // Convert a standard A/C Vertical Swing into its native version. uint8_t IRGreeAC::convertSwingV(const stdAc::swingv_t swingv) { switch (swingv) { case stdAc::swingv_t::kHighest: return kGreeSwingUp; case stdAc::swingv_t::kHigh: return kGreeSwingMiddleUp; case stdAc::swingv_t::kMiddle: return kGreeSwingMiddle; case stdAc::swingv_t::kLow: return kGreeSwingMiddleDown; case stdAc::swingv_t::kLowest: return kGreeSwingDown; default: return kGreeSwingAuto; } } // Convert a native mode to it's common equivalent. stdAc::opmode_t IRGreeAC::toCommonMode(const uint8_t mode) { switch (mode) { case kGreeCool: return stdAc::opmode_t::kCool; case kGreeHeat: return stdAc::opmode_t::kHeat; case kGreeDry: return stdAc::opmode_t::kDry; case kGreeFan: return stdAc::opmode_t::kFan; default: return stdAc::opmode_t::kAuto; } } // Convert a native fan speed to it's common equivalent. stdAc::fanspeed_t IRGreeAC::toCommonFanSpeed(const uint8_t speed) { switch (speed) { case kGreeFanMax: return stdAc::fanspeed_t::kMax; case kGreeFanMax - 1: return stdAc::fanspeed_t::kMedium; case kGreeFanMin: return stdAc::fanspeed_t::kMin; default: return stdAc::fanspeed_t::kAuto; } } // Convert a native vertical swing to it's common equivalent. stdAc::swingv_t IRGreeAC::toCommonSwingV(const uint8_t pos) { switch (pos) { case kGreeSwingUp: return stdAc::swingv_t::kHighest; case kGreeSwingMiddleUp: return stdAc::swingv_t::kHigh; case kGreeSwingMiddle: return stdAc::swingv_t::kMiddle; case kGreeSwingMiddleDown: return stdAc::swingv_t::kLow; case kGreeSwingDown: return stdAc::swingv_t::kLowest; default: return stdAc::swingv_t::kAuto; } } // Convert the A/C state to it's common equivalent. stdAc::state_t IRGreeAC::toCommon(void) { stdAc::state_t result; result.protocol = decode_type_t::GREE; 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()); if (this->getSwingVerticalAuto()) result.swingv = stdAc::swingv_t::kAuto; else result.swingv = this->toCommonSwingV(this->getSwingVerticalPosition()); result.turbo = this->getTurbo(); result.light = this->getLight(); result.clean = this->getXFan(); result.sleep = this->getSleep() ? 0 : -1; // Not supported. result.swingh = stdAc::swingh_t::kOff; result.quiet = false; result.econo = false; result.filter = false; result.beep = false; result.clock = -1; return result; } // Convert the internal state into a human readable string. String IRGreeAC::toString(void) { String result = ""; result.reserve(150); // Reserve some heap for the string to reduce fragging. result += addModelToString(decode_type_t::GREE, getModel(), false); result += addBoolToString(getPower(), kPowerStr); result += addModeToString(getMode(), kGreeAuto, kGreeCool, kGreeHeat, kGreeDry, kGreeFan); result += addTempToString(getTemp()); result += addFanToString(getFan(), kGreeFanMax, kGreeFanMin, kGreeFanAuto, kGreeFanAuto, kGreeFanMed); result += addBoolToString(getTurbo(), kTurboStr); result += addBoolToString(getIFeel(), kIFeelStr); result += addBoolToString(getWiFi(), kWifiStr); result += addBoolToString(getXFan(), kXFanStr); result += addBoolToString(getLight(), kLightStr); result += addBoolToString(getSleep(), kSleepStr); result += addLabeledString(getSwingVerticalAuto() ? kAutoStr : kManualStr, kSwingVModeStr); result += addIntToString(getSwingVerticalPosition(), kSwingVStr); result += kSpaceLBraceStr; switch (getSwingVerticalPosition()) { case kGreeSwingLastPos: result += kLastStr; break; case kGreeSwingAuto: result += kAutoStr; break; default: result += kUnknownStr; } result += ')'; result += addLabeledString( getTimerEnabled() ? minsToString(getTimer()) : kOffStr, kTimerStr); return result; } #if DECODE_GREE // Decode the supplied Gree 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 kGreeBits. // 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. bool IRrecv::decodeGree(decode_results* results, uint16_t nbits, bool strict) { if (results->rawlen < 2 * (nbits + kGreeBlockFooterBits) + (kHeader + kFooter + 1)) return false; // Can't possibly be a valid Gree message. if (strict && nbits != kGreeBits) return false; // Not strictly a Gree message. uint16_t offset = kStartOffset; // There are two blocks back-to-back in a full Gree IR message // sequence. uint16_t used; // Header + Data Block #1 (32 bits) used = matchGeneric(results->rawbuf + offset, results->state, results->rawlen - offset, nbits / 2, kGreeHdrMark, kGreeHdrSpace, kGreeBitMark, kGreeOneSpace, kGreeBitMark, kGreeZeroSpace, 0, 0, false, _tolerance, kMarkExcess, false); if (used == 0) return false; offset += used; // Block #1 footer (3 bits, B010) match_result_t data_result; data_result = matchData(&(results->rawbuf[offset]), kGreeBlockFooterBits, kGreeBitMark, kGreeOneSpace, kGreeBitMark, kGreeZeroSpace, _tolerance, kMarkExcess, false); if (data_result.success == false) return false; if (data_result.data != kGreeBlockFooter) return false; offset += data_result.used; // Inter-block gap + Data Block #2 (32 bits) + Footer if (!matchGeneric(results->rawbuf + offset, results->state + 4, results->rawlen - offset, nbits / 2, kGreeBitMark, kGreeMsgSpace, kGreeBitMark, kGreeOneSpace, kGreeBitMark, kGreeZeroSpace, kGreeBitMark, kGreeMsgSpace, true, _tolerance, kMarkExcess, false)) return false; // Compliance if (strict) { // Verify the message's checksum is correct. if (!IRGreeAC::validChecksum(results->state)) return false; } // Success results->decode_type = GREE; 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_GREE