diff --git a/src/IRac.cpp b/src/IRac.cpp index f19443054..8795aab25 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 @@ -1252,6 +1256,36 @@ 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] mode The operation mode setting. +/// @param[in] degrees The temperature setting in degrees. +/// @param[in] fan The speed setting for the fan. +/// @param[in] sleep Run the device in sleep/quiet mode. +/// @param[in] superCool Run the device in Super cooling mode. +/// @param[in] dryGrade The dehumidification intensity grade +/// @param[in] togglePower Whether to toggle the unit's power +/// @param[in] toggleSwing Whether to toggle the swing setting +void IRac::kelon(IRKelonAC *ac, + const stdAc::opmode_t mode, const float degrees, + const stdAc::fanspeed_t fan, const bool sleep, + const bool superCool, const int8_t dryGrade, + const bool togglePower = false, const bool toggleSwing = false) { + ac->begin(); + ac->setMode(IRKelonAC::convertMode(mode)); + ac->setFan(IRKelonAC::convertFan(fan)); + ac->setTemp(static_cast(degrees)); + ac->setSleep(sleep); + ac->setSupercool(superCool); + + 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. @@ -2264,6 +2298,14 @@ stdAc::state_t IRac::handleToggles(const stdAc::state_t desired, if (desired.model == panasonic_ac_remote_model_t::kPanasonicCkp) result.power = desired.power ^ prev->power; break; + case decode_type_t::KELON: + result.power = desired.power ^ prev->power; + 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. + break; default: {}; } @@ -2574,6 +2616,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.mode, send.degrees, send.fanspeed, send.sleep, + send.turbo, 0, send.power, send.swingv != stdAc::swingv_t::kOff); + break; + } +#endif #if SEND_KELVINATOR case KELVINATOR: { @@ -3283,6 +3333,13 @@ namespace IRAcUtils { return ac.toString(); } #endif // DECODE_FUJITSU_AC +#if DECODE_KELON + case decode_type_t::KELON: { + IRKelonAC ac(kGpioUnused); + ac.setRaw(result->state); + return ac.toString(); + } +#endif // DECODE_KELON #if DECODE_KELVINATOR case decode_type_t::KELVINATOR: { IRKelvinatorAC ac(kGpioUnused); @@ -3768,6 +3825,14 @@ namespace IRAcUtils { break; } #endif // DECODE_HITACHI_AC424 +#if DECODE_KELON + case decode_type_t::KELON: { + IRKelonAC ac(kGpioUnused); + ac.setRaw(decode->state); + *result = ac.toCommon(); + break; + } +#endif #if DECODE_KELVINATOR case decode_type_t::KELVINATOR: { IRKelvinatorAC ac(kGpioUnused); diff --git a/src/IRac.h b/src/IRac.h index 9c62f9198..415b744fd 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 stdAc::opmode_t mode, const float degrees, + const stdAc::fanspeed_t fan, const bool sleep, const bool superCool, + const int8_t dryGrade, const bool togglePower, const bool toggleSwing); +#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 545c0d48a..67f5355c0 100644 --- a/src/IRrecv.cpp +++ b/src/IRrecv.cpp @@ -1010,6 +1010,10 @@ bool IRrecv::decode(decode_results *results, irparams_t *save, DPRINTLN("Attempting XMP decode"); if (decodeXmp(results, offset, kXmpBits)) return true; #endif // DECODE_XMP +#if DECODE_KELON + DPRINTLN("Attempting Kelon decode"); + if (decodeKelon(results, offset, kKelonBits)) 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 ca67bb813..4a6e0fa3c 100644 --- a/src/IRrecv.h +++ b/src/IRrecv.h @@ -739,6 +739,10 @@ class IRrecv { bool decodeTruma(decode_results *results, uint16_t offset = kStartOffset, const uint16_t nbits = kTrumaBits, const bool strict = true); #endif // DECODE_TRUMA +#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 9712bebbd..3544a3f29 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 ****************************************************/ @@ -754,6 +755,13 @@ #define SEND_HAIER_AC176 _IR_ENABLE_DEFAULT_ #endif // SEND_HAIER_AC176 +#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 || \ @@ -766,7 +774,7 @@ DECODE_AMCOR || DECODE_DAIKIN152 || DECODE_MITSUBISHI136 || \ 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_VOLTAS || DECODE_MIRAGE || DECODE_HAIER_AC176 || DECODE_KELON) // Add any DECODE to the above if it uses result->state (see kStateSizeMax) // you might also want to add the protocol to hasACState function #define DECODE_AC true // We need some common infrastructure for decoding A/Cs. @@ -907,8 +915,9 @@ enum decode_type_t { XMP, TRUMA, // 100 HAIER_AC176, + KELON, // Add new entries before this one, and update it to point to the last entry. - kLastDecodeType = HAIER_AC176, + kLastDecodeType = KELON, }; // Message lengths & required repeat values @@ -1021,6 +1030,8 @@ const uint16_t kHitachiAc424Bits = kHitachiAc424StateLength * 8; const uint16_t kInaxBits = 24; const uint16_t kInaxMinRepeat = kSingleRepeat; const uint16_t kJvcBits = 16; +const uint16_t kKelonStateLength = 6; +const uint16_t kKelonBits = kKelonStateLength * 8; 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 0bb223c9b..27fe43b88 100644 --- a/src/IRsend.cpp +++ b/src/IRsend.cpp @@ -1180,6 +1180,11 @@ bool IRsend::send(const decode_type_t type, const uint8_t *state, sendHitachiAc424(state, nbytes); break; #endif // SEND_HITACHI_AC424 +#if SEND_KELON + case KELON: + sendKelon(state, nbytes); + break; +#endif // SEND_KELON #if SEND_KELVINATOR case KELVINATOR: sendKelvinator(state, nbytes); diff --git a/src/IRsend.h b/src/IRsend.h index 20fd7d0c6..010de855c 100644 --- a/src/IRsend.h +++ b/src/IRsend.h @@ -712,6 +712,10 @@ class IRsend { void sendTruma(const uint64_t data, const uint16_t nbits = kTrumaBits, const uint16_t repeat = kNoRepeat); #endif // SEND_TRUMA +#if SEND_KELON + void sendKelon(const unsigned char data[], const uint16_t nbytes = kKelonStateLength, + const uint16_t repeat = kNoRepeat); +#endif // SEND_KELON protected: #ifdef UNIT_TEST diff --git a/src/IRtext.cpp b/src/IRtext.cpp index 55737c780..f6ebb2d2d 100644 --- a/src/IRtext.cpp +++ b/src/IRtext.cpp @@ -99,6 +99,7 @@ const PROGMEM char* k6thSenseStr = D_STR_6THSENSE; ///< "6th Sense" const PROGMEM char* kTypeStr = D_STR_TYPE; ///< "Type" const PROGMEM char* kSpecialStr = D_STR_SPECIAL; ///< "Special" const PROGMEM char* kIdStr = D_STR_ID; ///< "Id" / Device Identifier +const PROGMEM char* kDryGradeStr = D_STR_DRY_GRADE; const PROGMEM char* kAutoStr = D_STR_AUTO; ///< "Auto" const PROGMEM char* kAutomaticStr = D_STR_AUTOMATIC; ///< "Automatic" @@ -286,5 +287,6 @@ const PROGMEM char *kAllProtocolNamesStr = D_STR_XMP "\x0" D_STR_TRUMA "\x0" D_STR_HAIER_AC176 "\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/IRtext.h b/src/IRtext.h index d991276cd..05fa563ae 100644 --- a/src/IRtext.h +++ b/src/IRtext.h @@ -46,6 +46,7 @@ extern const char* kDayStr; extern const char* kDisplayTempStr; extern const char* kDownStr; extern const char* kDryStr; +extern const char* kDryGradeStr; extern const char* kEconoStr; extern const char* kEconoToggleStr; extern const char* kEyeAutoStr; diff --git a/src/IRutils.cpp b/src/IRutils.cpp index 5d16c850c..ac0cee12c 100644 --- a/src/IRutils.cpp +++ b/src/IRutils.cpp @@ -152,6 +152,7 @@ bool hasACState(const decode_type_t protocol) { case HITACHI_AC3: case HITACHI_AC344: case HITACHI_AC424: + case KELON: case KELVINATOR: case MIRAGE: case MITSUBISHI136: diff --git a/src/ir_Kelon.cpp b/src/ir_Kelon.cpp new file mode 100644 index 000000000..828ffaa6d --- /dev/null +++ b/src/ir_Kelon.cpp @@ -0,0 +1,443 @@ +// 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::addLabeledString; +using irutils::addModeToString; +using irutils::addFanToString; +using irutils::addTempToString; + +// 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 = kDefaultMessageGap; +const uint16_t kKelonFreq = 38000; + +#if SEND_KELON + +/// Send a Kelon message. +/// Status: Beta / Should be working. +/// @param[in] data The message to be sent. +/// @param[in] nbytes The number of bytes of message to be sent. +/// @param[in] repeat The number of times the command is to be repeated. +void IRsend::sendKelon(const unsigned char data[], const uint16_t nbytes, + const uint16_t repeat) { + sendGeneric(kKelonHdrMark, kKelonHdrSpace, + kKelonBitMark, kKelonOneSpace, + kKelonBitMark, kKelonZeroSpace, + kKelonBitMark, kKelonGap, + data, nbytes, kKelonFreq, false, // LSB First. + repeat, 50); +} + +#endif // SEND_KELON + +#if DECODE_KELON +/// Decode the supplied Kelon message. +/// Status: Beta / Should be 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; + } + uint16_t used; + used = matchGeneric(results->rawbuf + offset, results->state, + results->rawlen - offset, nbits, + kKelonHdrMark, kKelonHdrSpace, + kKelonBitMark, kKelonOneSpace, + kKelonBitMark, kKelonZeroSpace, + kKelonBitMark, kKelonBitMark, false, + _tolerance, 0, false); + + // Data bits + 2 bits header + 1 bit footer = 99 bits + if (strict && used != nbits * 2 + 3) { + 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() { + for (unsigned char &i : _.raw) i = 0x0; + _.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(), kKelonStateLength, repeat); + + // Reset toggle flags + _.PowerToggle = false; + _.SwingVToggle = false; + + // Remove the timer time setting + _.TimerHours = 0; + _.TimerHalfHour = 0; +} + +#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); + + // 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 { + auto outval = static_cast(_.DehumidifierGrade & 0b011); + if ((_.DehumidifierGrade & 0b100) == 0b100) { + outval *= -1; + } + return outval; +} + +/// Set the desired operation mode. +/// @param[in] mode The desired operation mode. +void IRKelonAC::setMode(const uint8_t mode) { + if (_previousMode == kKelonModeSmart || _previousMode == kKelonModeFan || _.SuperCoolEnabled1) { + _.Temperature = _previousTemp; + } + _.SuperCoolEnabled1 = false; + _.SuperCoolEnabled2 = false; + _previousMode = _.Mode; + + switch (mode) { + case kKelonModeSmart: + _.Temperature = 26 - kKelonMinTemp; // Do not save _previousTemp + _.SmartModeEnabled = true; + _.Mode = mode; + break; + case kKelonModeDry: + case kKelonModeFan: + _.Temperature = 25 - kKelonMinTemp; // Do not save _previousTemp + _.Mode = mode; + //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) { + _.SuperCoolEnabled1 = on; + _.SuperCoolEnabled2 = on; + if (on) { + setTemp(kKelonMinTemp); + setMode(kKelonModeCool); + } else { + _.Temperature = _previousTemp; + setMode(_previousMode); + } +} + +/// 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. +/// @param[in] mins Timer minutes (only multiples of 30m are supported for < 10h, then only multiples of 60m) +void IRKelonAC::setTimer(uint8_t mins) { + const uint8_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 +uint8_t IRKelonAC::getTimer() const { + if (_.TimerHours >= 10) { + return (((_.TimerHours << 1) | _.TimerHalfHour) - 10) * 60; + } + return _.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. +uint8_t *IRKelonAC::getRaw() { + 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 uint8_t *new_code) { + std::memcpy(_.raw, new_code, kKelonStateLength); +} + +/// 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; + case stdAc::opmode_t::kAuto: + default: + return kKelonModeSmart; + } +} + +/// Convert a standard A/C fan speed (stdAc::fanspeed_t) into it a native speed. +/// @param[in] mode 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; + case stdAc::fanspeed_t::kAuto: + 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; + case kKelonModeSmart: + 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; + case kKelonFanAuto: + 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 result{}; + result.protocol = decode_type_t::KELON; + result.model = -1; // Unused. + result.mode = toCommonMode(_.Mode); + result.celsius = true; + result.degrees = getTemp(); + result.fanspeed = toCommonFanSpeed(_.Fan); + result.turbo = getSupercool(); + result.sleep = _.SleepEnabled ? 0 : -1; + // Not supported. + result.power = true; // N/A, AC only supports toggling it + result.swingv = stdAc::swingv_t::kAuto; // N/A, AC only supports toggling it + result.swingh = stdAc::swingh_t::kOff; // N/A, horizontal air direction can only be set by manually adjusting the flaps + 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 = ""; + result.reserve(160); // Reserve some heap for the string to reduce fragging. + result += addTempToString(getTemp(), false); + result += addModeToString(_.Mode, kKelonModeSmart, kKelonModeCool, kKelonModeHeat, kKelonModeDry, kKelonModeFan); + result += addFanToString(_.Fan, kKelonFanMax, kKelonFanMin, kKelonFanAuto, -1, kKelonFanMedium, kKelonFanMax); + result += addBoolToString(_.SleepEnabled, kSleepStr); + result += addIntToString(getDryGrade(), kDryGradeStr); + return result; +} diff --git a/src/ir_Kelon.h b/src/ir_Kelon.h new file mode 100644 index 000000000..1cfa95f49 --- /dev/null +++ b/src/ir_Kelon.h @@ -0,0 +1,157 @@ +// 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_ + +#include "IRremoteESP8266.h" +#include "IRsend.h" +#include "IRutils.h" + +union KelonProtocol { + uint8_t raw[kKelonStateLength]; + + 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; + }; +}; +#endif // IR_KELON_H_ + +// 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 kKelonDryGradeMax{2}; +const int8_t kKelonDryGradeMin{-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(); + + #if SEND_KELON + + void send(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() { return _irsend.calibrate(); } + + #endif + + + void begin(); + + void setTogglePower(bool toggle); + + bool getTogglePower() const; + + void setTemp(uint8_t degrees); + + uint8_t getTemp() const; + + void setFan(uint8_t speed); + + uint8_t getFan() const; + + void setDryGrade(int8_t grade); + + int8_t getDryGrade() const; + + void setMode(uint8_t mode); + + uint8_t getMode() const; + + void setToggleSwingVertical(bool toggle); + + bool getToggleSwingVertical() const; + + void setSleep(bool on); + + bool getSleep() const; + + void setSupercool(bool on); + + bool getSupercool() const; + + void setTimer(uint8_t mins); + + uint8_t getTimer() const; + + void setTimerEnabled(bool on); + + bool getTimerEnabled() const; + + uint8_t *getRaw(); + + void setRaw(const uint8_t new_code[]); + + static uint8_t convertMode(stdAc::opmode_t mode); + + static uint8_t convertFan(stdAc::fanspeed_t fan); + + static stdAc::opmode_t toCommonMode(uint8_t mode); + + static stdAc::fanspeed_t toCommonFanSpeed(uint8_t speed); + + stdAc::state_t toCommon() const; + + String toString() 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}; +}; \ No newline at end of file diff --git a/src/locale/defaults.h b/src/locale/defaults.h index b22bd45be..e69285b7a 100644 --- a/src/locale/defaults.h +++ b/src/locale/defaults.h @@ -279,6 +279,9 @@ #ifndef D_STR_ID #define D_STR_ID "Id" #endif // D_STR_ID +#ifndef D_STR_DRY_GRADE +#define D_STR_DRY_GRADE "Dry grade" +#endif // D_STR_DRY_GRADE #ifndef D_STR_AUTO #define D_STR_AUTO "Auto" @@ -799,6 +802,9 @@ #ifndef D_STR_ZEPEAL #define D_STR_ZEPEAL "ZEPEAL" #endif // D_STR_ZEPEAL +#ifndef D_STR_KELON +#define D_STR_KELON "KELON" +#endif // D_STR_KELON // IRrecvDumpV2+ #ifndef D_STR_TIMESTAMP