diff --git a/.github/Contributors.md b/.github/Contributors.md index 811bfea84..afa0a51e9 100644 --- a/.github/Contributors.md +++ b/.github/Contributors.md @@ -18,6 +18,7 @@ - [Mark Kuchel](https://github.com/kuchel77) - [Christian Nilsson](https://github.com/NiKiZe) - [Zhongxian Li](https://github.com/siriuslzx) +- [Davide Depau](https://github.com/Depau) All contributors can be found on the [contributors site](https://github.com/crankyoldgit/IRremoteESP8266/graphs/contributors). diff --git a/src/IRac.cpp b/src/IRac.cpp index 97fd695e3..4988b67d7 100644 --- a/src/IRac.cpp +++ b/src/IRac.cpp @@ -28,6 +28,7 @@ #include "ir_Fujitsu.h" #include "ir_Haier.h" #include "ir_Hitachi.h" +#include "ir_Kelon.h" #include "ir_Kelvinator.h" #include "ir_LG.h" #include "ir_Midea.h" @@ -217,6 +218,9 @@ bool IRac::isProtocolSupported(const decode_type_t protocol) { #if SEND_HITACHI_AC424 case decode_type_t::HITACHI_AC424: #endif +#if SEND_KELON + case decode_type_t::KELON: +#endif #if SEND_KELVINATOR case decode_type_t::KELVINATOR: #endif @@ -1250,6 +1254,37 @@ void IRac::hitachi424(IRHitachiAc424 *ac, } #endif // SEND_HITACHI_AC424 +#if SEND_KELON +/// Send a Kelon A/C message with the supplied settings. +/// @param[in, out] ac A Ptr to an IRKelonAc object to use. +/// @param[in] togglePower Whether to toggle the unit's power +/// @param[in] mode The operation mode setting. +/// @param[in] dryGrade The dehumidification intensity grade +/// @param[in] degrees The temperature setting in degrees. +/// @param[in] fan The speed setting for the fan. +/// @param[in] toggleSwing Whether to toggle the swing setting +/// @param[in] superCool Run the device in Super cooling mode. +/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, >= 0 is on +void IRac::kelon(IRKelonAc *ac, const bool togglePower, + const stdAc::opmode_t mode, const int8_t dryGrade, + const float degrees, const stdAc::fanspeed_t fan, + const bool toggleSwing, const bool superCool, + const int16_t sleep) { + ac->begin(); + ac->setMode(IRKelonAc::convertMode(mode)); + ac->setFan(IRKelonAc::convertFan(fan)); + ac->setTemp(static_cast(degrees)); + ac->setSleep(sleep >= 0); + ac->setSupercool(superCool); + ac->setDryGrade(dryGrade); + + ac->setTogglePower(togglePower); + ac->setToggleSwingVertical(toggleSwing); + + ac->send(); +} +#endif // SEND_KELON + #if SEND_KELVINATOR /// Send a Kelvinator A/C message with the supplied settings. /// @param[in, out] ac A Ptr to an IRKelvinatorAC object to use. @@ -2251,6 +2286,13 @@ stdAc::state_t IRac::handleToggles(const stdAc::state_t desired, else result.swingv = stdAc::swingv_t::kOff; // No change, so no toggle. break; + case decode_type_t::KELON: + if ((desired.swingv == stdAc::swingv_t::kOff) ^ + (prev->swingv == stdAc::swingv_t::kOff)) // It changed, so toggle. + result.swingv = stdAc::swingv_t::kAuto; + else + result.swingv = stdAc::swingv_t::kOff; // No change, so no toggle. + // FALL-THRU case decode_type_t::AIRWELL: case decode_type_t::DAIKIN64: case decode_type_t::PANASONIC_AC32: @@ -2572,6 +2614,14 @@ bool IRac::sendAc(const stdAc::state_t desired, const stdAc::state_t *prev) { break; } #endif // SEND_HITACHI_AC424 +#if SEND_KELON + case KELON: { + IRKelonAc ac(_pin, _inverted, _modulation); + kelon(&ac, send.power, send.mode, 0, send.degrees, send.fanspeed, + send.swingv != stdAc::swingv_t::kOff, send.turbo, send.sleep); + break; + } +#endif #if SEND_KELVINATOR case KELVINATOR: { @@ -3281,6 +3331,13 @@ namespace IRAcUtils { return ac.toString(); } #endif // DECODE_FUJITSU_AC +#if DECODE_KELON + case decode_type_t::KELON: { + IRKelonAc ac(kGpioUnused); + ac.setRaw(result->value); + return ac.toString(); + } +#endif // DECODE_KELON #if DECODE_KELVINATOR case decode_type_t::KELVINATOR: { IRKelvinatorAC ac(kGpioUnused); @@ -3766,6 +3823,14 @@ namespace IRAcUtils { break; } #endif // DECODE_HITACHI_AC424 +#if DECODE_KELON + case decode_type_t::KELON: { + IRKelonAc ac(kGpioUnused); + ac.setRaw(decode->value); + *result = ac.toCommon(); + break; + } +#endif // DECODE_KELON #if DECODE_KELVINATOR case decode_type_t::KELVINATOR: { IRKelvinatorAC ac(kGpioUnused); diff --git a/src/IRac.h b/src/IRac.h index 9c62f9198..8f765ccbe 100644 --- a/src/IRac.h +++ b/src/IRac.h @@ -22,6 +22,7 @@ #include "ir_Gree.h" #include "ir_Haier.h" #include "ir_Hitachi.h" +#include "ir_Kelon.h" #include "ir_Kelvinator.h" #include "ir_LG.h" #include "ir_Midea.h" @@ -289,6 +290,12 @@ void electra(IRElectraAc *ac, const float degrees, const stdAc::fanspeed_t fan, const stdAc::swingv_t swingv); #endif // SEND_HITACHI_AC424 +#if SEND_KELON + void kelon(IRKelonAc *ac, const bool togglePower, const stdAc::opmode_t mode, + const int8_t dryGrade, const float degrees, + const stdAc::fanspeed_t fan, const bool toggleSwing, + const bool superCool, const int16_t sleep); +#endif // SEND_KELON #if SEND_KELVINATOR void kelvinator(IRKelvinatorAC *ac, const bool on, const stdAc::opmode_t mode, diff --git a/src/IRrecv.cpp b/src/IRrecv.cpp index d58fce295..4a6281b45 100644 --- a/src/IRrecv.cpp +++ b/src/IRrecv.cpp @@ -1018,6 +1018,10 @@ bool IRrecv::decode(decode_results *results, irparams_t *save, DPRINTLN("Attempting Teknopoint decode"); if (decodeTeknopoint(results, offset)) return true; #endif // DECODE_TEKNOPOINT +#if DECODE_KELON + DPRINTLN("Attempting Kelon decode"); + if (decodeKelon(results, offset)) return true; +#endif // DECODE_KELON // Typically new protocols are added above this line. } #if DECODE_HASH diff --git a/src/IRrecv.h b/src/IRrecv.h index de9794939..ed204cbb0 100644 --- a/src/IRrecv.h +++ b/src/IRrecv.h @@ -744,6 +744,10 @@ class IRrecv { const uint16_t nbits = kTeknopointBits, const bool strict = true); #endif // DECODE_TEKNOPOINT +#if DECODE_KELON + bool decodeKelon(decode_results *results, uint16_t offset = kStartOffset, + const uint16_t nbits = kKelonBits, const bool strict = true); +#endif // DECODE_KELON }; #endif // IRRECV_H_ diff --git a/src/IRremoteESP8266.h b/src/IRremoteESP8266.h index 8661f8b3f..da389fccf 100644 --- a/src/IRremoteESP8266.h +++ b/src/IRremoteESP8266.h @@ -37,6 +37,7 @@ * Vestel AC code by Erdem U. Altınyurt * Teco AC code by Fabien Valthier (hcoohb) * Mitsubishi 112 AC Code by kuchel77 + * Kelon AC code by Davide Depau (Depau) * * GPL license, all text above must be included in any redistribution ****************************************************/ @@ -761,6 +762,13 @@ #define SEND_TEKNOPOINT _IR_ENABLE_DEFAULT_ #endif // SEND_TEKNOPOINT +#ifndef DECODE_KELON +#define DECODE_KELON _IR_ENABLE_DEFAULT_ +#endif // DECODE_KELON +#ifndef SEND_KELON +#define SEND_KELON _IR_ENABLE_DEFAULT_ +#endif // SEND_KELON + #if (DECODE_ARGO || DECODE_DAIKIN || DECODE_FUJITSU_AC || DECODE_GREE || \ DECODE_KELVINATOR || DECODE_MITSUBISHI_AC || DECODE_TOSHIBA_AC || \ DECODE_TROTEC || DECODE_HAIER_AC || DECODE_HITACHI_AC || \ @@ -774,7 +782,7 @@ DECODE_MITSUBISHI112 || DECODE_HITACHI_AC424 || DECODE_HITACHI_AC3 || \ DECODE_HITACHI_AC344 || DECODE_CORONA_AC || DECODE_SANYO_AC || \ DECODE_VOLTAS || DECODE_MIRAGE || DECODE_HAIER_AC176 || \ - DECODE_TEKNOPOINT || \ + DECODE_TEKNOPOINT || DECODE_KELON || \ false) // Add any DECODE to the above if it uses result->state (see kStateSizeMax) // you might also want to add the protocol to hasACState function @@ -917,8 +925,9 @@ enum decode_type_t { TRUMA, // 100 HAIER_AC176, TEKNOPOINT, + KELON, // Add new entries before this one, and update it to point to the last entry. - kLastDecodeType = TEKNOPOINT, + kLastDecodeType = KELON, }; // Message lengths & required repeat values @@ -1031,6 +1040,7 @@ const uint16_t kHitachiAc424Bits = kHitachiAc424StateLength * 8; const uint16_t kInaxBits = 24; const uint16_t kInaxMinRepeat = kSingleRepeat; const uint16_t kJvcBits = 16; +const uint16_t kKelonBits = 48; const uint16_t kKelvinatorStateLength = 16; const uint16_t kKelvinatorBits = kKelvinatorStateLength * 8; const uint16_t kKelvinatorDefaultRepeat = kNoRepeat; diff --git a/src/IRsend.cpp b/src/IRsend.cpp index c7ba7daa7..f552000aa 100644 --- a/src/IRsend.cpp +++ b/src/IRsend.cpp @@ -658,6 +658,7 @@ uint16_t IRsend::defaultBits(const decode_type_t protocol) { case SANYO_LC7461: return kSanyoLC7461Bits; // 42 case GOODWEATHER: + case KELON: case MIDEA: case PANASONIC: return 48; @@ -869,6 +870,11 @@ bool IRsend::send(const decode_type_t type, const uint64_t data, sendJVC(data, nbits, min_repeat); break; #endif +#if SEND_KELON + case KELON: + sendKelon(data, nbits, min_repeat); + break; +#endif // SEND_KELON #if SEND_LASERTAG case LASERTAG: sendLasertag(data, nbits, min_repeat); diff --git a/src/IRsend.h b/src/IRsend.h index 2799924fd..a936da0f7 100644 --- a/src/IRsend.h +++ b/src/IRsend.h @@ -717,6 +717,10 @@ class IRsend { const uint16_t nbytes = kTeknopointStateLength, const uint16_t repeat = kNoRepeat); #endif // SEND_TEKNOPOINT +#if SEND_KELON + void sendKelon(const uint64_t data, const uint16_t nbits = kKelonBits, + const uint16_t repeat = kNoRepeat); +#endif // SEND_KELON protected: #ifdef UNIT_TEST diff --git a/src/IRtext.cpp b/src/IRtext.cpp index cbd03c434..91233ad80 100644 --- a/src/IRtext.cpp +++ b/src/IRtext.cpp @@ -287,5 +287,6 @@ const PROGMEM char *kAllProtocolNamesStr = D_STR_TRUMA "\x0" D_STR_HAIER_AC176 "\x0" D_STR_TEKNOPOINT "\x0" + D_STR_KELON "\x0" ///< New protocol strings should be added just above this line. "\x0"; ///< This string requires double null termination. diff --git a/src/IRutils.cpp b/src/IRutils.cpp index bf35d6cbf..9f6cee1be 100644 --- a/src/IRutils.cpp +++ b/src/IRutils.cpp @@ -67,6 +67,18 @@ String uint64ToString(uint64_t input, uint8_t base) { return result; } +/// Convert a int64_t (signed long long) to a string. +/// Arduino String/toInt/Serial.print() can't handle printing 64 bit values. +/// @param[in] input The value to print +/// @param[in] base The output base. +/// @returns A String representation of the integer. +String int64ToString(int64_t input, uint8_t base) { + if (input < 0) { + return "-" + uint64ToString(-input, base); + } + return uint64ToString(input, base); +} + #ifdef ARDUINO /// Print a uint64_t/unsigned long long to the Serial port /// Serial.print() can't handle printing long longs. (uint64_t) @@ -496,6 +508,19 @@ namespace irutils { return addLabeledString(uint64ToString(value), label, precomma); } + /// Create a String with a colon separated labeled Integer suitable for + /// Humans. + /// e.g. "Foo: 23" + /// @param[in] value The value to come after the label. + /// @param[in] label The label to precede the value. + /// @param[in] precomma Should the output string start with ", " or not? + /// @return The resulting String. + String addSignedIntToString(const int16_t value, const String label, + const bool precomma) { + return addLabeledString(int64ToString(value), label, precomma); + } + + /// Generate the model string for a given Protocol/Model pair. /// @param[in] protocol The IR protocol. /// @param[in] model The model number for that protocol. diff --git a/src/IRutils.h b/src/IRutils.h index 8ce04b814..fdda6d7ae 100644 --- a/src/IRutils.h +++ b/src/IRutils.h @@ -20,6 +20,7 @@ const uint8_t kHighNibble = 4; const uint8_t kModeBitsSize = 3; uint64_t reverseBits(uint64_t input, uint16_t nbits); String uint64ToString(uint64_t input, uint8_t base = 10); +String int64ToString(int64_t input, uint8_t base = 10); String typeToString(const decode_type_t protocol, const bool isRepeat = false); void serialPrintUint64(uint64_t input, uint8_t base = 10); @@ -49,6 +50,8 @@ namespace irutils { const bool precomma = true); String addIntToString(const uint16_t value, const String label, const bool precomma = true); + String addSignedIntToString(const int16_t value, const String label, + const bool precomma = true); String modelToStr(const decode_type_t protocol, const int16_t model); String addModelToString(const decode_type_t protocol, const int16_t model, const bool precomma = true); diff --git a/src/ir_Kelon.cpp b/src/ir_Kelon.cpp new file mode 100644 index 000000000..39b61744e --- /dev/null +++ b/src/ir_Kelon.cpp @@ -0,0 +1,502 @@ +// Copyright 2021 Davide Depau + +/// @file +/// @brief Support for Kelan AC protocol. +/// Both sending and decoding should be functional for models of series +/// KELON ON/OFF 9000-12000. +/// All features of the standard remote are implemented. +/// +/// @note Unsupported: +/// - Explicit on/off due to AC unit limitations +/// - Explicit swing position due to AC unit limitations +/// - Fahrenheit. + +#include + +#include "ir_Kelon.h" + +#include "IRrecv.h" +#include "IRsend.h" +#include "IRutils.h" +#include "IRtext.h" + + +using irutils::addBoolToString; +using irutils::addIntToString; +using irutils::addSignedIntToString; +using irutils::addModeToString; +using irutils::addFanToString; +using irutils::addTempToString; +using irutils::addLabeledString; +using irutils::minsToString; + +// Constants +const uint16_t kKelonHdrMark = 9000; +const uint16_t kKelonHdrSpace = 4600; +const uint16_t kKelonBitMark = 560; +const uint16_t kKelonOneSpace = 1680; +const uint16_t kKelonZeroSpace = 600; +const uint32_t kKelonGap = 2 * kDefaultMessageGap; +const uint16_t kKelonFreq = 38000; + +#if SEND_KELON + +/// Send a Kelon message. +/// Status: STABLE / Working. +/// @param[in] data The data to be transmitted. +/// @param[in] nbits Nr. of bits of data to be sent. +/// @param[in] repeat The number of times the command is to be repeated. +void IRsend::sendKelon(const uint64_t data, const uint16_t nbits, + const uint16_t repeat) { + sendGeneric(kKelonHdrMark, kKelonHdrSpace, + kKelonBitMark, kKelonOneSpace, + kKelonBitMark, kKelonZeroSpace, + kKelonBitMark, kKelonGap, + data, nbits, kKelonFreq, false, // LSB First. + repeat, 50); +} + +#endif // SEND_KELON + +#if DECODE_KELON +/// Decode the supplied Kelon message. +/// Status: STABLE / Working. +/// @param[in,out] results Ptr to the data to decode & where to store the result +/// @param[in] offset The starting index to use when attempting to decode the +/// raw data. Typically/Defaults to kStartOffset. +/// @param[in] nbits The number of data bits to expect. +/// @param[in] strict Flag indicating if we should perform strict matching. +/// @return True if it can decode it, false if it can't. + +bool IRrecv::decodeKelon(decode_results *results, uint16_t offset, + const uint16_t nbits, const bool strict) { + if (strict && nbits != kKelonBits) { + return false; + } + if (!matchGeneric(results->rawbuf + offset, results->state, + results->rawlen - offset, nbits, + kKelonHdrMark, kKelonHdrSpace, + kKelonBitMark, kKelonOneSpace, + kKelonBitMark, kKelonZeroSpace, + kKelonBitMark, 0, false, + _tolerance, 0, false)) { + return false; + } + + results->decode_type = decode_type_t::KELON; + results->bits = nbits; + return true; +} + +#endif // DECODE_KELON + +/// Class constructor +/// @param[in] pin GPIO to be used when sending. +/// @param[in] inverted Is the output signal to be inverted? +/// @param[in] use_modulation Is frequency modulation to be used? +IRKelonAc::IRKelonAc(const uint16_t pin, const bool inverted, + const bool use_modulation) + : _irsend{pin, inverted, use_modulation}, _{} { stateReset(); } + +/// Reset the internals of the object to a known good state. +void IRKelonAc::stateReset() { + _.raw = 0L; + _.preamble[0] = 0b10000011; + _.preamble[1] = 0b00000110; +} + +#if SEND_KELON + +/// Send the current internal state as an IR message. +/// @param[in] repeat Nr. of times the message will be repeated. +void IRKelonAc::send(const uint16_t repeat) { + _irsend.sendKelon(getRaw(), kKelonBits, repeat); + + // Reset toggle flags + _.PowerToggle = false; + _.SwingVToggle = false; + + // Remove the timer time setting + _.TimerHours = 0; + _.TimerHalfHour = 0; +} + +/// Ensures the AC is on or off by exploiting the fact that setting +/// it to "smart" will always turn it on if it's off. +/// This method will send 2 commands to the AC to do the trick +/// @param[in] on Whether to ensure the AC is on or off +void IRKelonAc::ensurePower(bool on) { + // Try to avoid turning on the compressor for this operation. + // "Dry grade", when in "smart" mode, acts as a temperature offset that + // the user can configure if they feel too cold or too hot. By setting it + // to +2 we're setting the temperature to ~28°C, which will effectively + // set the AC to fan mode. + int8_t previousDry = getDryGrade(); + setDryGrade(2); + setMode(kKelonModeSmart); + send(); + + setDryGrade(previousDry); + setMode(_previousMode); + send(); + + // Now we're sure it's on. Turn it back off. The AC seems to turn back on if + // we don't send this separately + if (!on) { + setTogglePower(true); + send(); + } +} + +#endif // SEND_KELON + +/// Set up hardware to be able to send a message. +void IRKelonAc::begin() { + _irsend.begin(); +} + +/// Request toggling power - will be reset to false after sending +/// @param[in] toggle Whether to toggle the power state +void IRKelonAc::setTogglePower(const bool toggle) { + _.PowerToggle = toggle; +} + +/// Get whether toggling power will be requested +/// @return The power toggle state +bool IRKelonAc::getTogglePower() const { + return _.PowerToggle; +} + +/// Set the temperature setting. +/// @param[in] degrees The temperature in degrees celsius. +void IRKelonAc::setTemp(const uint8_t degrees) { + uint8_t temp = std::max(kKelonMinTemp, degrees); + temp = std::min(kKelonMaxTemp, temp); + _previousTemp = _.Temperature; + _.Temperature = temp - kKelonMinTemp; +} + +/// Get the current temperature setting. +/// @return Get current setting for temp. in degrees celsius. +uint8_t IRKelonAc::getTemp() const { + return _.Temperature + kKelonMinTemp; +} + +/// Set the speed of the fan. +/// @param[in] speed 0 is auto, 1-5 is the speed +void IRKelonAc::setFan(const uint8_t speed) { + uint8_t fan = std::min(speed, kKelonFanMax); + + _previousFan = _.Fan; + // Note: Kelon fan speeds are backwards! This code maps the range 0,1:3 to + // 0,3:1 to save the API's user's sanity. + _.Fan = ((static_cast(fan) - 4) * -1) % 4; +} + +/// Get the current fan speed setting. +/// @return The current fan speed. +uint8_t IRKelonAc::getFan() const { + return ((static_cast(_.Fan) - 4) * -1) % 4;; +} + +/// Set the dehumidification intensity. +/// @param[in] grade has to be in the range [-2 : +2] +void IRKelonAc::setDryGrade(const int8_t grade) { + int8_t drygrade = std::max(kKelonDryGradeMin, grade); + drygrade = std::min(kKelonDryGradeMax, drygrade); + + // Two's complement is clearly too bleeding edge for this manufacturer + uint8_t outval; + if (drygrade < 0) { + outval = 0b100 | (-drygrade & 0b011); + } else { + outval = drygrade & 0b011; + } + _.DehumidifierGrade = outval; +} + +/// Get the current dehumidification intensity setting. In smart mode, this +/// controls the temperature adjustment. +/// @return The current dehumidification intensity. +int8_t IRKelonAc::getDryGrade() const { + return static_cast(_.DehumidifierGrade & 0b011) * + ((_.DehumidifierGrade & 0b100) ? -1 : 1); +} + +/// Set the desired operation mode. +/// @param[in] mode The desired operation mode. +void IRKelonAc::setMode(const uint8_t mode) { + if (_.Mode == kKelonModeSmart || _.Mode == kKelonModeFan || + _.Mode == kKelonModeDry) { + _.Temperature = _previousTemp; + } + if (_.SuperCoolEnabled1) { + // Cancel supercool + _.SuperCoolEnabled1 = false; + _.SuperCoolEnabled2 = false; + _.Temperature = _previousTemp; + _.Fan = _previousFan; + } + _previousMode = _.Mode; + + switch (mode) { + case kKelonModeSmart: + setTemp(26); + _.SmartModeEnabled = true; + _.Mode = mode; + break; + case kKelonModeDry: + case kKelonModeFan: + setTemp(25); + // fallthrough + case kKelonModeCool: + case kKelonModeHeat: + _.Mode = mode; + // fallthrough + default: + _.SmartModeEnabled = false; + } +} + +/// Get the current operation mode setting. +/// @return The current operation mode. +uint8_t IRKelonAc::getMode() const { + return _.Mode; +} + +/// Request toggling the vertical swing - will be reset to false after sending +/// @param[in] toggle If true, the swing mode will be toggled when sent. +void IRKelonAc::setToggleSwingVertical(const bool toggle) { + _.SwingVToggle = toggle; +} + +/// Get whether the swing mode is set to be toggled +/// @return Whether the toggle bit is set +bool IRKelonAc::getToggleSwingVertical() const { + return _.SwingVToggle; +} + +/// Control the current sleep (quiet) setting. +/// @param[in] on The desired setting. +void IRKelonAc::setSleep(const bool on) { + _.SleepEnabled = on; +} + +/// Is the sleep setting on? +/// @return The current value. +bool IRKelonAc::getSleep() const { + return _.SleepEnabled; +} + +/// Control the current super cool mode setting. +/// @param[in] on The desired setting. +void IRKelonAc::setSupercool(const bool on) { + if (on) { + setTemp(kKelonMinTemp); + setMode(kKelonModeCool); + setFan(kKelonFanMax); + } else { + // All reverts to previous are handled by setMode as needed + setMode(_previousMode); + } + _.SuperCoolEnabled1 = on; + _.SuperCoolEnabled2 = on; +} + +/// Is the super cool mode setting on? +/// @return The current value. +bool IRKelonAc::getSupercool() const { + return _.SuperCoolEnabled1; +} + +/// Set the timer time and enable it. Timer is an off timer if the unit is on, +/// it is an on timer if the unit is off. +/// Only multiples of 30m are supported for < 10h, then only multiples of 60m +/// @param[in] mins Nr. of minutes +void IRKelonAc::setTimer(uint16_t mins) { + const uint16_t minutes = std::min(static_cast(mins), 24 * 60); + + if (minutes / 60 >= 10) { + uint8_t hours = minutes / 60 + 10; + _.TimerHalfHour = hours & 1; + _.TimerHours = hours >> 1; + } else { + _.TimerHalfHour = (minutes % 60) >= 30 ? 1 : 0; + _.TimerHours = minutes / 60; + } + + setTimerEnabled(true); +} + +/// Get the set timer. Timer set time is deleted once the command is sent, so +/// calling this after send() will return 0. +/// The AC unit will continue keeping track of the remaining time unless it is +/// later disabled. +/// @return The timer set minutes +uint16_t IRKelonAc::getTimer() const { + if (_.TimerHours >= 10) { + return ((uint16_t) ((_.TimerHours << 1) | _.TimerHalfHour) - 10) * 60; + } + return (((uint16_t) _.TimerHours) * 60) + (_.TimerHalfHour ? 30 : 0); +} + +/// Enable or disable the timer. Note that in order to enable the timer the +/// minutes must be set with setTimer(). +/// @param[in] on Whether to enable or disable the timer +void IRKelonAc::setTimerEnabled(bool on) { + _.TimerEnabled = on; +} + +/// Get the current timer status +/// @return Whether the timer is enabled. +bool IRKelonAc::getTimerEnabled() const { + return _.TimerEnabled; +} + + +/// Get the raw state of the object, suitable to be sent with the appropriate +/// IRsend object method. +/// @return A PTR to the internal state. +uint64_t IRKelonAc::getRaw() const { + return _.raw; +} + +/// Set the raw state of the object. +/// @param[in] new_code The raw state from the native IR message. +void IRKelonAc::setRaw(const uint64_t new_code) { + _.raw = new_code; +} + +/// Convert a standard A/C mode (stdAc::opmode_t) into it a native mode. +/// @param[in] mode A stdAc::opmode_t operation mode. +/// @return The native mode equivalent. +uint8_t IRKelonAc::convertMode(const stdAc::opmode_t mode) { + switch (mode) { + case stdAc::opmode_t::kCool: + return kKelonModeCool; + case stdAc::opmode_t::kHeat: + return kKelonModeHeat; + case stdAc::opmode_t::kDry: + return kKelonModeDry; + case stdAc::opmode_t::kFan: + return kKelonModeFan; + default: + return kKelonModeSmart; + } +} + +/// Convert a standard A/C fan speed (stdAc::fanspeed_t) into it a native speed. +/// @param[in] fan A stdAc::fanspeed_t fan speed +/// @return The native speed equivalent. +uint8_t IRKelonAc::convertFan(stdAc::fanspeed_t fan) { + switch (fan) { + case stdAc::fanspeed_t::kMin: + case stdAc::fanspeed_t::kLow: + return kKelonFanMin; + case stdAc::fanspeed_t::kMedium: + return kKelonFanMedium; + case stdAc::fanspeed_t::kHigh: + case stdAc::fanspeed_t::kMax: + return kKelonFanMax; + default: + return kKelonFanAuto; + } +} + +/// Convert a native mode to it's stdAc::opmode_t equivalent. +/// @param[in] mode A native operating mode value. +/// @return The stdAc::opmode_t equivalent. +stdAc::opmode_t IRKelonAc::toCommonMode(const uint8_t mode) { + switch (mode) { + case kKelonModeCool: + return stdAc::opmode_t::kCool; + case kKelonModeHeat: + return stdAc::opmode_t::kHeat; + case kKelonModeDry: + return stdAc::opmode_t::kDry; + case kKelonModeFan: + return stdAc::opmode_t::kFan; + default: + return stdAc::opmode_t::kAuto; + } +} + +/// Convert a native fan speed to it's stdAc::fanspeed_t equivalent. +/// @param[in] speed A native fan speed value. +/// @return The stdAc::fanspeed_t equivalent. +stdAc::fanspeed_t IRKelonAc::toCommonFanSpeed(const uint8_t speed) { + switch (speed) { + case kKelonFanMin: + return stdAc::fanspeed_t::kLow; + case kKelonFanMedium: + return stdAc::fanspeed_t::kMedium; + case kKelonFanMax: + return stdAc::fanspeed_t::kHigh; + default: + return stdAc::fanspeed_t::kAuto; + } +} + +/// Convert the internal A/C object state to it's stdAc::state_t equivalent. +/// @return A stdAc::state_t containing the current settings. +stdAc::state_t IRKelonAc::toCommon(const stdAc::state_t *prev) const { + stdAc::state_t result{}; + result.protocol = decode_type_t::KELON; + result.model = -1; // Unused. + result.mode = toCommonMode(getMode()); + result.celsius = true; + result.degrees = getTemp(); + result.fanspeed = toCommonFanSpeed(getFan()); + result.turbo = getSupercool(); + result.sleep = getSleep() ? 0 : -1; + // Not supported. + // N/A, AC only supports toggling it + result.power = (prev == nullptr || prev->power) ^ _.PowerToggle; + // N/A, AC only supports toggling it + result.swingv = stdAc::swingv_t::kAuto; + if (prev != nullptr && + (prev->swingv != stdAc::swingv_t::kAuto) ^ _.SwingVToggle) { + result.swingv = stdAc::swingv_t::kOff; + } + result.swingh = stdAc::swingh_t::kOff; + result.light = true; + result.beep = true; + result.quiet = false; + result.filter = false; + result.clean = false; + result.econo = false; + result.clock = -1; + return result; +} + +/// Convert the internal settings into a human readable string. +/// @return A String. +String IRKelonAc::toString() const { + String result = ""; + // Reserve some heap for the string to reduce fragging. + result.reserve(160); + result += addTempToString(getTemp(), true, false); + result += addModeToString(_.Mode, kKelonModeSmart, kKelonModeCool, + kKelonModeHeat, kKelonModeDry, kKelonModeFan); + result += addFanToString(_.Fan, kKelonFanMax, kKelonFanMin, kKelonFanAuto, + -1, kKelonFanMedium, kKelonFanMax); + result += addBoolToString(_.SleepEnabled, kSleepStr); + result += addSignedIntToString(getDryGrade(), kDryStr); + result += addLabeledString( + getTimerEnabled() + ? ( + getTimer() > 0 + ? minsToString(getTimer()) + : kOnStr + ) + : kOffStr, + kTimerStr); + result += addBoolToString(getSupercool(), kTurboStr); + if (getTogglePower()) { + result += addBoolToString(true, kPowerToggleStr); + } + if (getToggleSwingVertical()) { + result += addBoolToString(true, kSwingVToggleStr); + } + return result; +} diff --git a/src/ir_Kelon.h b/src/ir_Kelon.h new file mode 100644 index 000000000..498650623 --- /dev/null +++ b/src/ir_Kelon.h @@ -0,0 +1,170 @@ +// Copyright 2021 Davide Depau + +/// @file +/// @brief Support for Kelan AC protocol. +/// Both sending and decoding should be functional for models of series KELON +/// ON/OFF 9000-12000. +/// All features of the standard remote are implemented. +/// +/// @note Unsupported: +/// - Explicit on/off due to AC unit limitations +/// - Explicit swing position due to AC unit limitations +/// - Fahrenheit. +// Supports: +// Brand: Kelon, Model: ON/OFF 9000-12000 + +#ifndef IR_KELON_H_ +#define IR_KELON_H_ + +#ifdef UNIT_TEST +#include "IRsend_test.h" +#endif + +#include "IRremoteESP8266.h" +#include "IRsend.h" +#include "IRutils.h" + +union KelonProtocol { + uint64_t raw; + + struct { + uint8_t preamble[2]; + uint8_t Fan: 2; + uint8_t PowerToggle: 1; + uint8_t SleepEnabled: 1; + uint8_t DehumidifierGrade: 3; + uint8_t SwingVToggle: 1; + uint8_t Mode: 3; + uint8_t TimerEnabled: 1; + uint8_t Temperature: 4; + uint8_t TimerHalfHour: 1; + uint8_t TimerHours: 6; + uint8_t SmartModeEnabled: 1; + uint8_t pad1: 4; + uint8_t SuperCoolEnabled1: 1; + uint8_t pad2: 2; + uint8_t SuperCoolEnabled2: 1; + }; +}; + +// Constants +const uint8_t kKelonModeHeat = 0; +const uint8_t kKelonModeSmart = 1; // (temp = 26C, but not shown) +const uint8_t kKelonModeCool = 2; +const uint8_t kKelonModeDry = 3; // (temp = 25C, but not shown) +const uint8_t kKelonModeFan = 4; // (temp = 25C, but not shown) +const uint8_t kKelonFanAuto = 0; +// Note! Kelon fan speeds are actually 0:AUTO, 1:MAX, 2:MED, 3:MIN +// Since this is insane, I decided to invert them in the public API, they are +// converted back in setFan/getFan +const uint8_t kKelonFanMin = 1; +const uint8_t kKelonFanMedium = 2; +const uint8_t kKelonFanMax = 3; + +const int8_t kKelonDryGradeMin = -2; +const int8_t kKelonDryGradeMax = +2; +const uint8_t kKelonMinTemp = 18; +const uint8_t kKelonMaxTemp = 32; + + +class IRKelonAc { + public: + explicit IRKelonAc(uint16_t pin, bool inverted = false, + bool use_modulation = true); + + void stateReset(void); + + #if SEND_KELON + + void send(const uint16_t repeat = kNoRepeat); + + /// Run the calibration to calculate uSec timing offsets for this platform. + /// @return The uSec timing offset needed per modulation of the IR Led. + /// @note This will produce a 65ms IR signal pulse at 38kHz. + /// Only ever needs to be run once per object instantiation, if at all. + int8_t calibrate(void) { return _irsend.calibrate(); } + + /// Since the AC does not support actually setting the power state to a known + /// value, this utility allow ensuring the AC is on or off by exploiting + /// the fact that the AC, according to the user manual, will always turn on + /// when setting it to "smart" or "super" mode. + void ensurePower(const bool on); + + #endif + + + void begin(void); + + void setTogglePower(const bool toggle); + + bool getTogglePower(void) const; + + void setTemp(const uint8_t degrees); + + uint8_t getTemp(void) const; + + void setFan(const uint8_t speed); + + uint8_t getFan(void) const; + + void setDryGrade(const int8_t grade); + + int8_t getDryGrade(void) const; + + void setMode(const uint8_t mode); + + uint8_t getMode(void) const; + + void setToggleSwingVertical(const bool toggle); + + bool getToggleSwingVertical(void) const; + + void setSleep(const bool on); + + bool getSleep(void) const; + + void setSupercool(const bool on); + + bool getSupercool(void) const; + + void setTimer(const uint16_t mins); + + uint16_t getTimer(void) const; + + void setTimerEnabled(const bool on); + + bool getTimerEnabled(void) const; + + uint64_t getRaw(void) const; + + void setRaw(const uint64_t new_code); + + static uint8_t convertMode(const stdAc::opmode_t mode); + + static uint8_t convertFan(const stdAc::fanspeed_t fan); + + static stdAc::opmode_t toCommonMode(const uint8_t mode); + + static stdAc::fanspeed_t toCommonFanSpeed(const uint8_t speed); + + stdAc::state_t toCommon(const stdAc::state_t *prev = nullptr) const; + + String toString(void) const; + + private: +#ifndef UNIT_TEST + IRsend _irsend; ///< Instance of the IR send class +#else // UNIT_TEST + /// @cond IGNORE + IRsendTest _irsend; ///< Instance of the testing IR send class + /// @endcond +#endif // UNIT_TEST + KelonProtocol _; + + // Used when exiting supercool mode + uint8_t _previousMode = 0; + uint8_t _previousTemp = kKelonMinTemp; + uint8_t _previousFan = kKelonFanAuto; +}; + +#endif // IR_KELON_H_ diff --git a/src/locale/defaults.h b/src/locale/defaults.h index 582e2cc93..8c6096289 100644 --- a/src/locale/defaults.h +++ b/src/locale/defaults.h @@ -613,6 +613,9 @@ #ifndef D_STR_JVC #define D_STR_JVC "JVC" #endif // D_STR_JVC +#ifndef D_STR_KELON +#define D_STR_KELON "KELON" +#endif // D_STR_KELON #ifndef D_STR_KELVINATOR #define D_STR_KELVINATOR "KELVINATOR" #endif // D_STR_KELVINATOR diff --git a/test/ir_Kelon_test.cpp b/test/ir_Kelon_test.cpp new file mode 100644 index 000000000..7212d49ee --- /dev/null +++ b/test/ir_Kelon_test.cpp @@ -0,0 +1,428 @@ +// Copyright 2021 Davide Depau + +#include "IRac.h" +#include "IRrecv.h" +#include "IRrecv_test.h" +#include "IRsend.h" +#include "IRsend_test.h" +#include "gtest/gtest.h" + +// Tests for sendKelon(). + +// Test sending typical data only. +TEST(TestSendKelon, SendDataOnly) { + IRsendTest irsend(kGpioUnused); + irsend.begin(); + + // Temp: 26C, Mode: 2 (Cool), Fan: 0 (Auto), Sleep: Off, Dry: 0, Timer: Off, + // Turbo: Off + irsend.reset(); + irsend.sendKelon(0x82000683); + EXPECT_EQ( + "f38000d50" + "m9000s4600m560s1680m560s1680m560s600m560s600m560s600m560s600" + "m560s600m560s1680m560s600m560s1680m560s1680m560s600m560s600m560s600" + "m560s600m560s600m560s600m560s600m560s600m560s600m560s600m560s600" + "m560s600m560s600m560s600m560s1680m560s600m560s600m560s600m560s600" + "m560s600m560s1680m560s600m560s600m560s600m560s600m560s600m560s600" + "m560s600m560s600m560s600m560s600m560s600m560s600m560s600m560s600" + "m560s600m560s600m560s200000", + irsend.outputStr()); + + // Temp: 18C, Mode: 2 (Cool), Fan: 1 (Low), Sleep: Off, Dry: 0, Timer: Off, + // Turbo: On + irsend.reset(); + irsend.sendKelon(0x900002010683); + EXPECT_EQ( + "f38000d50" + "m9000s4600m560s1680m560s1680m560s600m560s600m560s600m560s600m560s600" + "m560s1680m560s600m560s1680m560s1680m560s600m560s600m560s600" + "m560s600m560s600m560s1680m560s600m560s600m560s600m560s600" + "m560s600m560s600m560s600m560s600m560s1680m560s600m560s600m560s600" + "m560s600m560s600m560s600m560s600m560s600m560s600m560s600" + "m560s600m560s600m560s600m560s600m560s600m560s600m560s600" + "m560s600m560s1680m560s600m560s600m560s1680m560s200000", + irsend.outputStr()); + + // Temp: 23C, Mode: 0 (Heat), Fan: 0 (Auto), Sleep: Off, Dry: 0, Timer: Off, + // Turbo: Off, Power Toggle: On + irsend.reset(); + irsend.sendKelon(0x50040683); + EXPECT_EQ( + "f38000d50" + "m9000s4600m560s1680m560s1680m560s600m560s600m560s600m560s600" + "m560s600m560s1680m560s600m560s1680m560s1680m560s600m560s600" + "m560s600m560s600m560s600m560s600m560s600m560s1680m560s600m560s600" + "m560s600m560s600m560s600m560s600m560s600m560s600m560s600m560s1680" + "m560s600m560s1680m560s600m560s600m560s600m560s600m560s600m560s600" + "m560s600m560s600m560s600m560s600m560s600m560s600m560s600m560s600" + "m560s600m560s600m560s600m560s200000", + irsend.outputStr()); + + // Temp: 26C, Mode: 2 (Cool), Fan: 0 (Auto), Sleep: Off, Dry: 0, Timer: + // On (9.5h), Turbo: + irsend.reset(); + irsend.sendKelon(0x138A000683); + EXPECT_EQ( + "f38000d50" + "m9000s4600m560s1680m560s1680m560s600m560s600m560s600" + "m560s600m560s600m560s1680m560s600m560s1680m560s1680m560s600" + "m560s600m560s600m560s600m560s600m560s600m560s600m560s600" + "m560s600m560s600m560s600m560s600m560s600m560s600m560s1680" + "m560s600m560s1680m560s600m560s600m560s600m560s1680m560s1680" + "m560s1680m560s600m560s600m560s1680m560s600m560s600m560s600" + "m560s600m560s600m560s600m560s600m560s600m560s600m560s600" + "m560s600m560s200000", + irsend.outputStr()); + + // Temp: 26C, Mode: 2 (Cool), Fan: 0 (Auto), Sleep: Off, Dry: 0, Timer: + // On (15h), Turbo: Off: + irsend.reset(); + irsend.sendKelon(0x198A000683); + EXPECT_EQ( + "f38000d50" + "m9000s4600m560s1680m560s1680m560s600m560s600m560s600" + "m560s600m560s600m560s1680m560s600m560s1680m560s1680" + "m560s600m560s600m560s600m560s600m560s600m560s600m560s600" + "m560s600m560s600m560s600m560s600m560s600m560s600m560s600" + "m560s1680m560s600m560s1680m560s600m560s600m560s600m560s1680" + "m560s1680m560s600m560s600m560s1680m560s1680m560s600" + "m560s600m560s600m560s600m560s600m560s600m560s600m560s600" + "m560s600m560s600m560s600m560s200000", + irsend.outputStr()); +} + +// Tests for decodeKelon(). +// Decode normal Kelon messages. +TEST(TestDecodeKelon, Timer12HSmartMode) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + irsend.begin(); + + irsend.reset(); + irsend.sendKelon(0x1679030683); + irsend.makeDecodeResult(); + EXPECT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(KELON, irsend.capture.decode_type); + EXPECT_EQ(kKelonBits, irsend.capture.bits); + EXPECT_EQ( + "Temp: 25C, Mode: 1 (Auto), Fan: 3 (High), Sleep: Off, Dry: 0, " + "Timer: 12:00, Turbo: Off", + IRAcUtils::resultAcToString(&irsend.capture)); + stdAc::state_t result, prev; + ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &result, &prev)); +} + +TEST(TestDecodeKelon, Timer5_5hSuperCoolMode) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + irsend.begin(); + + irsend.reset(); + irsend.sendKelon(0x100B0A010683); + irsend.makeDecodeResult(); + EXPECT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(KELON, irsend.capture.decode_type); + EXPECT_EQ(kKelonBits, irsend.capture.bits); + EXPECT_EQ( + "Temp: 18C, Mode: 2 (Cool), Fan: 1 (Low), Sleep: Off, Dry: 0, " + "Timer: 05:30, Turbo: On", + IRAcUtils::resultAcToString(&irsend.capture)); + stdAc::state_t result, prev; + ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &result, &prev)); +} + +TEST(TestDecodeKelon, ChangeSettingsWithTimerSetHeatMode) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + irsend.begin(); + + irsend.reset(); + irsend.sendKelon(0x58000683); + irsend.makeDecodeResult(); + EXPECT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(KELON, irsend.capture.decode_type); + EXPECT_EQ(kKelonBits, irsend.capture.bits); + EXPECT_EQ( + "Temp: 23C, Mode: 0 (Heat), Fan: 0 (Auto), Sleep: Off, Dry: 0, " + "Timer: On, Turbo: Off", + IRAcUtils::resultAcToString(&irsend.capture)); + stdAc::state_t result, prev; + ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &result, &prev)); +} + +TEST(TestDecodeKelon, TestPowerToggleDryMode) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + irsend.begin(); + + irsend.reset(); + irsend.sendKelon(0x83040683); + irsend.makeDecodeResult(); + EXPECT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(KELON, irsend.capture.decode_type); + EXPECT_EQ(kKelonBits, irsend.capture.bits); + EXPECT_EQ( + "Temp: 26C, Mode: 3 (Dry), Fan: 0 (Auto), Sleep: Off, Dry: 0, Timer:" + " Off, Turbo: Off, Power Toggle: On", + IRAcUtils::resultAcToString(&irsend.capture)); + stdAc::state_t result, prev; + ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &result, &prev)); +} + +TEST(TestDecodeKelon, TestSwingToggleDryMode) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + irsend.begin(); + + irsend.reset(); + irsend.sendKelon(0x83800683); + irsend.makeDecodeResult(); + EXPECT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(KELON, irsend.capture.decode_type); + EXPECT_EQ(kKelonBits, irsend.capture.bits); + EXPECT_EQ( + "Temp: 26C, Mode: 3 (Dry), Fan: 0 (Auto), Sleep: Off, Dry: 0, Timer:" + " Off, Turbo: Off, Swing(V) Toggle: On", + IRAcUtils::resultAcToString(&irsend.capture)); + stdAc::state_t result, prev; + ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &result, &prev)); +} + +TEST(TestDecodeKelon, TestDryGradeNegativeValue) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + irsend.begin(); + + irsend.reset(); + irsend.sendKelon(0x83600683); + irsend.makeDecodeResult(); + EXPECT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(KELON, irsend.capture.decode_type); + EXPECT_EQ(kKelonBits, irsend.capture.bits); + EXPECT_EQ( + "Temp: 26C, Mode: 3 (Dry), Fan: 0 (Auto), Sleep: Off, Dry: -2," + " Timer: Off, Turbo: Off", + IRAcUtils::resultAcToString(&irsend.capture)); + stdAc::state_t result, prev; + ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &result, &prev)); +} + +TEST(TestIRKelonClass, SetAndGetRaw) { + uint64_t rawData = 0x100B0A010683; + IRKelonAc ac(kGpioUnused); + ac.setRaw(rawData); + EXPECT_EQ(rawData, ac.getRaw()); +} + +TEST(TestIRKelonClass, SetAndGetTemp) { + IRKelonAc ac(kGpioUnused); + + for (uint8_t temp = kKelonMinTemp; temp <= kKelonMaxTemp; temp++) { + ac.setTemp(temp); + EXPECT_EQ(temp, ac.getTemp()); + } + + // Check bounds + ac.setTemp(kKelonMinTemp - 1); + EXPECT_EQ(kKelonMinTemp, ac.getTemp()); + ac.setTemp(kKelonMaxTemp + 1); + EXPECT_EQ(kKelonMaxTemp, ac.getTemp()); +} + +TEST(TestIRKelonClass, SetAndGetTimer) { + IRKelonAc ac(kGpioUnused); + + // 0.5h to 10h timers have a granularity of 30 minutes + for (uint16_t minutes = 0; minutes <= 60 * 10; minutes += 30) { + ac.setTimer(minutes); + EXPECT_EQ(minutes, ac.getTimer()); + } + // 10h to 24h timers have a granularity of 1h + for (uint16_t minutes = 600; minutes <= 60 * 24; minutes += 60) { + ac.setTimer(minutes); + EXPECT_EQ(minutes, ac.getTimer()); + } + // For 10h to 24h timers, we expect the minutes to be floored down to the hour + for (uint16_t minutes = 600 + 30; minutes <= 60 * 24; minutes += 60) { + ac.setTimer(minutes); + EXPECT_EQ(minutes - 30, ac.getTimer()); + } +} + +TEST(TestIRKelonClass, CheckToggles) { + IRKelonAc ac(kGpioUnused); + + ac.setTogglePower(true); + EXPECT_TRUE(ac.getTogglePower()); + ac.send(); + EXPECT_FALSE(ac.getTogglePower()); + + ac.setToggleSwingVertical(true); + EXPECT_TRUE(ac.getToggleSwingVertical()); + ac.send(); + EXPECT_FALSE(ac.getToggleSwingVertical()); + + // Known state with a power toggle + ac.setRaw(0x62040683); + EXPECT_TRUE(ac.getTogglePower()); + + // Known state with a swing toggle + ac.setRaw(0x62800683); + EXPECT_TRUE(ac.getToggleSwingVertical()); +} + +TEST(TestIRKelonClass, SetAndGetMode) { + IRKelonAc ac(kGpioUnused); + + uint8_t initial_temp = 20; + ac.setMode(kKelonModeHeat); + ac.setTemp(initial_temp); + + ac.setMode(kKelonModeCool); + EXPECT_EQ(kKelonModeCool, ac.getMode()); + EXPECT_EQ(initial_temp, ac.getTemp()); + + ac.setMode(kKelonModeHeat); + EXPECT_EQ(kKelonModeHeat, ac.getMode()); + EXPECT_EQ(initial_temp, ac.getTemp()); + + ac.setMode(kKelonModeDry); + EXPECT_EQ(kKelonModeDry, ac.getMode()); + EXPECT_EQ(25, ac.getTemp()); + + // Should revert back to previous temp + ac.setMode(kKelonModeHeat); + EXPECT_EQ(kKelonModeHeat, ac.getMode()); + EXPECT_EQ(initial_temp, ac.getTemp()); + + ac.setMode(kKelonModeFan); + EXPECT_EQ(kKelonModeFan, ac.getMode()); + EXPECT_EQ(25, ac.getTemp()); + + // Should revert back to previous temp + ac.setMode(kKelonModeHeat); + EXPECT_EQ(kKelonModeHeat, ac.getMode()); + EXPECT_EQ(initial_temp, ac.getTemp()); + + ac.setMode(kKelonModeSmart); + EXPECT_EQ(kKelonModeSmart, ac.getMode()); + EXPECT_EQ(26, ac.getTemp()); + + // Should revert back to previous temp + ac.setMode(kKelonModeHeat); + EXPECT_EQ(kKelonModeHeat, ac.getMode()); + EXPECT_EQ(initial_temp, ac.getTemp()); +} + +TEST(TestIRKelonClass, CheckSuperCoolMode) { + IRKelonAc ac(kGpioUnused); + + uint8_t initial_temp = 20; + uint8_t initial_fan = kKelonFanMin; + ac.setMode(kKelonModeHeat); + ac.setTemp(initial_temp); + ac.setFan(initial_fan); + + ac.setSupercool(true); + EXPECT_TRUE(ac.getSupercool()); + EXPECT_EQ(kKelonModeCool, ac.getMode()); + EXPECT_EQ(kKelonMinTemp, ac.getTemp()); + EXPECT_EQ(kKelonFanMax, ac.getFan()); + + // Should revert back to previous temp and mode + ac.setSupercool(false); + EXPECT_EQ(kKelonModeHeat, ac.getMode()); + EXPECT_EQ(initial_temp, ac.getTemp()); + EXPECT_EQ(initial_fan, ac.getFan()); + + ac.setSupercool(true); + EXPECT_TRUE(ac.getSupercool()); + EXPECT_EQ(kKelonModeCool, ac.getMode()); + EXPECT_EQ(kKelonMinTemp, ac.getTemp()); + EXPECT_EQ(kKelonFanMax, ac.getFan()); + + // Setting any mode should cancel supercool + ac.setMode(kKelonModeHeat); + EXPECT_EQ(kKelonModeHeat, ac.getMode()); + EXPECT_EQ(initial_temp, ac.getTemp()); + EXPECT_EQ(initial_fan, ac.getFan()); +} + +TEST(TestIRKelonClass, SetAndGetDryGrade) { + IRKelonAc ac(kGpioUnused); + + for (int i = -2; i <= 2; i++) { + ac.setDryGrade(i); + EXPECT_EQ(i, ac.getDryGrade()); + } + // Check bounds + ac.setDryGrade(3); + EXPECT_EQ(2, ac.getDryGrade()); + ac.setDryGrade(-3); + EXPECT_EQ(-2, ac.getDryGrade()); +} + +TEST(TestIRKelonClass, toCommon) { + IRKelonAc ac(kGpioUnused); + + ac.setSleep(false); + ac.setTemp(23); + ac.setMode(kKelonModeHeat); + ac.setFan(kAmcorFanMed); + auto common = ac.toCommon(); + + EXPECT_EQ(decode_type_t::KELON, common.protocol); + EXPECT_EQ(23, common.degrees); + EXPECT_TRUE(common.celsius); + EXPECT_EQ(stdAc::fanspeed_t::kMedium, common.fanspeed); + EXPECT_FALSE(common.turbo); + EXPECT_EQ(-1, common.sleep); + + ac.setSleep(true); + ac.setSupercool(true); + + common = ac.toCommon(); + EXPECT_EQ(decode_type_t::KELON, common.protocol); + EXPECT_EQ(kKelonMinTemp, common.degrees); + EXPECT_TRUE(common.celsius); + EXPECT_EQ(stdAc::fanspeed_t::kHigh, common.fanspeed); + EXPECT_TRUE(common.turbo); + EXPECT_EQ(0, common.sleep); +} + +TEST(TestIRKelonClass, toCommonToggles) { + IRKelonAc ac(kGpioUnused); + + stdAc::state_t common = ac.toCommon(); + stdAc::state_t prev = common; + + EXPECT_TRUE(common.power); + EXPECT_EQ(stdAc::swingv_t::kAuto, common.swingv); + + ac.setTogglePower(true); + ac.setToggleSwingVertical(true); + + common = ac.toCommon(&prev); + prev = common; + + EXPECT_FALSE(common.power); + EXPECT_EQ(stdAc::swingv_t::kOff, common.swingv); + + ac.setTogglePower(true); + ac.setToggleSwingVertical(true); + + common = ac.toCommon(&prev); + + EXPECT_TRUE(common.power); + EXPECT_EQ(stdAc::swingv_t::kAuto, common.swingv); +} + +TEST(TestUtils, Housekeeping) { + ASSERT_EQ("KELON", typeToString(decode_type_t::KELON)); + ASSERT_EQ(decode_type_t::KELON, strToDecodeType("KELON")); + ASSERT_FALSE(hasACState(decode_type_t::KELON)); + ASSERT_TRUE(IRac::isProtocolSupported(decode_type_t::KELON)); + ASSERT_EQ(kKelonBits, IRsend::defaultBits(decode_type_t::KELON)); + ASSERT_EQ(kNoRepeat, IRsend::minRepeats(decode_type_t::KELON)); +}