diff --git a/src/IRac.cpp b/src/IRac.cpp index 94e6778f6..d7e236fbe 100644 --- a/src/IRac.cpp +++ b/src/IRac.cpp @@ -42,6 +42,7 @@ #include "ir_Toshiba.h" #include "ir_Trotec.h" #include "ir_Vestel.h" +#include "ir_Voltas.h" #include "ir_Whirlpool.h" /// Class constructor @@ -262,6 +263,9 @@ bool IRac::isProtocolSupported(const decode_type_t protocol) { #if SEND_VESTEL_AC case decode_type_t::VESTEL_AC: #endif +#if SEND_VOLTAS + case decode_type_t::VOLTAS: +#endif #if SEND_WHIRLPOOL_AC case decode_type_t::WHIRLPOOL_AC: #endif @@ -547,7 +551,7 @@ void IRac::daikin(IRDaikinESP *ac, /// @param[in] turbo Run the device in turbo/powerful mode. /// @param[in] light Turn on the LED/Display mode. /// @param[in] econo Run the device in economical mode. -/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, > 0 is on. +/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, >= 0 is on. /// @param[in] clock The time in Nr. of mins since midnight. < 0 is ignore. void IRac::daikin128(IRDaikin128 *ac, const bool on, const stdAc::opmode_t mode, @@ -671,7 +675,7 @@ void IRac::daikin176(IRDaikin176 *ac, /// @param[in] filter Turn on the (ion/pollen/etc) filter mode. /// @param[in] clean Turn on the self-cleaning mode. e.g. Mould, dry filters etc /// @param[in] beep Enable/Disable beeps when receiving IR messages. -/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, > 0 is on. +/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, >= 0 is on. /// @param[in] clock The time in Nr. of mins since midnight. < 0 is ignore. void IRac::daikin2(IRDaikin2 *ac, const bool on, const stdAc::opmode_t mode, @@ -740,7 +744,7 @@ void IRac::daikin216(IRDaikin216 *ac, /// @param[in] swingv The vertical swing setting. /// @param[in] quiet Run the device in quiet/silent mode. /// @param[in] turbo Run the device in turbo/powerful mode. -/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, > 0 is on. +/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, >= 0 is on. /// @param[in] clock The time in Nr. of mins since midnight. < 0 is ignore. void IRac::daikin64(IRDaikin64 *ac, const bool on, const stdAc::opmode_t mode, @@ -1454,7 +1458,7 @@ void IRac::mitsubishiHeavy88(IRMitsubishiHeavy88Ac *ac, /// @param[in] econo Run the device in economical mode. /// @param[in] filter Turn on the (ion/pollen/etc) filter mode. /// @param[in] clean Turn on the self-cleaning mode. e.g. Mould, dry filters etc -/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, > 0 is on. +/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, >= 0 is on. void IRac::mitsubishiHeavy152(IRMitsubishiHeavy152Ac *ac, const bool on, const stdAc::opmode_t mode, const float degrees, @@ -1496,7 +1500,7 @@ void IRac::mitsubishiHeavy152(IRMitsubishiHeavy152Ac *ac, /// @param[in] turbo Run the device in turbo/powerful mode. /// @param[in] light Turn on the LED/Display mode. /// @param[in] filter Turn on the (ion/pollen/etc) filter mode. -/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, > 0 is on. +/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, >= 0 is on. void IRac::neoclima(IRNeoclimaAc *ac, const bool on, const stdAc::opmode_t mode, const float degrees, const stdAc::fanspeed_t fan, @@ -1621,7 +1625,7 @@ void IRac::samsung(IRSamsungAc *ac, /// @param[in] fan The speed setting for the fan. /// @param[in] swingv The vertical swing setting. /// @param[in] beep Enable/Disable beeps when receiving IR messages. -/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, > 0 is on. +/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, >= 0 is on. void IRac::sanyo(IRSanyoAc *ac, const bool on, const stdAc::opmode_t mode, const float degrees, const stdAc::fanspeed_t fan, @@ -1751,7 +1755,7 @@ void IRac::tcl112(IRTcl112Ac *ac, /// @param[in] fan The speed setting for the fan. /// @param[in] swingv The vertical swing setting. /// @param[in] light Turn on the LED/Display mode. -/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, > 0 is on. +/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, >= 0 is on. void IRac::teco(IRTecoAc *ac, const bool on, const stdAc::opmode_t mode, const float degrees, const stdAc::fanspeed_t fan, const stdAc::swingv_t swingv, @@ -1820,7 +1824,7 @@ void IRac::toshiba(IRToshibaAC *ac, /// @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 Nr. of minutes for sleep mode. -1 is Off, > 0 is on. +/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, >= 0 is on. void IRac::trotec(IRTrotecESP *ac, const bool on, const stdAc::opmode_t mode, const float degrees, const stdAc::fanspeed_t fan, @@ -1854,7 +1858,7 @@ void IRac::trotec(IRTrotecESP *ac, /// @param[in] swingv The vertical swing setting. /// @param[in] turbo Run the device in turbo/powerful mode. /// @param[in] filter Turn on the (ion/pollen/etc) filter mode. -/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, > 0 is on. +/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, >= 0 is on. /// @param[in] clock The time in Nr. of mins since midnight. < 0 is ignore. /// @param[in] sendNormal Do we send a Normal settings message at all? /// i.e In addition to the clock/time/timer message @@ -1886,6 +1890,48 @@ void IRac::vestel(IRVestelAc *ac, } #endif // SEND_VESTEL_AC +#if SEND_VOLTAS +/// Send a Voltas A/C message with the supplied settings. +/// @param[in, out] ac A Ptr to an IRVoltas object to use. +/// @param[in] model The A/C model to use. +/// @param[in] on The power setting. +/// @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] swingv The vertical swing setting. +/// @param[in] swingh The horizontal swing setting. +/// @param[in] turbo Run the device in turbo/powerful mode. +/// @param[in] econo Run the device in economical mode. +/// @param[in] light Turn on the LED/Display mode. +/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, >= 0 is on. +void IRac::voltas(IRVoltas *ac, + const voltas_ac_remote_model_t model, + const bool on, const stdAc::opmode_t mode, + const float degrees, const stdAc::fanspeed_t fan, + const stdAc::swingv_t swingv, const stdAc::swingh_t swingh, + const bool turbo, const bool econo, const bool light, + const int16_t sleep) { + ac->begin(); + ac->setModel(model); + ac->setPower(on); + ac->setMode(ac->convertMode(mode)); + ac->setTemp(degrees); + ac->setFan(ac->convertFan(fan)); + ac->setSwingV(swingv != stdAc::swingv_t::kOff); + ac->setSwingH(swingh != stdAc::swingh_t::kOff); + // No Quiet setting available. + ac->setTurbo(turbo); + ac->setEcono(econo); + ac->setLight(light); + // No Filter setting available. + // No Clean setting available. + // No Beep setting available. + ac->setSleep(sleep >= 0); // Sleep is either on/off, so convert to boolean. + // No Clock setting available. + ac->send(); +} +#endif // SEND_VOLTAS + #if SEND_WHIRLPOOL_AC /// Send a Whirlpool A/C message with the supplied settings. /// @param[in, out] ac A Ptr to an IRWhirlpoolAc object to use. @@ -1897,7 +1943,7 @@ void IRac::vestel(IRVestelAc *ac, /// @param[in] swingv The vertical swing setting. /// @param[in] turbo Run the device in turbo/powerful mode. /// @param[in] light Turn on the LED/Display mode. -/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, > 0 is on. +/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, >= 0 is on. /// @param[in] clock The time in Nr. of mins since midnight. < 0 is ignore. void IRac::whirlpool(IRWhirlpoolAc *ac, const whirlpool_ac_remote_model_t model, const bool on, const stdAc::opmode_t mode, @@ -2451,6 +2497,16 @@ bool IRac::sendAc(const stdAc::state_t desired, const stdAc::state_t *prev) { break; } #endif // SEND_VESTEL_AC +#if SEND_VOLTAS + case VOLTAS: + { + IRVoltas ac(_pin, _inverted, _modulation); + voltas(&ac, (voltas_ac_remote_model_t)send.model, send.power, send.mode, + degC, send.fanspeed, send.swingv, send.swingh, send.turbo, + send.econo, send.light, send.sleep); + break; + } +#endif // SEND_VOLTAS #if SEND_WHIRLPOOL_AC case WHIRLPOOL_AC: { @@ -2667,6 +2723,11 @@ int16_t IRac::strToModel(const char *str, const int16_t def) { return fujitsu_ac_remote_model_t::ARJW2; } else if (!strcasecmp(str, "ARRY4")) { return fujitsu_ac_remote_model_t::ARRY4; + // LG A/C models + } else if (!strcasecmp(str, "GE6711AR2853M")) { + return lg_ac_remote_model_t::GE6711AR2853M; + } else if (!strcasecmp(str, "AKB75215403")) { + return lg_ac_remote_model_t::AKB75215403; // Panasonic A/C families } else if (!strcasecmp(str, "LKE") || !strcasecmp(str, "PANASONICLKE")) { return panasonic_ac_remote_model_t::kPanasonicLke; @@ -2681,6 +2742,9 @@ int16_t IRac::strToModel(const char *str, const int16_t def) { return panasonic_ac_remote_model_t::kPanasonicCkp; } else if (!strcasecmp(str, "RKR") || !strcasecmp(str, "PANASONICRKR")) { return panasonic_ac_remote_model_t::kPanasonicRkr; + // Voltas A/C models + } else if (!strcasecmp(str, "122LZF")) { + return voltas_ac_remote_model_t::kVoltas122LZF; // Whirlpool A/C models } else if (!strcasecmp(str, "DG11J13A") || !strcasecmp(str, "DG11J104") || !strcasecmp(str, "DG11J1-04")) { @@ -3113,6 +3177,13 @@ namespace IRAcUtils { return ac.toString(); } #endif // DECODE_VESTEL_AC +#if DECODE_VOLTAS + case decode_type_t::VOLTAS: { + IRVoltas ac(kGpioUnused); + ac.setRaw(result->state); + return ac.toString(); + } +#endif // DECODE_VOLTAS #if DECODE_TECO case decode_type_t::TECO: { IRTecoAc ac(kGpioUnused); @@ -3514,6 +3585,14 @@ namespace IRAcUtils { break; } #endif // DECODE_VESTEL_AC +#if DECODE_VOLTAS + case decode_type_t::VOLTAS: { + IRVoltas ac(kGpioUnused); + ac.setRaw(decode->state); + *result = ac.toCommon(prev); + break; + } +#endif // DECODE_VOLTAS #if DECODE_WHIRLPOOL_AC case decode_type_t::WHIRLPOOL_AC: { IRWhirlpoolAc ac(kGpioUnused); diff --git a/src/IRac.h b/src/IRac.h index 5721f2bd6..9e210c83b 100644 --- a/src/IRac.h +++ b/src/IRac.h @@ -36,6 +36,7 @@ #include "ir_Toshiba.h" #include "ir_Trotec.h" #include "ir_Vestel.h" +#include "ir_Voltas.h" #include "ir_Whirlpool.h" // Constants @@ -405,6 +406,14 @@ void electra(IRElectraAc *ac, const int16_t sleep = -1, const int16_t clock = -1, const bool sendNormal = true); #endif // SEND_VESTEL_AC +#if SEND_VOLTAS + void voltas(IRVoltas *ac, const voltas_ac_remote_model_t model, + const bool on, const stdAc::opmode_t mode, + const float degrees, const stdAc::fanspeed_t fan, + const stdAc::swingv_t swingv, const stdAc::swingh_t swingh, + const bool turbo, const bool econo, const bool light, + const int16_t sleep = -1); +#endif // SEND_VOLTAS #if SEND_WHIRLPOOL_AC void whirlpool(IRWhirlpoolAc *ac, const whirlpool_ac_remote_model_t model, const bool on, const stdAc::opmode_t mode, const float degrees, diff --git a/src/IRsend.h b/src/IRsend.h index 57b088533..76fd5cdae 100644 --- a/src/IRsend.h +++ b/src/IRsend.h @@ -148,6 +148,12 @@ enum panasonic_ac_remote_model_t { kPanasonicRkr = 6, }; +/// Voltas A/C model numbers +enum voltas_ac_remote_model_t { + kVoltasUnknown = 0, // Full Function + kVoltas122LZF = 1, // (1) 122LZF (No SwingH support) (Default) +}; + /// Whirlpool A/C model numbers enum whirlpool_ac_remote_model_t { DG11J13A = 1, // DG11J1-04 too diff --git a/src/IRutils.cpp b/src/IRutils.cpp index 25a1fe13a..33eab5474 100644 --- a/src/IRutils.cpp +++ b/src/IRutils.cpp @@ -544,6 +544,12 @@ namespace irutils { default: return kUnknownStr; } break; + case decode_type_t::VOLTAS: + switch (model) { + case voltas_ac_remote_model_t::kVoltas122LZF: return F("122LZF"); + default: return kUnknownStr; + } + break; case decode_type_t::WHIRLPOOL_AC: switch (model) { case whirlpool_ac_remote_model_t::DG11J13A: return F("DG11J13A"); diff --git a/src/ir_Voltas.cpp b/src/ir_Voltas.cpp index f0fe910f7..fe3e507cb 100644 --- a/src/ir_Voltas.cpp +++ b/src/ir_Voltas.cpp @@ -4,13 +4,22 @@ /// @brief Support for Voltas A/C protocol /// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1238 -// Supports: -// Brand: Voltas, Model: 122LZF 4011252 Window A/C - +#include "ir_Voltas.h" +#include +#include #include "IRrecv.h" #include "IRsend.h" +#include "IRtext.h" #include "IRutils.h" +using irutils::addBoolToString; +using irutils::addModelToString; +using irutils::addModeToString; +using irutils::addFanToString; +using irutils::addLabeledString; +using irutils::addTempToString; +using irutils::minsToString; + // Constants const uint16_t kVoltasBitMark = 1026; ///< uSeconds. const uint16_t kVoltasOneSpace = 2553; ///< uSeconds. @@ -19,7 +28,7 @@ const uint16_t kVoltasFreq = 38000; ///< Hz. #if SEND_VOLTAS /// Send a Voltas formatted message. -/// Status: ALPHA / Untested. +/// Status: STABLE / Working on real device. /// @param[in] data An array of bytes containing the IR command. /// It is assumed to be in MSB order for this code. /// e.g. @@ -42,7 +51,7 @@ void IRsend::sendVoltas(const uint8_t data[], const uint16_t nbytes, #if DECODE_VOLTAS /// Decode the supplied Voltas message. -/// Status: ALPHA / Untested. +/// Status: STABLE / Working on real device. /// @param[in,out] results Ptr to the data to decode & where to store the decode /// @param[in] offset The starting index to use when attempting to decode the /// raw data. Typically/Defaults to kStartOffset. @@ -61,9 +70,447 @@ bool IRrecv::decodeVoltas(decode_results *results, uint16_t offset, kVoltasBitMark, kVoltasZeroSpace, kVoltasBitMark, kDefaultMessageGap, true)) return false; + // Compliance + if (strict && !IRVoltas::validChecksum(results->state, nbits / 8)) + return false; // Success results->decode_type = decode_type_t::VOLTAS; results->bits = nbits; return true; } #endif // DECODE_VOLTAS + +/// 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? +IRVoltas::IRVoltas(const uint16_t pin, const bool inverted, + const bool use_modulation) + : _irsend(pin, inverted, use_modulation) { + stateReset(); +} + +// Reset the internal state to a fixed known good state. +void IRVoltas::stateReset() { + // This resets to a known-good state. + // ref: https://github.com/crankyoldgit/IRremoteESP8266/issues/1238#issuecomment-674699746 + const uint8_t kReset[kVoltasStateLength] = { + 0x33, 0x28, 0x00, 0x17, 0x3B, 0x3B, 0x3B, 0x11, 0x00, 0xCB}; + setRaw(kReset); +} + +/// Set up hardware to be able to send a message. +void IRVoltas::begin() { _irsend.begin(); } + +#if SEND_VOLTAS +/// Send the current internal state as an IR message. +/// @param[in] repeat Nr. of times the message will be repeated. +void IRVoltas::send(const uint16_t repeat) { + _irsend.sendVoltas(getRaw(), kVoltasStateLength, repeat); +} +#endif // SEND_VOLTAS + +/// Get the model information currently known. +/// @param[in] raw Work out the model info from the current raw state. +/// @return The known model number. +voltas_ac_remote_model_t IRVoltas::getModel(const bool raw) const { + if (raw) { + switch (_.SwingHChange) { + case kVoltasSwingHNoChange: + return voltas_ac_remote_model_t::kVoltas122LZF; + default: + return voltas_ac_remote_model_t::kVoltasUnknown; + } + } else { + return _model; + } +} + +/// Set the current model for the remote. +/// @param[in] model The model number. +void IRVoltas::setModel(const voltas_ac_remote_model_t model) { + switch (model) { + case voltas_ac_remote_model_t::kVoltas122LZF: + _model = model; + setSwingHChange(false); + break; + default: _model = voltas_ac_remote_model_t::kVoltasUnknown; + } +} + +/// Get a PTR to the internal state/code for this protocol. +/// @return PTR to a code for this protocol based on the current internal state. +uint8_t* IRVoltas::getRaw(void) { + checksum(); // Ensure correct settings before sending. + return _.raw; +} + +/// Set the internal state from a valid code for this protocol. +/// @param[in] new_code A valid code for this protocol. +void IRVoltas::setRaw(const uint8_t new_code[]) { + std::memcpy(_.raw, new_code, kVoltasStateLength); + setModel(getModel(true)); +} + +/// Calculate and set the checksum values for the internal state. +void IRVoltas::checksum(void) { + _.Checksum = calcChecksum(_.raw); +} + +/// Verify the checksum is valid for a given state. +/// @param[in] state The array to verify the checksum of. +/// @param[in] length The length of the state array. +/// @return true, if the state has a valid checksum. Otherwise, false. +bool IRVoltas::validChecksum(const uint8_t state[], const uint16_t length) { + if (length) return state[length - 1] == calcChecksum(state, length); + return true; +} + +/// Calculate the checksum is valid for a given state. +/// @param[in] state The array to calculate the checksum of. +/// @param[in] length The length of the state array. +/// @return The valid checksum value for the state. +uint8_t IRVoltas::calcChecksum(const uint8_t state[], const uint16_t length) { + uint8_t result = 0; + if (length) + result = sumBytes(state, length - 1); + return ~result; +} + +/// Change the power setting to On. +void IRVoltas::on() { setPower(true); } + +/// Change the power setting to Off. +void IRVoltas::off() { setPower(false); } + +/// Change the power setting. +/// @param[in] on true, the setting is on. false, the setting is off. +void IRVoltas::setPower(const bool on) { _.Power = on; } + +/// Get the value of the current power setting. +/// @return true, the setting is on. false, the setting is off. +bool IRVoltas::getPower(void) const { return _.Power; } + +/// Set the operating mode of the A/C. +/// @param[in] mode The desired operating mode. +/// @note If we get an unexpected mode, default to AUTO. +void IRVoltas::setMode(const uint8_t mode) { + _.Mode = mode; + switch (mode) { + case kVoltasFan: + setFan(getFan()); // Force the fan speed to a correct one fo the mode. + break; + case kVoltasDry: + setFan(kVoltasFanLow); + setTemp(kVoltasDryTemp); + break; + case kVoltasHeat: + case kVoltasCool: + break; + default: + setMode(kVoltasCool); + return; + } + // Reset some settings if needed. + setEcono(getEcono()); + setTurbo(getTurbo()); + setSleep(getSleep()); +} + +/// Get the operating mode setting of the A/C. +/// @return The current operating mode setting. +uint8_t IRVoltas::getMode(void) { return _.Mode; } + +/// Convert a stdAc::opmode_t enum into its native mode. +/// @param[in] mode The enum to be converted. +/// @return The native equivilant of the enum. +uint8_t IRVoltas::convertMode(const stdAc::opmode_t mode) { + switch (mode) { + case stdAc::opmode_t::kHeat: return kVoltasHeat; + case stdAc::opmode_t::kDry: return kVoltasDry; + case stdAc::opmode_t::kFan: return kVoltasFan; + default: return kVoltasCool; + } +} + +/// Convert a native mode into its stdAc equivilant. +/// @param[in] mode The native setting to be converted. +/// @return The stdAc equivilant of the native setting. +stdAc::opmode_t IRVoltas::toCommonMode(const uint8_t mode) { + switch (mode) { + case kVoltasHeat: return stdAc::opmode_t::kHeat; + case kVoltasDry: return stdAc::opmode_t::kDry; + case kVoltasFan: return stdAc::opmode_t::kFan; + default: return stdAc::opmode_t::kCool; + } +} + +/// Set the temperature. +/// @param[in] temp The temperature in degrees celsius. +void IRVoltas::setTemp(const uint8_t temp) { + uint8_t new_temp = std::max(kVoltasMinTemp, temp); + new_temp = std::min(kVoltasMaxTemp, new_temp); + _.Temp = new_temp - kVoltasMinTemp; +} + +/// Get the current temperature setting. +/// @return The current setting for temp. in degrees celsius. +uint8_t IRVoltas::getTemp(void) { return _.Temp + kVoltasMinTemp; } + +/// Set the speed of the fan. +/// @param[in] fan The desired setting. +void IRVoltas::setFan(const uint8_t fan) { + switch (fan) { + case kVoltasFanAuto: + if (_.Mode == kVoltasFan) { // Auto speed is not available in fan mode. + setFan(kVoltasFanHigh); + return; + } + // FALL-THRU + case kVoltasFanLow: + case kVoltasFanMed: + case kVoltasFanHigh: + _.FanSpeed = fan; + break; + default: + setFan(kVoltasFanAuto); + } +} + +/// Get the current fan speed setting. +/// @return The current fan speed/mode. +uint8_t IRVoltas::getFan(void) { return _.FanSpeed; } + +/// Convert a stdAc::fanspeed_t enum into it's native speed. +/// @param[in] speed The enum to be converted. +/// @return The native equivilant of the enum. +uint8_t IRVoltas::convertFan(const stdAc::fanspeed_t speed) { + switch (speed) { + case stdAc::fanspeed_t::kMin: + case stdAc::fanspeed_t::kLow: return kVoltasFanLow; + case stdAc::fanspeed_t::kMedium: return kVoltasFanMed; + case stdAc::fanspeed_t::kHigh: + case stdAc::fanspeed_t::kMax: return kVoltasFanHigh; + default: return kVoltasFanAuto; + } +} + +/// Convert a native fan speed into its stdAc equivilant. +/// @param[in] spd The native setting to be converted. +/// @return The stdAc equivilant of the native setting. +stdAc::fanspeed_t IRVoltas::toCommonFanSpeed(const uint8_t spd) { + switch (spd) { + case kVoltasFanHigh: return stdAc::fanspeed_t::kMax; + case kVoltasFanMed: return stdAc::fanspeed_t::kMedium; + case kVoltasFanLow: return stdAc::fanspeed_t::kMin; + default: return stdAc::fanspeed_t::kAuto; + } +} + +/// Set the Vertical Swing setting of the A/C. +/// @param[in] on true, the setting is on. false, the setting is off. +void IRVoltas::setSwingV(const bool on) { _.SwingV = on ? 0b111 : 0b000; } + +/// Get the Vertical Swing setting of the A/C. +/// @return true, the setting is on. false, the setting is off. +bool IRVoltas::getSwingV(void) const { return _.SwingV == 0b111; } + +/// Set the Horizontal Swing setting of the A/C. +/// @param[in] on true, the setting is on. false, the setting is off. +void IRVoltas::setSwingH(const bool on) { + switch (_model) { + case voltas_ac_remote_model_t::kVoltas122LZF: + break; // unsupported on these models. + default: + _.SwingH = on; + setSwingHChange(true); + } +} + +/// Get the Horizontal Swing setting of the A/C. +/// @return true, the setting is on. false, the setting is off. +bool IRVoltas::getSwingH(void) const { + switch (_model) { + case voltas_ac_remote_model_t::kVoltas122LZF: + return false; // unsupported on these models. + default: + return _.SwingH; + } +} + +/// Set the bits for changing the Horizontal Swing setting of the A/C. +/// @param[in] on true, the change bits are set. +/// false, the "no change" bits are set. +void IRVoltas::setSwingHChange(const bool on) { + _.SwingHChange = on ? kVoltasSwingHChange : kVoltasSwingHNoChange; + if (!on) _.SwingH = true; // "No Change" also sets SwingH to 1. +} + +/// Are the Horizontal Swing change bits set in the message? +/// @return true, the correct bits are set. false, the correct bits are not set. +bool IRVoltas::getSwingHChange(void) const { + return _.SwingHChange == kVoltasSwingHChange; +} + +/// Change the Wifi setting. +/// @param[in] on true, the setting is on. false, the setting is off. +void IRVoltas::setWifi(const bool on) { _.Wifi = on; } + +/// Get the value of the current Wifi setting. +/// @return true, the setting is on. false, the setting is off. +bool IRVoltas::getWifi(void) const { return _.Wifi; } + +/// Change the Turbo setting. +/// @param[in] on true, the setting is on. false, the setting is off. +/// @note The Turbo setting is only available in Cool mode. +void IRVoltas::setTurbo(const bool on) { + if (on && _.Mode == kVoltasCool) + _.Turbo = true; + else + _.Turbo = false; +} + +/// Get the value of the current Turbo setting. +/// @return true, the setting is on. false, the setting is off. +bool IRVoltas::getTurbo(void) const { return _.Turbo; } + +/// Change the Economy setting. +/// @param[in] on true, the setting is on. false, the setting is off. +/// @note The Economy setting is only available in Cool mode. +void IRVoltas::setEcono(const bool on) { + if (on && _.Mode == kVoltasCool) + _.Econo = true; + else + _.Econo = false; +} + +/// Get the value of the current Econo setting. +/// @return true, the setting is on. false, the setting is off. +bool IRVoltas::getEcono(void) const { return _.Econo; } + +/// Change the Light setting. +/// @param[in] on true, the setting is on. false, the setting is off. +void IRVoltas::setLight(const bool on) { _.Light = on; } + +/// Get the value of the current Light setting. +/// @return true, the setting is on. false, the setting is off. +bool IRVoltas::getLight(void) const { return _.Light; } + +/// Change the Sleep setting. +/// @param[in] on true, the setting is on. false, the setting is off. +/// @note The Sleep setting is only available in Cool mode. +void IRVoltas::setSleep(const bool on) { + if (on && _.Mode == kVoltasCool) + _.Sleep = true; + else + _.Sleep = false; +} + +/// Get the value of the current Sleep setting. +/// @return true, the setting is on. false, the setting is off. +bool IRVoltas::getSleep(void) const { return _.Sleep; } + +/// Get the value of the On Timer time. +/// @return Number of minutes before the timer activates. +uint16_t IRVoltas::getOnTime(void) const { + return std::min((unsigned)(12 * _.OnTimer12Hr + _.OnTimerHrs - 1), 23U) * 60 + + _.OnTimerMins; +} + +/// Set the value of the On Timer time. +/// @param[in] nr_of_mins Number of minutes before the timer activates. +/// 0 disables the timer. Max is 23 hrs & 59 mins (1439 mins) +void IRVoltas::setOnTime(const uint16_t nr_of_mins) { + // Cap the total number of mins. + uint16_t mins = std::min(nr_of_mins, (uint16_t)(23 * 60 + 59)); + uint16_t hrs = (mins / 60) + 1; + _.OnTimerMins = mins % 60; + _.OnTimer12Hr = hrs / 12; + _.OnTimerHrs = hrs % 12; + _.OnTimerEnable = (mins > 0); // Is the timer is to be enabled? +} + +/// Get the value of the On Timer time. +/// @return Number of minutes before the timer activates. +uint16_t IRVoltas::getOffTime(void) const { + return std::min((unsigned)(12 * _.OffTimer12Hr + _.OffTimerHrs - 1), 23U) * + 60 + _.OffTimerMins; +} + +/// Set the value of the Off Timer time. +/// @param[in] nr_of_mins Number of minutes before the timer activates. +/// 0 disables the timer. Max is 23 hrs & 59 mins (1439 mins) +void IRVoltas::setOffTime(const uint16_t nr_of_mins) { + // Cap the total number of mins. + uint16_t mins = std::min(nr_of_mins, (uint16_t)(23 * 60 + 59)); + uint16_t hrs = (mins / 60) + 1; + _.OffTimerMins = mins % 60; + _.OffTimer12Hr = hrs / 12; + _.OffTimerHrs = hrs % 12; + _.OffTimerEnable = (mins > 0); // Is the timer is to be enabled? +} + +/// Convert the current internal state into its stdAc::state_t equivilant. +/// @param[in] prev Ptr to the previous state if available. +/// @return The stdAc equivilant of the native settings. +stdAc::state_t IRVoltas::toCommon(const stdAc::state_t *prev) { + stdAc::state_t result; + // Start with the previous state if given it. + if (prev != NULL) { + result = *prev; + } else { + // Set defaults for non-zero values that are not implicitly set for when + // there is no previous state. + result.swingh = stdAc::swingh_t::kOff; + } + result.model = getModel(); + result.protocol = decode_type_t::VOLTAS; + result.power = _.Power; + result.mode = toCommonMode(_.Mode); + result.celsius = true; + result.degrees = getTemp(); + result.fanspeed = toCommonFanSpeed(_.FanSpeed); + result.swingv = getSwingV() ? stdAc::swingv_t::kAuto : stdAc::swingv_t::kOff; + if (getSwingHChange()) + result.swingh = _.SwingH ? stdAc::swingh_t::kAuto : stdAc::swingh_t::kOff; + result.turbo = _.Turbo; + result.econo = _.Econo; + result.light = _.Light; + result.sleep = _.Sleep ? 0 : -1; + // Not supported. + result.quiet = false; + result.filter = false; + result.clean = false; + result.beep = false; + result.clock = -1; + return result; +} + +/// Convert the current internal state into a human readable string. +/// @return A human readable string. +String IRVoltas::toString() { + String result = ""; + result.reserve(200); // Reserve some heap for the string to reduce fragging. + result += addModelToString(decode_type_t::VOLTAS, getModel(), false); + result += addBoolToString(_.Power, kPowerStr); + result += addModeToString(_.Mode, 255, kVoltasCool, kVoltasHeat, + kVoltasDry, kVoltasFan); + result += addTempToString(getTemp()); + result += addFanToString(_.FanSpeed, kVoltasFanHigh, kVoltasFanLow, + kVoltasFanAuto, kVoltasFanAuto, kVoltasFanMed); + result += addBoolToString(getSwingV(), kSwingVStr); + if (getSwingHChange()) + result += addBoolToString(_.SwingH, kSwingHStr); + else + result += addLabeledString(kNAStr, kSwingHStr); + result += addBoolToString(_.Turbo, kTurboStr); + result += addBoolToString(_.Econo, kEconoStr); + result += addBoolToString(_.Wifi, kWifiStr); + result += addBoolToString(_.Light, kLightStr); + result += addBoolToString(_.Sleep, kSleepStr); + result += addLabeledString(_.OnTimerEnable ? minsToString(getOnTime()) + : kOffStr, kOnTimerStr); + result += addLabeledString(_.OffTimerEnable ? minsToString(getOffTime()) + : kOffStr, kOffTimerStr); + return result; +} diff --git a/src/ir_Voltas.h b/src/ir_Voltas.h new file mode 100644 index 000000000..b880e5e45 --- /dev/null +++ b/src/ir_Voltas.h @@ -0,0 +1,161 @@ +// Copyright 2020 David Conran (crankyoldgit) +// Copyright 2020 manj9501 +/// @file +/// @brief Support for Voltas A/C protocol +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1238 + +// Supports: +// Brand: Voltas, Model: 122LZF 4011252 Window A/C +// +// Ref: https://docs.google.com/spreadsheets/d/1zzDEUQ52y7MZ7_xCU3pdjdqbRXOwZLsbTGvKWcicqCI/ +// Ref: https://www.corona.co.jp/box/download.php?id=145060636229 + +#ifndef IR_VOLTAS_H_ +#define IR_VOLTAS_H_ + +#define __STDC_LIMIT_MACROS +#include +#ifndef UNIT_TEST +#include +#endif +#include "IRremoteESP8266.h" +#include "IRsend.h" +#ifdef UNIT_TEST +#include "IRsend_test.h" +#endif + + +union VoltasProtocol { + uint8_t raw[kVoltasStateLength]; ///< The state in native IR code form + struct { + // Byte 0 + uint8_t SwingH :1; + uint8_t SwingHChange :7; + // Byte 1 + uint8_t Mode :4; + uint8_t :1; // Unknown/Unused + uint8_t FanSpeed :3; + // Byte 2 + uint8_t SwingV :3; + uint8_t Wifi :1; + uint8_t :1; // Unknown/Unused + uint8_t Turbo :1; + uint8_t Sleep :1; + uint8_t Power :1; + // Byte 3 + uint8_t Temp :4; + uint8_t :2; // Typically 0b01 + uint8_t Econo :1; + uint8_t TempSet :1; + // Byte 4 + uint8_t OnTimerMins :6; // 0-59 + uint8_t :1; // Unknown/Unused + uint8_t OnTimer12Hr :1; // (Nr of Hours + 1) % 12. + // Byte 5 + uint8_t OffTimerMins :6; // 0-59 + uint8_t :1; // Unknown/Unused + uint8_t OffTimer12Hr :1; // (Nr of Hours + 1) % 12. + // Byte 6 + uint8_t :8; // Typically 0b00111011(0x3B) + // Byte 7 + uint8_t OnTimerHrs :4; // (Nr of Hours + 1) % 12. + uint8_t OffTimerHrs :4; // (Nr of Hours + 1) % 12. + // Byte 8 + uint8_t :5; // Typically 0b00000 + uint8_t Light :1; + uint8_t OffTimerEnable :1; + uint8_t OnTimerEnable :1; + // Byte 9 + uint8_t Checksum :8; + }; +}; + +// Constants +const uint8_t kVoltasFan = 0b0001; ///< 1 +const uint8_t kVoltasHeat = 0b0010; ///< 2 +const uint8_t kVoltasDry = 0b0100; ///< 4 +const uint8_t kVoltasCool = 0b1000; ///< 8 +const uint8_t kVoltasMinTemp = 16; ///< Celsius +const uint8_t kVoltasDryTemp = 24; ///< Celsius +const uint8_t kVoltasMaxTemp = 30; ///< Celsius +const uint8_t kVoltasFanHigh = 0b001; ///< 1 +const uint8_t kVoltasFanMed = 0b010; ///< 2 +const uint8_t kVoltasFanLow = 0b100; ///< 4 +const uint8_t kVoltasFanAuto = 0b111; ///< 7 +const uint8_t kVoltasSwingHChange = 0b1111100; ///< 0x7D +const uint8_t kVoltasSwingHNoChange = 0b0011001; ///< 0x19 + +// Classes +/// Class for handling detailed Voltas A/C messages. +class IRVoltas { + public: + explicit IRVoltas(const uint16_t pin, const bool inverted = false, + const bool use_modulation = true); + void stateReset(); +#if SEND_VOLTAS + 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(); } +#endif // SEND_VOLTAS + void begin(); + static bool validChecksum(const uint8_t state[], + const uint16_t length = kVoltasStateLength); + void setModel(const voltas_ac_remote_model_t model); + voltas_ac_remote_model_t getModel(const bool raw = false) const; + void setPower(const bool on); + bool getPower(void) const; + void on(void); + void off(void); + void setWifi(const bool on); + bool getWifi(void) const; + void setTemp(const uint8_t temp); + uint8_t getTemp(void); + void setFan(const uint8_t speed); + uint8_t getFan(void); + void setMode(const uint8_t mode); + uint8_t getMode(void); + void setSwingH(const bool on); + bool getSwingH(void) const; + void setSwingHChange(const bool on); + bool getSwingHChange(void) const; + void setSwingV(const bool on); + bool getSwingV(void) const; + void setEcono(const bool on); + bool getEcono(void) const; + void setLight(const bool on); + bool getLight(void) const; + void setTurbo(const bool on); + bool getTurbo(void) const; + void setSleep(const bool on); + bool getSleep(void) const; + uint16_t getOnTime(void) const; + void setOnTime(const uint16_t nr_of_mins); + uint16_t getOffTime(void) const; + void setOffTime(const uint16_t nr_of_mins); + uint8_t* getRaw(void); + void setRaw(const uint8_t new_code[]); + uint8_t convertMode(const stdAc::opmode_t mode); + uint8_t convertFan(const stdAc::fanspeed_t speed); + 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 = NULL); + String toString(void); +#ifndef UNIT_TEST + + private: + IRsend _irsend; ///< Instance of the IR send class +#else + /// @cond IGNORE + IRsendTest _irsend; ///< Instance of the testing IR send class + /// @endcond +#endif + VoltasProtocol _; ///< The state of the IR remote. + voltas_ac_remote_model_t _model; ///< Model type. + void checksum(void); + static uint8_t calcChecksum(const uint8_t state[], + const uint16_t length = kVoltasStateLength); +}; +#endif // IR_VOLTAS_H_ diff --git a/test/IRac_test.cpp b/test/IRac_test.cpp index f99d727aa..722163d56 100644 --- a/test/IRac_test.cpp +++ b/test/IRac_test.cpp @@ -29,6 +29,7 @@ #include "ir_Toshiba.h" #include "ir_Trotec.h" #include "ir_Vestel.h" +#include "ir_Voltas.h" #include "ir_Whirlpool.h" #include "IRac.h" #include "IRrecv.h" @@ -1554,6 +1555,83 @@ TEST(TestIRac, Vestel) { "m520s100000", ac._irsend.outputStr()); } +TEST(TestIRac, Voltas) { + IRVoltas ac(kGpioUnused); + IRac irac(kGpioUnused); + IRrecv capture(kGpioUnused); + ac.begin(); + + // Test the UNKNOWN model type + char expected_unknown[] = + "Model: 0 (UNKNOWN), Power: On, Mode: 8 (Cool), Temp: 18C, " + "Fan: 1 (High), Swing(V): On, Swing(H): On, " + "Turbo: Off, Econo: Off, WiFi: Off, Light: On, Sleep: On, " + "On Timer: Off, Off Timer: Off"; + irac.voltas(&ac, + voltas_ac_remote_model_t::kVoltasUnknown, // Model + true, // Power + stdAc::opmode_t::kCool, // Mode + 18, // Celsius + stdAc::fanspeed_t::kHigh, // Fan speed + stdAc::swingv_t::kAuto, // Vertical Swing + stdAc::swingh_t::kAuto, // Horizontal Swing + false, // Turbo + false, // Econo + true, // Light + 3 * 60); // Sleep + EXPECT_EQ(voltas_ac_remote_model_t::kVoltasUnknown, ac.getModel()); + EXPECT_TRUE(ac.getPower()); + EXPECT_EQ(kVoltasCool, ac.getMode()); + EXPECT_EQ(18, ac.getTemp()); + EXPECT_EQ(kVoltasFanHigh, ac.getFan()); + EXPECT_FALSE(ac.getTurbo()); + EXPECT_FALSE(ac.getEcono()); + EXPECT_TRUE(ac.getLight()); + EXPECT_TRUE(ac.getSleep()); + ASSERT_EQ(expected_unknown, ac.toString()); + ac._irsend.makeDecodeResult(); + EXPECT_TRUE(capture.decode(&ac._irsend.capture)); + ASSERT_EQ(VOLTAS, ac._irsend.capture.decode_type); + ASSERT_EQ(kVoltasBits, ac._irsend.capture.bits); + ASSERT_EQ(expected_unknown, IRAcUtils::resultAcToString(&ac._irsend.capture)); + stdAc::state_t r, p; + ASSERT_TRUE(IRAcUtils::decodeToState(&ac._irsend.capture, &r, &p)); + + ac._irsend.reset(); + // Test the UNKNOWN model type + char expected_122LZF[] = + "Model: 1 (122LZF), Power: On, Mode: 8 (Cool), Temp: 18C, " + "Fan: 1 (High), Swing(V): On, Swing(H): N/A, " + "Turbo: Off, Econo: Off, WiFi: Off, Light: On, Sleep: On, " + "On Timer: Off, Off Timer: Off"; + irac.voltas(&ac, + voltas_ac_remote_model_t::kVoltas122LZF, // Model + true, // Power + stdAc::opmode_t::kCool, // Mode + 18, // Celsius + stdAc::fanspeed_t::kHigh, // Fan speed + stdAc::swingv_t::kAuto, // Vertical Swing + stdAc::swingh_t::kAuto, // Horizontal Swing + false, // Turbo + false, // Econo + true, // Light + 3 * 60); // Sleep + EXPECT_EQ(voltas_ac_remote_model_t::kVoltas122LZF, ac.getModel()); + EXPECT_TRUE(ac.getPower()); + EXPECT_EQ(kVoltasCool, ac.getMode()); + EXPECT_EQ(18, ac.getTemp()); + EXPECT_EQ(kVoltasFanHigh, ac.getFan()); + EXPECT_FALSE(ac.getTurbo()); + EXPECT_FALSE(ac.getEcono()); + EXPECT_TRUE(ac.getLight()); + EXPECT_TRUE(ac.getSleep()); + ASSERT_EQ(expected_122LZF, ac.toString()); + ac._irsend.makeDecodeResult(); + EXPECT_TRUE(capture.decode(&ac._irsend.capture)); + ASSERT_EQ(VOLTAS, ac._irsend.capture.decode_type); + ASSERT_EQ(kVoltasBits, ac._irsend.capture.bits); + ASSERT_EQ(expected_122LZF, IRAcUtils::resultAcToString(&ac._irsend.capture)); +} TEST(TestIRac, Whirlpool) { IRWhirlpoolAc ac(kGpioUnused); diff --git a/test/ir_Voltas_test.cpp b/test/ir_Voltas_test.cpp index 8931fcab3..38fa9223b 100644 --- a/test/ir_Voltas_test.cpp +++ b/test/ir_Voltas_test.cpp @@ -1,5 +1,6 @@ // Copyright 2020 crankyoldgit +#include "ir_Voltas.h" #include "IRac.h" #include "IRrecv.h" #include "IRrecv_test.h" @@ -38,6 +39,14 @@ TEST(TestDecodeVoltas, RealExample) { ASSERT_EQ(decode_type_t::VOLTAS, irsend.capture.decode_type); ASSERT_EQ(kVoltasBits, irsend.capture.bits); EXPECT_STATE_EQ(expected, irsend.capture.state, irsend.capture.bits); + EXPECT_EQ( + "Model: 1 (122LZF), Power: On, Mode: 4 (Dry), Temp: 24C, Fan: 4 (Low), " + "Swing(V): Off, Swing(H): N/A, " + "Turbo: Off, Econo: Off, WiFi: On, Light: Off, Sleep: Off, " + "On Timer: Off, Off Timer: Off", + IRAcUtils::resultAcToString(&irsend.capture)); + stdAc::state_t r, p; + ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &r, &p)); } TEST(TestDecodeVoltas, SyntheticExample) { @@ -61,7 +70,356 @@ TEST(TestUtils, Housekeeping) { ASSERT_EQ("VOLTAS", typeToString(decode_type_t::VOLTAS)); ASSERT_EQ(decode_type_t::VOLTAS, strToDecodeType("VOLTAS")); ASSERT_TRUE(hasACState(decode_type_t::VOLTAS)); - ASSERT_FALSE(IRac::isProtocolSupported(decode_type_t::VOLTAS)); + ASSERT_TRUE(IRac::isProtocolSupported(decode_type_t::VOLTAS)); ASSERT_EQ(kVoltasBits, IRsend::defaultBits(decode_type_t::VOLTAS)); ASSERT_EQ(kNoRepeat, IRsend::minRepeats(decode_type_t::VOLTAS)); } + +TEST(TestIRVoltasClass, Checksums) { + const uint8_t valid[kVoltasStateLength] = { + 0x33, 0x84, 0x88, 0x18, 0x3B, 0x3B, 0x3B, 0x11, 0x00, 0xE6}; + EXPECT_TRUE(IRVoltas::validChecksum(valid)); + EXPECT_FALSE(IRVoltas::validChecksum(valid, kVoltasStateLength - 1)); + EXPECT_EQ(0xE6, IRVoltas::calcChecksum(valid)); + const uint8_t badchecksum[kVoltasStateLength] = { + 0x33, 0x84, 0x88, 0x18, 0x3B, 0x3B, 0x3B, 0x11, 0x00, 0x00}; + EXPECT_FALSE(IRVoltas::validChecksum(badchecksum)); + EXPECT_EQ(0xE6, IRVoltas::calcChecksum(badchecksum)); + const uint8_t kReset[kVoltasStateLength] = { + 0x33, 0x28, 0x00, 0x17, 0x3B, 0x3B, 0x3B, 0x11, 0x00, 0xCB}; + EXPECT_TRUE(IRVoltas::validChecksum(kReset)); + EXPECT_EQ(0xCB, IRVoltas::calcChecksum(kReset)); +} + +TEST(TestIRVoltasClass, SetandGetRaw) { + const uint8_t valid[kVoltasStateLength] = { + 0x33, 0x84, 0x88, 0x18, 0x3B, 0x3B, 0x3B, 0x11, 0x00, 0xE6}; + const uint8_t badchecksum[kVoltasStateLength] = { + 0x33, 0x84, 0x88, 0x18, 0x3B, 0x3B, 0x3B, 0x11, 0x00, 0x00}; + IRVoltas ac(kGpioUnused); + + ac.setRaw(valid); + EXPECT_STATE_EQ(valid, ac.getRaw(), kVoltasBits); + ac.setRaw(badchecksum); + EXPECT_STATE_EQ(valid, ac.getRaw(), kVoltasBits); +} + +TEST(TestIRVoltasClass, Power) { + IRVoltas ac(kGpioUnused); + ac.begin(); + + ac.on(); + EXPECT_TRUE(ac.getPower()); + + ac.off(); + EXPECT_FALSE(ac.getPower()); + + ac.setPower(true); + EXPECT_TRUE(ac.getPower()); + + ac.setPower(false); + EXPECT_FALSE(ac.getPower()); +} + +TEST(TestIRVoltasClass, Wifi) { + IRVoltas ac(kGpioUnused); + ac.begin(); + + ac.setWifi(false); + EXPECT_FALSE(ac.getWifi()); + ac.setWifi(true); + EXPECT_TRUE(ac.getWifi()); + ac.setWifi(false); + EXPECT_FALSE(ac.getWifi()); +} + +TEST(TestIRVoltasClass, Turbo) { + IRVoltas ac(kGpioUnused); + ac.begin(); + + ac.setTurbo(false); + EXPECT_FALSE(ac.getTurbo()); + ac.setTurbo(true); + EXPECT_TRUE(ac.getTurbo()); + ac.setTurbo(false); + EXPECT_FALSE(ac.getTurbo()); +} + +TEST(TestIRVoltasClass, Sleep) { + IRVoltas ac(kGpioUnused); + ac.begin(); + + ac.setSleep(false); + EXPECT_FALSE(ac.getSleep()); + ac.setSleep(true); + EXPECT_TRUE(ac.getSleep()); + ac.setSleep(false); + EXPECT_FALSE(ac.getSleep()); +} + +TEST(TestIRVoltasClass, Econo) { + IRVoltas ac(kGpioUnused); + ac.begin(); + // Control of econo mode is only available in cool. + ac.setMode(kVoltasCool); + ac.setEcono(false); + EXPECT_FALSE(ac.getEcono()); + ac.setEcono(true); + EXPECT_TRUE(ac.getEcono()); + ac.setEcono(false); + EXPECT_FALSE(ac.getEcono()); + ac.setEcono(true); + ac.setMode(kVoltasHeat); // Control of econo mode should now be disabled. + EXPECT_FALSE(ac.getEcono()); + ac.setEcono(true); + EXPECT_FALSE(ac.getEcono()); +} + +TEST(TestIRVoltasClass, Light) { + IRVoltas ac(kGpioUnused); + ac.begin(); + + ac.setLight(false); + EXPECT_FALSE(ac.getLight()); + ac.setLight(true); + EXPECT_TRUE(ac.getLight()); + ac.setLight(false); + EXPECT_FALSE(ac.getLight()); + + const uint8_t light_off[kVoltasStateLength] = { + 0x33, 0x84, 0x88, 0x18, 0x3B, 0x3B, 0x3B, 0x11, 0x00, 0xE6}; + ac.setRaw(light_off); + EXPECT_FALSE(ac.getLight()); + const uint8_t light_on[kVoltasStateLength] = { + 0x33, 0x84, 0x88, 0x18, 0x3B, 0x3B, 0x3B, 0x11, 0x20, 0xC6}; + ac.setRaw(light_on); + EXPECT_TRUE(ac.getLight()); + ac.setLight(false); + EXPECT_STATE_EQ(light_off, ac.getRaw(), kVoltasBits); + ac.setLight(true); + EXPECT_STATE_EQ(light_on, ac.getRaw(), kVoltasBits); +} + +TEST(TestVoltasClass, OperatingMode) { + IRVoltas ac(kGpioUnused); + ac.begin(); + + ac.setMode(kVoltasCool); + EXPECT_EQ(kVoltasCool, ac.getMode()); + ac.setMode(kVoltasFan); + EXPECT_EQ(kVoltasFan, ac.getMode()); + ac.setMode(kVoltasDry); + EXPECT_EQ(kVoltasDry, ac.getMode()); + ac.setMode(kVoltasHeat); + EXPECT_EQ(kVoltasHeat, ac.getMode()); + + ac.setMode(kVoltasCool - 1); + EXPECT_EQ(kVoltasCool, ac.getMode()); + + ac.setMode(kVoltasCool + 1); + EXPECT_EQ(kVoltasCool, ac.getMode()); + + ac.setMode(255); + EXPECT_EQ(kVoltasCool, ac.getMode()); +} + +TEST(TestVoltasClass, Temperature) { + IRVoltas ac(kGpioUnused); + ac.begin(); + + ac.setTemp(kVoltasMinTemp); + EXPECT_EQ(kVoltasMinTemp, ac.getTemp()); + + ac.setTemp(kVoltasMinTemp + 1); + EXPECT_EQ(kVoltasMinTemp + 1, ac.getTemp()); + + ac.setTemp(kVoltasMaxTemp); + EXPECT_EQ(kVoltasMaxTemp, ac.getTemp()); + + ac.setTemp(kVoltasMinTemp - 1); + EXPECT_EQ(kVoltasMinTemp, ac.getTemp()); + + ac.setTemp(kVoltasMaxTemp + 1); + EXPECT_EQ(kVoltasMaxTemp, ac.getTemp()); + + ac.setTemp(23); + EXPECT_EQ(23, ac.getTemp()); + + ac.setTemp(0); + EXPECT_EQ(kVoltasMinTemp, ac.getTemp()); + + ac.setTemp(255); + EXPECT_EQ(kVoltasMaxTemp, ac.getTemp()); +} + +TEST(TestVoltasClass, FanSpeed) { + IRVoltas ac(kGpioUnused); + ac.begin(); + ac.setMode(kVoltasCool); // All fan speeds are allowed in cool mode. + ac.setFan(kVoltasFanLow); + + ac.setFan(kVoltasFanAuto); + EXPECT_EQ(kVoltasFanAuto, ac.getFan()); + + ac.setFan(kVoltasFanLow); + EXPECT_EQ(kVoltasFanLow, ac.getFan()); + ac.setFan(kVoltasFanMed); + EXPECT_EQ(kVoltasFanMed, ac.getFan()); + ac.setFan(kVoltasFanHigh); + EXPECT_EQ(kVoltasFanHigh, ac.getFan()); + + ac.setFan(0); + EXPECT_EQ(kVoltasFanAuto, ac.getFan()); + + ac.setFan(255); + EXPECT_EQ(kVoltasFanAuto, ac.getFan()); + + // Confirm auto speed isn't operable in Fan mode. + ac.setMode(kVoltasFan); + EXPECT_NE(kVoltasFanAuto, ac.getFan()); + ac.setFan(kVoltasFanLow); + EXPECT_EQ(kVoltasFanLow, ac.getFan()); + ac.setFan(kVoltasFanMed); + EXPECT_EQ(kVoltasFanMed, ac.getFan()); + ac.setFan(kVoltasFanHigh); + EXPECT_EQ(kVoltasFanHigh, ac.getFan()); + ac.setFan(kVoltasFanAuto); + EXPECT_NE(kVoltasFanAuto, ac.getFan()); +} + +TEST(TestVoltasClass, SwingV) { + IRVoltas ac(kGpioUnused); + ac.begin(); + + ac.setSwingV(true); + EXPECT_TRUE(ac.getSwingV()); + + ac.setSwingV(false); + EXPECT_EQ(false, ac.getSwingV()); + + ac.setSwingV(true); + EXPECT_TRUE(ac.getSwingV()); +} + +TEST(TestVoltasClass, SwingH) { + IRVoltas ac(kGpioUnused); + ac.begin(); + // This model allows full control. + ac.setModel(voltas_ac_remote_model_t::kVoltasUnknown); + + ac.setSwingHChange(false); + EXPECT_FALSE(ac.getSwingHChange()); + + ac.setSwingH(true); + EXPECT_TRUE(ac.getSwingH()); + EXPECT_TRUE(ac.getSwingHChange()); + + ac.setSwingHChange(false); + ac.setSwingH(false); + EXPECT_FALSE(ac.getSwingH()); + EXPECT_TRUE(ac.getSwingHChange()); + + ac.setSwingH(true); + EXPECT_TRUE(ac.getSwingH()); + EXPECT_TRUE(ac.getSwingHChange()); + + // Switch to a model that does not allow SwingH control. + ac.setModel(voltas_ac_remote_model_t::kVoltas122LZF); + EXPECT_FALSE(ac.getSwingHChange()); + EXPECT_FALSE(ac.getSwingH()); + ac.setSwingH(true); + EXPECT_FALSE(ac.getSwingHChange()); + EXPECT_FALSE(ac.getSwingH()); + ac.setSwingH(false); + EXPECT_FALSE(ac.getSwingHChange()); + EXPECT_FALSE(ac.getSwingH()); +} + +TEST(TestVoltasClass, HumanReadable) { + IRVoltas ac(kGpioUnused); + EXPECT_EQ( + "Model: 1 (122LZF), Power: Off, Mode: 8 (Cool), Temp: 23C, " + "Fan: 1 (High), Swing(V): Off, Swing(H): N/A, Turbo: Off, Econo: Off, " + "WiFi: Off, Light: Off, Sleep: Off, On Timer: Off, Off Timer: Off", + ac.toString()); + ac.on(); + ac.setTemp(21); + ac.setFan(kVoltasFanAuto); + ac.setSwingV(true); + ac.setWifi(true); + ac.setLight(true); + ac.setTurbo(true); + ac.setSleep(true); + ac.setEcono(true); + ac.setOnTime(2 * 60 + 17); + ac.setMode(kVoltasHeat); // Heat mode should cancel Sleep, Turbo, & Econo. + EXPECT_EQ( + "Model: 1 (122LZF), Power: On, Mode: 2 (Heat), Temp: 21C, " + "Fan: 7 (Auto), Swing(V): On, Swing(H): N/A, Turbo: Off, Econo: Off, " + "WiFi: On, Light: On, Sleep: Off, On Timer: 02:17, Off Timer: Off", + ac.toString()); + ac.setOffTime(13 * 60 + 37); + ac.setMode(kVoltasCool); + ac.setTurbo(true); + ac.setSleep(true); + ac.setEcono(true); + EXPECT_EQ( + "Model: 1 (122LZF), Power: On, Mode: 8 (Cool), Temp: 21C, " + "Fan: 7 (Auto), Swing(V): On, Swing(H): N/A, Turbo: On, Econo: On, " + "WiFi: On, Light: On, Sleep: On, On Timer: 02:17, Off Timer: 13:37", + ac.toString()); + ac.setModel(voltas_ac_remote_model_t::kVoltasUnknown); + ac.setSwingH(true); + EXPECT_EQ( + "Model: 0 (UNKNOWN), Power: On, Mode: 8 (Cool), Temp: 21C, " + "Fan: 7 (Auto), Swing(V): On, Swing(H): On, Turbo: On, Econo: On, " + "WiFi: On, Light: On, Sleep: On, On Timer: 02:17, Off Timer: 13:37", + ac.toString()); + ac.setModel(voltas_ac_remote_model_t::kVoltas122LZF); + ac.setOnTime(0); + EXPECT_EQ( + "Model: 1 (122LZF), Power: On, Mode: 8 (Cool), Temp: 21C, " + "Fan: 7 (Auto), Swing(V): On, Swing(H): N/A, Turbo: On, Econo: On, " + "WiFi: On, Light: On, Sleep: On, On Timer: Off, Off Timer: 13:37", + ac.toString()); + ac.setOffTime(0); + EXPECT_EQ( + "Model: 1 (122LZF), Power: On, Mode: 8 (Cool), Temp: 21C, " + "Fan: 7 (Auto), Swing(V): On, Swing(H): N/A, Turbo: On, Econo: On, " + "WiFi: On, Light: On, Sleep: On, On Timer: Off, Off Timer: Off", + ac.toString()); +} + +TEST(TestVoltasClass, Timers) { + IRVoltas ac(kGpioUnused); + const uint8_t off_7hrs[10] = { // Real Data + 0x33, 0x28, 0x80, 0x1B, 0x3B, 0x3B, 0x3B, 0x71, 0x40, 0xA7}; + ac.setRaw(off_7hrs); + EXPECT_EQ( + "Model: 1 (122LZF), Power: On, Mode: 8 (Cool), Temp: 27C, Fan: 1 (High), " + "Swing(V): Off, Swing(H): N/A, Turbo: Off, Econo: Off, WiFi: Off, " + "Light: Off, Sleep: Off, On Timer: Off, Off Timer: 06:59", + ac.toString()); + const uint8_t off_16hrs[10] = { // Real Data + 0x33, 0x28, 0x80, 0x1B, 0x3B, 0xBB, 0x3B, 0x41, 0x40, 0x57}; + ac.setRaw(off_16hrs); + EXPECT_EQ( + "Model: 1 (122LZF), Power: On, Mode: 8 (Cool), Temp: 27C, Fan: 1 (High), " + "Swing(V): Off, Swing(H): N/A, Turbo: Off, Econo: Off, WiFi: Off, " + "Light: Off, Sleep: Off, On Timer: Off, Off Timer: 15:59", + ac.toString()); + ac.setOffTime(23 * 60 + 59); + EXPECT_EQ( + "Model: 1 (122LZF), Power: On, Mode: 8 (Cool), Temp: 27C, Fan: 1 (High), " + "Swing(V): Off, Swing(H): N/A, Turbo: Off, Econo: Off, WiFi: Off, " + "Light: Off, Sleep: Off, On Timer: Off, Off Timer: 23:59", + ac.toString()); + const uint8_t off_24hrs[10] = { // Real Data + 0x33, 0x28, 0x80, 0x1B, 0x3A, 0x3A, 0x3B, 0x01, 0x40, 0x19}; + ac.setRaw(off_24hrs); + EXPECT_EQ( + "Model: 1 (122LZF), Power: On, Mode: 8 (Cool), Temp: 27C, Fan: 1 (High), " + "Swing(V): Off, Swing(H): N/A, Turbo: Off, Econo: Off, WiFi: Off, " + "Light: Off, Sleep: Off, On Timer: Off, Off Timer: 23:58", + ac.toString()); +}