diff --git a/src/IRac.cpp b/src/IRac.cpp index da59af1b0..63e9e44a3 100644 --- a/src/IRac.cpp +++ b/src/IRac.cpp @@ -34,6 +34,7 @@ #include "ir_Daikin.h" #include "ir_Ecoclim.h" #include "ir_Electra.h" +#include "ir_Eurom.h" #include "ir_Fujitsu.h" #include "ir_Haier.h" #include "ir_Hitachi.h" @@ -244,6 +245,9 @@ bool IRac::isProtocolSupported(const decode_type_t protocol) { #if SEND_ELECTRA_AC case decode_type_t::ELECTRA_AC: #endif +#if SEND_EUROM + case decode_type_t::EUROM: +#endif #if SEND_FUJITSU_AC case decode_type_t::FUJITSU_AC: #endif @@ -1211,6 +1215,31 @@ void IRac::electra(IRElectraAc *ac, } #endif // SEND_ELECTRA_AC +#if SEND_EUROM +/// Send an Eurom A/C message with the supplied settings. +/// @param[in, out] ac A Ptr to an IREuromAc object to use. +/// @param[in] power The power setting. +/// @param[in] mode The operation mode setting. +/// @param[in] degrees The temperature setting in degrees, normally Celsius. +/// @param[in] fahrenheit If the given temperature is in Fahrenheit instead. +/// @param[in] fan The speed setting for the fan. +/// @param[in] swingv The swing setting. +/// @param[in] sleep The sleep mode setting. +void IRac::eurom(IREuromAc *ac, const bool power, const stdAc::opmode_t mode, + const float degrees, const bool fahrenheit, + const stdAc::fanspeed_t fan, const stdAc::swingv_t swingv, + const bool sleep) { + ac->begin(); + ac->setPower(power); + ac->setMode(ac->convertMode(mode)); + ac->setTemp(degrees, fahrenheit); + ac->setFan(ac->convertFan(fan)); + ac->setSwing(ac->convertSwing(swingv)); + ac->setSleep(sleep); + ac->send(); +} +#endif // SEND_EUROM + #if SEND_FUJITSU_AC /// Send a Fujitsu A/C message with the supplied settings. /// @param[in, out] ac A Ptr to an IRFujitsuAC object to use. @@ -3239,6 +3268,15 @@ bool IRac::sendAc(const stdAc::state_t desired, const stdAc::state_t *prev) { break; } #endif // SEND_ELECTRA_AC +#if SEND_EUROM + case EUROM: + { + IREuromAc ac(_pin, _inverted, _modulation); + eurom(&ac, send.power, send.mode, send.degrees, !send.celsius, + send.fanspeed, send.swingv, send.sleep); + break; + } +#endif // SEND_EUROM #if SEND_FUJITSU_AC case FUJITSU_AC: { @@ -4215,6 +4253,13 @@ String resultAcToString(const decode_results * const result) { return ac.toString(); } #endif // DECODE_ELECTRA_AC +#if DECODE_EUROM + case decode_type_t::EUROM: { + IREuromAc ac(kGpioUnused); + ac.setRaw(result->state); + return ac.toString(); + } +#endif // DECODE_EUROM #if DECODE_FUJITSU_AC case decode_type_t::FUJITSU_AC: { IRFujitsuAC ac(kGpioUnused); @@ -4715,6 +4760,14 @@ bool decodeToState(const decode_results *decode, stdAc::state_t *result, break; } #endif // DECODE_ELECTRA_AC +#if DECODE_EUROM + case decode_type_t::EUROM: { + IREuromAc ac(kGpioUnused); + ac.setRaw(decode->state); + *result = ac.toCommon(); + break; + } +#endif // DECODE_EUROM #if DECODE_FUJITSU_AC case decode_type_t::FUJITSU_AC: { IRFujitsuAC ac(kGpioUnused); diff --git a/src/IRac.h b/src/IRac.h index ae78b8ce3..b95bd724b 100644 --- a/src/IRac.h +++ b/src/IRac.h @@ -22,6 +22,7 @@ #include "ir_Fujitsu.h" #include "ir_Ecoclim.h" #include "ir_Electra.h" +#include "ir_Eurom.h" #include "ir_Goodweather.h" #include "ir_Gree.h" #include "ir_Haier.h" @@ -267,6 +268,12 @@ void electra(IRElectraAc *ac, const stdAc::swingh_t swingh, const bool iFeel, const bool quiet, const bool turbo, const bool lighttoggle, const bool clean); #endif // SEND_ELECTRA_AC +#if SEND_EUROM + void eurom(IREuromAc *ac, const bool power, const stdAc::opmode_t mode, + const float degrees, const bool fahrenheit, + const stdAc::fanspeed_t fan, const stdAc::swingv_t swingv, + const bool sleep); +#endif // SEND_EUROM #if SEND_FUJITSU_AC void fujitsu(IRFujitsuAC *ac, const fujitsu_ac_remote_model_t model, const bool on, const stdAc::opmode_t mode, diff --git a/src/IRrecv.cpp b/src/IRrecv.cpp index 3cd88b377..f966daacd 100644 --- a/src/IRrecv.cpp +++ b/src/IRrecv.cpp @@ -1189,6 +1189,10 @@ bool IRrecv::decode(decode_results *results, irparams_t *save, DPRINTLN("Attempting BluestarHeavy decode"); if (decodeBluestarHeavy(results, offset, kBluestarHeavyBits)) return true; #endif // DECODE_BLUESTARHEAVY +#if DECODE_EUROM + DPRINTLN("Attempting Eurom decode"); + if (decodeEurom(results, offset, kEuromBits)) return true; +#endif // DECODE_EUROM // Typically new protocols are added above this line. } #if DECODE_HASH diff --git a/src/IRrecv.h b/src/IRrecv.h index a9cbce610..dabf95815 100644 --- a/src/IRrecv.h +++ b/src/IRrecv.h @@ -889,6 +889,12 @@ class IRrecv { const uint16_t nbits = kBluestarHeavyBits, const bool strict = true); #endif // DECODE_BLUESTARHEAVY +#if DECODE_EUROM + bool decodeEurom(decode_results *results, + uint16_t offset = kStartOffset, + const uint16_t nbits = kEuromBits, + const bool strict = true); +#endif // DECODE_EUROM }; #endif // IRRECV_H_ diff --git a/src/IRremoteESP8266.h b/src/IRremoteESP8266.h index 4a0e60bf2..4eae39076 100644 --- a/src/IRremoteESP8266.h +++ b/src/IRremoteESP8266.h @@ -959,6 +959,13 @@ #define SEND_BLUESTARHEAVY _IR_ENABLE_DEFAULT_ #endif // SEND_BLUESTARHEAVY +#ifndef DECODE_EUROM +#define DECODE_EUROM _IR_ENABLE_DEFAULT_ +#endif // DECODE_EUROM +#ifndef SEND_EUROM +#define SEND_EUROM _IR_ENABLE_DEFAULT_ +#endif // SEND_EUROM + #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 || \ @@ -978,6 +985,7 @@ DECODE_DAIKIN200 || DECODE_HAIER_AC160 || DECODE_TCL96AC || \ DECODE_BOSCH144 || DECODE_SANYO_AC152 || DECODE_DAIKIN312 || \ DECODE_CARRIER_AC84 || DECODE_YORK || DECODE_BLUESTARHEAVY || \ + DECODE_EUROM || \ 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 @@ -1145,8 +1153,9 @@ enum decode_type_t { CARRIER_AC84, // 125 YORK, BLUESTARHEAVY, + EUROM, // Add new entries before this one, and update it to point to the last entry. - kLastDecodeType = BLUESTARHEAVY, + kLastDecodeType = EUROM, }; // Message lengths & required repeat values @@ -1445,6 +1454,8 @@ const uint16_t kRhossDefaultRepeat = 0; const uint16_t kClimaButlerBits = 52; const uint16_t kYorkBits = 136; const uint16_t kYorkStateLength = 17; +const uint16_t kEuromStateLength = 12; +const uint16_t kEuromBits = kEuromStateLength * 8; // Legacy defines. (Deprecated) #define AIWA_RC_T501_BITS kAiwaRcT501Bits diff --git a/src/IRsend.cpp b/src/IRsend.cpp index 8b2309b8f..e2427eb88 100644 --- a/src/IRsend.cpp +++ b/src/IRsend.cpp @@ -721,6 +721,8 @@ uint16_t IRsend::defaultBits(const decode_type_t protocol) { return kDaikin64Bits; case ELECTRA_AC: return kElectraAcBits; + case EUROM: + return kEuromBits; case GREE: return kGreeBits; case HAIER_AC: @@ -1245,6 +1247,11 @@ bool IRsend::send(const decode_type_t type, const uint8_t *state, sendElectraAC(state, nbytes); break; #endif // SEND_ELECTRA_AC +#if SEND_EUROM + case EUROM: + sendEurom(state, nbytes); + break; +#endif // SEND_EUROM #if SEND_FUJITSU_AC case FUJITSU_AC: sendFujitsuAC(state, nbytes); diff --git a/src/IRsend.h b/src/IRsend.h index 80e5d65d5..a558001ad 100644 --- a/src/IRsend.h +++ b/src/IRsend.h @@ -899,6 +899,11 @@ class IRsend { const uint16_t nbytes = kBluestarHeavyStateLength, const uint16_t repeat = kNoRepeat); #endif // SEND_BLUESTARHEAVY +#if SEND_EUROM + void sendEurom(const uint8_t data[], + const uint16_t nbytes = kEuromStateLength, + const uint16_t repeat = kNoRepeat); +#endif // SEND_EUROM protected: #ifdef UNIT_TEST diff --git a/src/IRtext.cpp b/src/IRtext.cpp index f8a3290bb..bf179da24 100644 --- a/src/IRtext.cpp +++ b/src/IRtext.cpp @@ -561,6 +561,8 @@ IRTEXT_CONST_BLOB_DECL(kAllProtocolNamesStr) { D_STR_YORK, D_STR_UNSUPPORTED) "\x0" COND(DECODE_BLUESTARHEAVY || SEND_BLUESTARHEAVY, D_STR_BLUESTARHEAVY, D_STR_UNSUPPORTED) "\x0" + COND(DECODE_EUROM || SEND_EUROM, + D_STR_EUROM, D_STR_UNSUPPORTED) "\x0" ///< New protocol (macro) 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 299545484..a85f90d1c 100644 --- a/src/IRutils.cpp +++ b/src/IRutils.cpp @@ -184,6 +184,7 @@ bool hasACState(const decode_type_t protocol) { case DAIKIN216: case DAIKIN312: case ELECTRA_AC: + case EUROM: case FUJITSU_AC: case GREE: case HAIER_AC: diff --git a/src/ir_Eurom.cpp b/src/ir_Eurom.cpp new file mode 100644 index 000000000..c9b42dd90 --- /dev/null +++ b/src/ir_Eurom.cpp @@ -0,0 +1,489 @@ +// Copyright 2025 GottemHams +/// @file +/// @brief Support for Eurom A/C protocols. +/// @see https://eurom.nl/wp-content/uploads/2022/04/Polar-12C-16CH-v1.0.pdf + +#include "ir_Eurom.h" +#include +#include +#include +#include "IRrecv.h" +#include "IRsend.h" +#include "IRtext.h" +#include "IRutils.h" + +#if __cplusplus >= 201103L && defined(_GLIBCXX_USE_C99_MATH_TR1) + using std::roundf; +#else + using ::roundf; +#endif + +using irutils::uint8ToBcd; +using irutils::bcdToUint8; +using irutils::addBoolToString; +using irutils::addFanToString; +using irutils::addLabeledString; +using irutils::addModeToString; +using irutils::addTempToString; +using irutils::minsToString; + +#if SEND_EUROM +/// Send a Eurom formatted message. +/// Status: STABLE / Confirmed Working. +/// @param[in] data An array of bytes containing the IR command. +/// It is assumed to be in MSB order for this code. +/// e.g. +/// @code +/// unsigned char data[kEuromStateLength] = +/// {0x18,0x27,0x31,0x80,0x00,0x00,0x00,0x80,0x00,0x80,0x10,0x1D}; +/// @endcode +/// @param[in] nbytes The number of bytes of data in the array. +/// @param[in] repeat The number of times the command is to be repeated. +void IRsend::sendEurom(const uint8_t data[], const uint16_t nbytes, + const uint16_t repeat) { + // Check if we have enough bytes to send a proper message + if (nbytes < kEuromStateLength) + return; + + sendGeneric(kEuromHdrMark, kEuromHdrSpace, + kEuromBitMark, kEuromOneSpace, + kEuromBitMark, kEuromZeroSpace, + kEuromBitMark, kEuromSpaceGap, + data, nbytes, kEuromFreq, true, repeat, kDutyDefault); +} +#endif // SEND_EUROM + +#if DECODE_EUROM +/// Decode the supplied Eurom message. +/// Status: STABLE / Confirmed 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::decodeEurom(decode_results *results, uint16_t offset, + const uint16_t nbits, const bool strict) { + if (results->rawlen < nbits) + return false; // Too short a message to match + + if (strict && nbits != kEuromBits) + return false; + + if (!matchGeneric(results->rawbuf + offset, results->state, + results->rawlen - offset, nbits, + kEuromHdrMark, kEuromHdrSpace, + kEuromBitMark, kEuromOneSpace, + kEuromBitMark, kEuromZeroSpace, + kEuromBitMark, kEuromSpaceGap, true)) return false; + + // Success + results->bits = nbits; + results->decode_type = EUROM; + return true; +} +#endif // DECODE_EUROM + +/// 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? +IREuromAc::IREuromAc(const uint16_t pin, const bool inverted, + const bool use_modulation) + : _irsend(pin, inverted, use_modulation) { + stateReset(); +} + +/// Combine a mode flag and temperature into a single byte for the AC. +/// Note that validity is not checked again. +/// @param[in] mode A valid mode flag. +/// @param[in] celsius A valid temperature, i.e. within the proper range. +uint8_t IREuromAc::getModeCelsiusByte(const uint8_t mode, + const uint8_t celsius) const { + if (celsius >= kEuromMaxTempC) + return mode | kEuromMaxTempFlag; + return mode | ((celsius - kEuromMinTempC) << 4); +} + +/// Combine sleep mode and a timer duration into a single byte for the AC. +/// Note that validity is not checked again. +/// @param[in] sleep Whether sleep mode should be enabled. +/// @param[in] hours A valid duration, i.e. within the proper range. +uint8_t IREuromAc::getSleepOnTimerByte(const bool sleep, + const uint8_t hours) const { + uint8_t base = sleep ? kEuromSleepEnabled : kEuromSleepOnTimerDisabled; + return base + uint8ToBcd(hours); +} + +/// Reset the internals of the object to a known good state. +void IREuromAc::stateReset(void) { + _.Sum1 = 0x18; + _.Sum2 = 0x27; + // No need to call setMode() separately, is handled by setTemp() + setTemp(state_celsius_); // 23 C + _.Power_Swing = kEuromPowerSwingDisabled; + setSleep(state_sleep_); // false + // No need to call setOnTimer() separately, is handled by setSleep() + _.Sum3 = 0x00; + setOffTimer(0); + _.Sum4 = 0x80; + setFan(kEuromFanLow); +} + +#if SEND_EUROM +/// Send the current internal state as an IR message. +/// @param[in] repeat Number of times the message will be repeated. Note that +/// the original remote sends the same signal twice, but the +/// actual A/C works just fine if you send it once. +void IREuromAc::send(const uint16_t repeat) { + _irsend.sendEurom(getRaw(), kEuromStateLength, repeat); +} +#endif // SEND_EUROM + +/// Set up hardware to be able to send a message. +void IREuromAc::begin(void) { + _irsend.begin(); +} + +/// Calculate the checksum for the supplied state. +/// @param[in] state The source state to generate the checksum from. +/// @param[in] length Length of the supplied state to checksum. +/// @return The checksum value. +uint8_t IREuromAc::calcChecksum(const uint8_t state[], const uint16_t length) { + uint8_t checksum = irutils::sumNibbles(state + 1, length - 2); + checksum -= irutils::sumNibbles(state, 1); + return checksum; +} + +/// Verify if the checksum is valid for a given state. +/// @param[in] state The source state to verify the checksum of. +/// @param[in] length The size of the supplied state. +/// @return A boolean indicating if its checksum is valid. +bool IREuromAc::validChecksum(const uint8_t state[], const uint16_t length) { + return state[length - 1] == IREuromAc::calcChecksum(state, length); +} + +/// Update the checksum value for the current internal state. +void IREuromAc::checksum(void) { + _.Checksum = IREuromAc::calcChecksum(_.raw, kEuromStateLength); +} + +/// Set the raw state of the remote. +/// @param[in] state The raw state from the native IR message. +void IREuromAc::setRaw(const uint8_t state[]) { + std::memcpy(_.raw, state, kEuromStateLength); +} + +/// Get the raw state of the remote, suitable to be sent with the appropriate +/// IRsend object method. +/// @return A PTR to the internal state. +uint8_t *IREuromAc::getRaw(void) { + checksum(); // Let's ensure this is updated before returning + return _.raw; +} + +/// Set the internal state to powered on. +void IREuromAc::on(void) { + setPower(true); +} + +/// Set the internal state to powered off. +void IREuromAc::off(void) { + setPower(false); +} + +/// Set the internal state to use the desired power setting. +/// @param[in] state The desired power setting. +void IREuromAc::setPower(const bool state) { + // We'll also have to preserve the swing state + if (state) + _.Power_Swing |= kEuromPowerOn; + else + _.Power_Swing &= kEuromSwingOn; +} + +/// Get the current power setting from the internal state. +/// @return A boolean indicating the current power setting. +bool IREuromAc::getPower(void) const { + return (_.Power_Swing & kEuromPowerOn) == kEuromPowerOn; +} + +/// Set the internal state to use the desired operation mode. +/// @param[in] mode The desired operation mode. +void IREuromAc::setMode(const uint8_t mode) { + switch (mode) { + case kEuromCool: + case kEuromHeat: + state_mode_ = mode; + _.Mode_Celsius = getModeCelsiusByte(mode, state_celsius_); + break; + case kEuromDehumidify: + case kEuromVentilate: + state_mode_ = mode; + _.Mode_Celsius = mode; + break; + default: + break; + } +} + +/// Get the current operation mode setting from the internal state. +/// @return The current operation mode. +uint8_t IREuromAc::getMode(void) const { + return state_mode_; +} + +/// Set the internal state to use the desired temperature. +/// @param[in] degrees The desired temperature in degrees, normally Celsius. +/// @param[in] fahrenheit If the given temperature is in Fahrenheit instead. +void IREuromAc::setTemp(const uint8_t degrees, const bool fahrenheit) { + if (state_mode_ != kEuromCool && state_mode_ != kEuromHeat) + return; + + uint8_t temp_c, temp_f; + if (fahrenheit) { + temp_f = std::max(kEuromMinTempF, degrees); + temp_f = std::min(kEuromMaxTempF, temp_f); + temp_c = static_cast(roundf(fahrenheitToCelsius(temp_f))); + _.Fahrenheit = kEuromFahrenheitEnabled + temp_f; + } else { + temp_c = degrees; + _.Fahrenheit = kEuromFahrenheitDisabled; + } + + temp_c = std::max(kEuromMinTempC, temp_c); + temp_c = std::min(kEuromMaxTempC, temp_c); + state_celsius_ = temp_c; + _.Mode_Celsius = getModeCelsiusByte(state_mode_, temp_c); +} + +/// Get the current temperature from the internal state. +/// @return The current temperature, which can be either Celsius or Fahrenheit, +/// depending on what was used with setTemp(). See also: getTempIsFahrenheit(). +uint8_t IREuromAc::getTemp(void) const { + if (state_mode_ != kEuromCool && state_mode_ != kEuromHeat) + return 0; // Not supported in other modes + if (getTempIsFahrenheit()) + return _.Fahrenheit - kEuromFahrenheitEnabled; + return state_celsius_; +} + +/// Check if Fahrenheit is currently being used by the internal state. +/// @return A boolean indicating if the current temperature is in Fahrenheit. +bool IREuromAc::getTempIsFahrenheit(void) const { + return _.Fahrenheit != kEuromFahrenheitDisabled; +} + +/// Set the internal state to use the desired fan speed. +/// @param[in] speed The desired fan speed. +void IREuromAc::setFan(const uint8_t speed) { + switch (speed) { + case kEuromFanLow: + case kEuromFanMed: + case kEuromFanHigh: + _.Fan = speed; + break; + default: + break; + } +} + +/// Get the current fan speed from the internal state. +/// @return The current fan speed. +uint8_t IREuromAc::getFan(void) const { + return _.Fan; +} + +/// Set the internal state to use the desired swing setting. +/// @param[in] state The desired swing setting. +void IREuromAc::setSwing(const bool state) { + if (state) + _.Power_Swing |= kEuromSwingOn; + else + _.Power_Swing &= kEuromPowerOn; +} + +/// Get the current swing setting from the internal state. +/// @return A boolean indicating the current swing setting. +bool IREuromAc::getSwing(void) const { + return (_.Power_Swing & kEuromSwingOn) == kEuromSwingOn; +} + +/// Set the internal state to use the desired sleep setting. +/// @param[in] state The desired sleep setting. +void IREuromAc::setSleep(const bool state) { + state_sleep_ = state; + _.Sleep_OnTimer = getSleepOnTimerByte(state, state_on_timer_); +} + +/// Get the current sleep setting from the internal state. +/// @return A boolean indicating the current sleep setting. +bool IREuromAc::getSleep(void) const { + return state_sleep_; +} + +/// Set the internal state to use the desired "off timer" duration. +/// @param[in] duration The desired duration, in hours. +void IREuromAc::setOffTimer(const uint8_t duration) { + uint8_t hours = std::max(kEuromTimerMin, duration); + hours = std::min(kEuromTimerMax, hours); + _.OffTimer = kEuromOffTimer + uint8ToBcd(hours); + _.OffTimerEnabled = hours ? kEuromOffTimer : kEuromOffTimerDisabled; +} + +/// Get the current "off timer" duration from the internal state. +/// @return The current duration, in hours. +uint8_t IREuromAc::getOffTimer(void) const { + return bcdToUint8(_.OffTimer - kEuromOffTimer); +} + +/// Set the internal state to use the desired "on timer" duration. +/// @param[in] duration The desired duration, in hours. +void IREuromAc::setOnTimer(const uint8_t duration) { + uint8_t hours = std::max(kEuromTimerMin, duration); + hours = std::min(kEuromTimerMax, hours); + state_on_timer_ = hours; + _.Sleep_OnTimer = getSleepOnTimerByte(state_sleep_, hours); +} + +/// Get the current "on timer" duration from the internal state. +/// @return The current duration, in hours. +uint8_t IREuromAc::getOnTimer(void) const { + return state_on_timer_; +} + +/// Convert a stdAc::opmode_t enum into its native operation mode. +/// @param[in] mode The enum to be converted. +/// @return The native equivalent of the enum. +uint8_t IREuromAc::convertMode(const stdAc::opmode_t mode) { + // This particular A/C doesn't actually have an 'Auto' mode, so we'll just use + // the normal fan mode instead + switch (mode) { + case stdAc::opmode_t::kCool: + return kEuromCool; + case stdAc::opmode_t::kHeat: + return kEuromHeat; + case stdAc::opmode_t::kDry: + return kEuromDehumidify; + default: + return kEuromVentilate; + } +} + +/// Convert a stdAc::fanspeed_t enum into its native speed. +/// @param[in] speed The enum to be converted. +/// @return The native equivalent of the enum. +uint8_t IREuromAc::convertFan(const stdAc::fanspeed_t speed) { + // This particular A/C doesn't actually have an 'Auto' mode, so we'll just use + // the lowest fan speed instead + switch (speed) { + case stdAc::fanspeed_t::kHigh: + case stdAc::fanspeed_t::kMax: + return kEuromFanHigh; + case stdAc::fanspeed_t::kMedium: + return kEuromFanMed; + default: + return kEuromFanLow; + } +} + +/// Convert a stdAc::swingv_t enum into its native swing. +/// @param[in] swing The enum to be converted. +/// @return The native equivalent of the enum. +bool IREuromAc::convertSwing(const stdAc::swingv_t swing) { + // The only choice is on or off, so let's just treat the former as auto mode + switch (swing) { + case stdAc::swingv_t::kAuto: + return true; + default: + return false; + } +} + +/// Convert a native operation mode into its stdAc enum equivalent. +/// @param[in] mode The native operation mode setting to be converted. +/// @return The stdAc enum equivalent of the native setting. +stdAc::opmode_t IREuromAc::toCommonMode(const uint8_t mode) { + // This particular A/C doesn't actually have an 'Auto' mode, so we'll just use + // the normal fan mode instead. To make this more clear, 'kEuromVentilate' is + // explicitly included in the switch (instead of being omitted and implicitly + // handled via the default case). + switch (mode) { + case kEuromCool: + return stdAc::opmode_t::kCool; + case kEuromHeat: + return stdAc::opmode_t::kHeat; + case kEuromDehumidify: + return stdAc::opmode_t::kDry; + case kEuromVentilate: + default: + return stdAc::opmode_t::kFan; + } +} + +/// Convert a native fan speed into its stdAc enum equivalent. +/// @param[in] speed The native speed setting to be converted. +/// @return The stdAc enum equivalent of the native setting. +stdAc::fanspeed_t IREuromAc::toCommonFanSpeed(const uint8_t speed) { + // This particular A/C doesn't actually have an 'Auto' mode, so we'll just use + // the lowest speed instead. To make this more clear, 'kEuromFanLow' is + // explicitly included in the switch (instead of being omitted and implicitly + // handled via the default case). + switch (speed) { + case kEuromFanHigh: + return stdAc::fanspeed_t::kMax; + case kEuromFanMed: + return stdAc::fanspeed_t::kMedium; + case kEuromFanLow: + default: + return stdAc::fanspeed_t::kMin; + } +} + +/// Convert a native swing setting into its stdAc enum equivalent. +/// @param[in] swing The native swing setting to be converted. +/// @return The stdAc enum equivalent of the native setting. +stdAc::swingv_t IREuromAc::toCommonSwing(const bool swing) { + // The only choice is on or off, so let's just treat the former as auto mode + return swing ? stdAc::swingv_t::kAuto : stdAc::swingv_t::kOff; +} + +/// Convert the current internal state into its stdAc::state_t equivalent. +/// @return The stdAc struct equivalent of the native settings. +stdAc::state_t IREuromAc::toCommon(void) const { + stdAc::state_t result{}; + result.protocol = EUROM; + result.power = getPower(); + result.mode = toCommonMode(getMode()); + result.degrees = getTemp(); + result.celsius = !getTempIsFahrenheit(); + result.fanspeed = toCommonFanSpeed(getFan()); + result.swingv = toCommonSwing(getSwing()); + result.sleep = getSleep(); + return result; +} + +/// Convert the current internal state into a human-readable string. +/// @return A human-readable string. +String IREuromAc::toString(void) const { + String result = ""; + result.reserve(70); // Reserve some heap for the string to reduce fragging + result += addBoolToString(getPower(), kPowerStr, false); + result += addModeToString(getMode(), 0xFF, kEuromCool, + kEuromHeat, kEuromDehumidify, kEuromVentilate); + result += addTempToString(getTemp(), !getTempIsFahrenheit()); + result += addFanToString(getFan(), kEuromFanHigh, kEuromFanLow, + 0xFF, 0xFF, + kEuromFanMed); + + result += addBoolToString(getSwing(), kSwingVStr); + result += addBoolToString(getSleep(), kSleepStr); + + uint8_t off_timer_min = getOffTimer() * 60; + uint8_t on_timer_min = getOnTimer() * 60; + String off_timer_str = off_timer_min ? minsToString(off_timer_min) : kOffStr; + String on_timer_str = on_timer_min ? minsToString(on_timer_min) : kOffStr; + result += addLabeledString(off_timer_str, kOffTimerStr); + result += addLabeledString(on_timer_str, kOnTimerStr); + return result; +} diff --git a/src/ir_Eurom.h b/src/ir_Eurom.h new file mode 100644 index 000000000..01cd5e5b2 --- /dev/null +++ b/src/ir_Eurom.h @@ -0,0 +1,244 @@ +// Copyright 2025 GottemHams +/// @file +/// @brief Support for Eurom A/C protocols. +/// @see https://eurom.nl/wp-content/uploads/2022/04/Polar-12C-16CH-v1.0.pdf + +// Supports: +// Brand: Eurom, Model: Polar 16CH + +#ifndef IR_EUROM_H_ +#define IR_EUROM_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 + +/// Native representation of a Eurom message. +union EuromProtocol { + uint8_t raw[kEuromStateLength]; // The state of the IR remote + struct { + // Byte 0 is used as a negative offset for the checksum and is always 0x18 + uint8_t Sum1 :8; + + // Byte 1 is used as part of the checksum only and is always 0x27 + uint8_t Sum2 :8; + + // Byte 2 combines 2 functions and has some considerations: + // 1. Cooling mode almost always has the lower nibble set to 0x1, + // e.g. 0x01 = 16 C, 0x11 = 17 C, 0xF1 = 31 C. + // Exception: 0x09 means 32 C (max temperature). + // 2. Dehumidification doesn't support temperatures, so this is always 0x72. + // 3. Same goes for fan mode, which is always 0x73. + // 4. Heating mode almost always has the lower nibble set to 0x4, + // e.g. 0x04 = 16 C, 0x14 = 17 C, 0xF4 = 31 C. + // Exception: 0x0C means 32 C (max temperature). + uint8_t Mode_Celsius :8; + + // Byte 3 also combines 2 functions, with the values being OR'ed together: + // 1. 0x00 means power off, swing off + // 2. 0x40 means power off, swing on + // 3. 0x80 means power on, swing off + // 3. 0xC0 means power on, swing on + uint8_t Power_Swing :8; + + // Byte 4 is to track Fahrenheit separately, but note that it will always + // reset to 0x00 if Celsius is used. On the other hand, Celsius moves along + // with this, i.e. a change of +1/-1 C for roughly every 3 F. The base value + // is 0x41 which corresponds to 61 F and increases by 0x01 for every degree. + // This gives it a range of 0x41 - 0x5E (inclusive). + uint8_t Fahrenheit :8; + + // Byte 5 yet again combines functions: + // 1. 0x00 for sleep mode disabled, 0x40 for enabled + // 2. The timer duration is simply encoded as BCD and added to this, with a + // maximum of 24 hours + uint8_t Sleep_OnTimer :8; + + // Byte 6 seems to be truly unused, since it's always 0x00. We'll still + // always use it in checksums though. + uint8_t Sum3 :8; + + // Byte 7 is always at least 0x80, with the hours also being added as BCD, + // e.g. 0x80 = 0 hours, 0x81 = 1 h, 0xA4 = 24 h. + uint8_t OffTimer :8; + + // Byte 8 doesn't really seem to matter, but it should be 0x00 or 0x80 for + // off and on respectively. Apparently setting the **duration** alone is + // already enough to set the timer? + uint8_t OffTimerEnabled :8; + + // Byte 9 is used as part of the checksum only and is slways 0x80 + uint8_t Sum4 :8; + + // Byte 10 is simple: 0x10, 0x20, 0x40 for low, medium and high respectively + uint8_t Fan :8; + + // Byte 11 holds a funny checksum. =] + // Add all nibbles beyond the first byte (excluding the checksum of course), + // then subtract the first byte. The second byte should always be larger, so + // this never results in sudden signedness (i.e. underflowing). It might be + // pure coincidence that the first byte is always 0x18 and they could have + // hardcoded that value elsewhere/otherwise. + uint8_t Checksum :8; + }; +}; + +// Constants + +// IR signal information +const uint16_t kEuromHdrMark = 3257; +const uint16_t kEuromBitMark = 454; +const uint16_t kEuromHdrSpace = 3187; +const uint16_t kEuromOneSpace = 1162; +const uint16_t kEuromZeroSpace = 355; +const uint16_t kEuromSpaceGap = 50058; +const uint16_t kEuromFreq = 38000; + +// Modes +const uint8_t kEuromCool = 0x01; // Lowest possible value, 16 C +const uint8_t kEuromDehumidify = 0x72; +const uint8_t kEuromVentilate = 0x73; +const uint8_t kEuromHeat = 0x04; // Also 16 C + +// Reaching the highest temperature breaks the formula that is used otherwise, +// because we should basically just OR this flag to the above mode byte. It +// seems more like it indicates "max temp" instead of "32 C". +const uint8_t kEuromMaxTempFlag = 0x08; + +// Temperatures +const uint8_t kEuromMinTempC = 16; +const uint8_t kEuromMaxTempC = 32; + +const uint8_t kEuromMinTempF = 61; +const uint8_t kEuromMaxTempF = 90; + +// The enabled flag will simply be added to chosen temperature +const uint8_t kEuromFahrenheitDisabled = 0x00; +const uint8_t kEuromFahrenheitEnabled = 0x04; + +// Power and swing +const uint8_t kEuromPowerSwingDisabled = 0x00; +const uint8_t kEuromPowerOn = 0x80; +const uint8_t kEuromSwingOn = 0x40; + +// Sleep mode and the "on timer" +const uint8_t kEuromSleepOnTimerDisabled = 0x00; +const uint8_t kEuromSleepEnabled = 0x40; + +// The "off timer" +const uint8_t kEuromOffTimerDisabled = 0x00; +const uint8_t kEuromOffTimerEnabled = 0x80; +const uint8_t kEuromOffTimer = kEuromOffTimerEnabled; // Corresponds to 0 hours + +// Stuff for all timers +const uint8_t kEuromTimerMin = 0; +const uint8_t kEuromTimerMax = 24; + +// Fan speeds +const uint8_t kEuromFanLow = 0x10; +const uint8_t kEuromFanMed = 0x20; +const uint8_t kEuromFanHigh = 0x40; + +// Classes + +/// Class for handling detailed Eurom A/C messages. +class IREuromAc { + public: + explicit IREuromAc(const uint16_t pin, const bool inverted = false, + const bool use_modulation = true); + + void stateReset(); +#if SEND_EUROM + 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 65 ms IR signal pulse at 38 kHz. + /// Only ever needs to be run once per object instantiation, if at all. + int8_t calibrate(void) { + return _irsend.calibrate(); + } +#endif // SEND_EUROM + + void begin(void); + static uint8_t calcChecksum(const uint8_t state[], + const uint16_t length = kEuromStateLength); + static bool validChecksum(const uint8_t state[], + const uint16_t length = kEuromStateLength); + + void setRaw(const uint8_t state[]); + uint8_t *getRaw(void); + + void on(void); + void off(void); + + void setPower(const bool state); + bool getPower(void) const; + + void setMode(const uint8_t mode); + uint8_t getMode(void) const; + + void setTemp(const uint8_t degrees, const bool fahrenheit = false); + uint8_t getTemp(void) const; + bool getTempIsFahrenheit(void) const; + + void setFan(const uint8_t speed); + uint8_t getFan(void) const; + + void setSwing(const bool state); + bool getSwing(void) const; + + void setSleep(const bool state); + bool getSleep(void) const; + + void setOffTimer(const uint8_t duration); + uint8_t getOffTimer(void) const; + + void setOnTimer(const uint8_t duration); + uint8_t getOnTimer(void) const; + + static uint8_t convertMode(const stdAc::opmode_t mode); + static uint8_t convertFan(const stdAc::fanspeed_t speed); + static bool convertSwing(const stdAc::swingv_t swing); + + static stdAc::opmode_t toCommonMode(const uint8_t mode); + static stdAc::fanspeed_t toCommonFanSpeed(const uint8_t speed); + static stdAc::swingv_t toCommonSwing(const bool swing); + + stdAc::state_t toCommon(void) const; + String toString(void) const; +#ifndef UNIT_TEST + + private: + IRsend _irsend; +#else + /// @cond IGNORE + IRsendTest _irsend; + /// @endcond +#endif + EuromProtocol _; + + // Due to some bytes combining multiple functions, we'll need to keep track of + // some of the original values ourselves. Otherwise we wouldn't really be able + // to e.g. return the current mode or temperature, or changing the sleep mode + // without also messing with the timer hours. + uint8_t state_mode_ = kEuromCool; + uint8_t state_celsius_ = 23; + bool state_sleep_ = false; + uint8_t state_on_timer_ = kEuromTimerMin; + + // Some helper functions for reusing the above state variables depending on + // context and returning the byte expected by the AC. + uint8_t getModeCelsiusByte(const uint8_t mode, const uint8_t celsius) const; + uint8_t getSleepOnTimerByte(const bool sleep, const uint8_t hours) const; + + void checksum(void); +}; + +#endif // IR_EUROM_H_ diff --git a/src/locale/defaults.h b/src/locale/defaults.h index a1329a97c..68b1a7c74 100644 --- a/src/locale/defaults.h +++ b/src/locale/defaults.h @@ -844,6 +844,9 @@ D_STR_INDIRECT " " D_STR_MODE #ifndef D_STR_EPSON #define D_STR_EPSON "EPSON" #endif // D_STR_EPSON +#ifndef D_STR_EUROM +#define D_STR_EUROM "EUROM" +#endif // D_STR_EUROM #ifndef D_STR_FUJITSU_AC #define D_STR_FUJITSU_AC "FUJITSU_AC" #endif // D_STR_FUJITSU_AC diff --git a/test/ir_Eurom_test.cpp b/test/ir_Eurom_test.cpp new file mode 100644 index 000000000..2d3fdd653 --- /dev/null +++ b/test/ir_Eurom_test.cpp @@ -0,0 +1,377 @@ +// Copyright 2025 GottemHams + +#include "ir_Eurom.h" +#include "gtest/gtest.h" +#include "IRac.h" +#include "IRrecv.h" +#include "IRrecv_test.h" +#include "IRsend.h" +#include "IRsend_test.h" + +TEST(TestEurom, Housekeeping) { + ASSERT_EQ("EUROM", typeToString(decode_type_t::EUROM)); + ASSERT_EQ(decode_type_t::EUROM, strToDecodeType("EUROM")); + ASSERT_TRUE(hasACState(decode_type_t::EUROM)); + ASSERT_TRUE(IRac::isProtocolSupported(decode_type_t::EUROM)); + ASSERT_EQ(kEuromBits, IRsend::defaultBits(decode_type_t::EUROM)); +} + +/// Tests for sendEurom(). + +/// Test sending typical data only. +TEST(TestSendEurom, SendDataOnly) { + IRsendTest irsend(kGpioUnused); + + const uint8_t state[kEuromStateLength] = { + 0x18, 0x27, + 0x71, // Cooling mode, 23 C + 0x80, // Power on, swing off + 0x00, // No Fahrenheit + 0x00, // Sleep disabled, no "on timer" + 0x00, + 0x80, // No "off timer" + 0x00, // "Off timer" disabled + 0x80, + 0x10, // Low fan + 0x21, // Checksum + }; + + irsend.begin(); + irsend.reset(); + irsend.sendEurom(state); + EXPECT_EQ( + "f38000d50" + "m3257s3187" + "m454s355m454s355m454s355m454s1162m454s1162m454s355m454s355m454s355m454s355" + "m454s355m454s1162m454s355m454s355m454s1162m454s1162m454s1162m454s355m454" + "s1162m454s1162m454s1162m454s355m454s355m454s355m454s1162m454s1162m454s355" + "m454s355m454s355m454s355m454s355m454s355m454s355m454s355m454s355m454s355" + "m454s355m454s355m454s355m454s355m454s355m454s355m454s355m454s355m454s355" + "m454s355m454s355m454s355m454s355m454s355m454s355m454s355m454s355m454s355" + "m454s355m454s355m454s355m454s1162m454s355m454s355m454s355m454s355m454s355" + "m454s355m454s355m454s355m454s355m454s355m454s355m454s355m454s355m454s355" + "m454s355m454s1162m454s355m454s355m454s355m454s355m454s355m454s355m454s355" + "m454s355m454s355m454s355m454s1162m454s355m454s355m454s355m454s355m454s355" + "m454s355m454s1162m454s355m454s355m454s355m454s355m454s1162m454" + "s50058", + irsend.outputStr()); + + irsend.reset(); +} + +/// Tests for decodeEurom(). + +/// Decode a normal Eurom message. +TEST(TestDecodeEurom, SyntheticExample) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + + // This is the same state as used in SendDataOnly + const uint8_t state[kEuromStateLength] = { + 0x18, 0x27, + 0x71, // Cooling mode, 23 C + 0x80, // Power on, swing off + 0x00, // No Fahrenheit + 0x00, // Sleep disabled, no "on timer" + 0x00, + 0x80, // No "off timer" + 0x00, // "Off timer" disabled + 0x80, + 0x10, // Low fan + 0x21, // Checksum + }; + + irsend.begin(); + irsend.reset(); + irsend.sendEurom(state); + irsend.makeDecodeResult(); + + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(decode_type_t::EUROM, irsend.capture.decode_type); + EXPECT_EQ(kEuromBits, irsend.capture.bits); + EXPECT_FALSE(irsend.capture.repeat); + + EXPECT_STATE_EQ(state, irsend.capture.state, irsend.capture.bits); + EXPECT_EQ( + "Power: On, Mode: 1 (Cool), Temp: 23C, Fan: 16 (Low), Swing(V): Off" + ", Sleep: Off, Off Timer: Off, On Timer: Off", + IRAcUtils::resultAcToString(&irsend.capture)); + + stdAc::state_t r, p; + ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &r, &p)); + + irsend.reset(); +} + +/// Decode a real example. +TEST(TestDecodeEurom, RealExample) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + + // UNKNOWN 1C60B17 + const uint16_t raw_data[391] = { + 3318, 3136, + 490, 318, 490, 322, 486, 324, 488, 1126, 490, 1118, 490, 322, 490, 318, 490, + 326, 490, 318, 488, 322, 486, 1124, 490, 328, 488, 318, 490, 1124, 490, + 1122, 486, 1130, 484, 324, 490, 1122, 486, 1122, 490, 1128, 490, 322, 486, + 322, 490, 322, 486, 1132, 484, 1124, 488, 1124, 486, 322, 490, 328, 488, + 318, 490, 322, 486, 322, 490, 328, 486, 322, 490, 322, 486, 322, 486, 330, + 486, 322, 486, 322, 490, 322, 486, 332, 486, 322, 486, 322, 490, 322, 484, + 328, 490, 322, 486, 322, 486, 322, 490, 326, 484, 324, 488, 324, 484, 324, + 484, 332, 486, 322, 486, 326, 486, 322, 486, 330, 486, 1122, 490, 324, 486, + 322, 484, 332, 486, 322, 486, 328, 484, 322, 486, 332, 484, 322, 490, 324, + 484, 322, 486, 332, 486, 326, 486, 322, 486, 322, 484, 336, 486, 1124, 486, + 348, 458, 328, 486, 330, 486, 350, 458, 326, 486, 324, 484, 332, 484, 326, + 486, 322, 486, 322, 486, 1136, 486, 322, 484, 348, 438, 348, 484, 332, 484, + 348, 460, 326, 486, 1122, 486, 336, 486, 344, 464, 1148, 438, 348, 486, + 1130, 486, + 50010, + 3308, 3140, + 490, 322, 486, 322, 486, 322, 486, 1132, 490, 1122, 486, 324, 484, 328, 484, + 328, 490, 322, 486, 322, 486, 1126, 486, 328, 488, 324, 484, 1124, 490, + 1122, 486, 1132, 486, 322, 490, 1122, 486, 1128, 484, 1132, 486, 322, 486, + 322, 490, 322, 486, 1132, 486, 1122, 490, 1122, 486, 322, 490, 326, 486, + 328, 484, 324, 486, 322, 484, 332, 486, 322, 486, 328, 484, 322, 486, 330, + 486, 324, 484, 322, 486, 326, 486, 328, 484, 328, 486, 324, 484, 322, 486, + 330, 486, 322, 486, 326, 486, 322, 486, 332, 486, 322, 484, 348, 460, 328, + 486, 330, 482, 326, 486, 322, 486, 322, 486, 330, 486, 1128, 484, 324, 484, + 322, 486, 332, 484, 348, 460, 328, 484, 324, 484, 358, 460, 326, 486, 322, + 486, 348, 460, 336, 480, 328, 484, 348, 460, 328, 480, 336, 486, 1148, 460, + 328, 484, 324, 484, 358, 460, 326, 482, 326, 486, 348, 458, 336, 480, 348, + 464, 348, 460, 328, 480, 1136, 484, 348, 460, 328, 480, 328, 484, 358, 460, + 326, 482, 348, 460, 1152, 460, 356, 460, 354, 458, 1148, 460, 328, 484, + 1162, 460, + }; + + // Note that this is a different state than before + const uint8_t expected_state[kEuromStateLength] = { + 0x18, 0x27, 0x71, 0xC0, 0x00, 0x00, 0x00, 0x80, 0x00, 0x80, 0x10, 0x25, + }; + + irsend.begin(); + irsend.reset(); + irsend.sendRaw(raw_data, 391, kEuromFreq); + irsend.makeDecodeResult(); + + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(decode_type_t::EUROM, irsend.capture.decode_type); + EXPECT_EQ(kEuromBits, irsend.capture.bits); + EXPECT_FALSE(irsend.capture.repeat); + + EXPECT_STATE_EQ(expected_state, irsend.capture.state, irsend.capture.bits); + EXPECT_EQ( + "Power: On, Mode: 1 (Cool), Temp: 23C, Fan: 16 (Low), Swing(V): On" + ", Sleep: Off, Off Timer: Off, On Timer: Off", + IRAcUtils::resultAcToString(&irsend.capture)); + + stdAc::state_t r, p; + ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &r, &p)); + + irsend.reset(); +} + +/// Decode a real example without repeat. +TEST(TestDecodeEurom, RealExampleNoRepeat) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + + // UNKNOWN 14601C1D + const uint16_t raw_data[195] = { + 3260, 3186, + 468, 344, 438, 374, 438, 370, 438, 1178, 438, 1174, 438, 374, 438, 370, 438, + 378, 438, 370, 438, 374, 438, 1174, 438, 378, 438, 370, 438, 1174, 438, + 1170, 438, 1184, 438, 370, 438, 1174, 438, 370, 438, 1182, 434, 374, 438, + 374, 434, 374, 438, 1178, 438, 374, 438, 370, 438, 374, 434, 382, 434, 374, + 438, 374, 434, 374, 438, 378, 438, 370, 438, 374, 434, 374, 438, 378, 438, + 370, 438, 374, 434, 374, 438, 378, 438, 370, 438, 374, 438, 370, 438, 378, + 438, 374, 434, 374, 438, 370, 438, 378, 438, 374, 434, 374, 438, 374, 434, + 378, 438, 374, 438, 370, 438, 374, 434, 382, 434, 1174, 438, 374, 438, 370, + 438, 378, 438, 370, 438, 374, 438, 370, 438, 382, 434, 374, 438, 370, 438, + 374, 438, 378, 438, 374, 434, 374, 438, 370, 438, 382, 434, 1174, 464, 348, + 434, 374, 438, 382, 434, 374, 438, 370, 438, 374, 438, 378, 438, 374, 434, + 374, 438, 370, 438, 1182, 438, 374, 434, 374, 438, 370, 438, 382, 434, 374, + 438, 374, 434, 374, 438, 1182, 434, 374, 438, 1174, 434, 1174, 438, 1182, + 434, + }; + + // This is also another state than all the earlier tests + const uint8_t expected_state[kEuromStateLength] = { + 0x18, 0x27, 0x51, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x80, 0x10, 0x17, + }; + + irsend.begin(); + irsend.reset(); + irsend.sendRaw(raw_data, 195, kEuromFreq); + irsend.makeDecodeResult(); + + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(decode_type_t::EUROM, irsend.capture.decode_type); + EXPECT_EQ(kEuromBits, irsend.capture.bits); + EXPECT_FALSE(irsend.capture.repeat); + + EXPECT_STATE_EQ(expected_state, irsend.capture.state, irsend.capture.bits); + EXPECT_EQ( + "Power: Off, Mode: 1 (Cool), Temp: 23C, Fan: 16 (Low), Swing(V): Off" + ", Sleep: Off, Off Timer: Off, On Timer: Off", + IRAcUtils::resultAcToString(&irsend.capture)); + + stdAc::state_t r, p; + ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &r, &p)); + + irsend.reset(); +} + +/// Tests for the IREuromAc class. + +/// Test power setting and getting. +TEST(TestEuromAc, SetAndGetPower) { + IREuromAc ac(kGpioUnused); + + // The initial state is powered off + ASSERT_FALSE(ac.getPower()); + + ac.setPower(true); + EXPECT_TRUE(ac.getPower()); +} + +/// Test operation mode setting and getting. +TEST(TestEuromAc, SetAndGetMode) { + IREuromAc ac(kGpioUnused); + + // The initial state is cooling mode + ASSERT_EQ(kEuromCool, ac.getMode()); + + // Temperature is not used in dehumidification/ventilation modes + ac.setMode(kEuromDehumidify); + EXPECT_EQ(kEuromDehumidify, ac.getMode()); + + ac.setMode(kEuromVentilate); + EXPECT_EQ(kEuromVentilate, ac.getMode()); + + ac.setMode(kEuromHeat); + EXPECT_EQ(kEuromHeat, ac.getMode()); +} + +/// Test temperature setting and getting. +TEST(TestEuromAc, SetAndGetTemperature) { + IREuromAc ac(kGpioUnused); + + // The initial state is 23 C + ASSERT_FALSE(ac.getTempIsFahrenheit()); + ASSERT_EQ(23, ac.getTemp()); + + ac.setTemp(22); + ASSERT_FALSE(ac.getTempIsFahrenheit()); + EXPECT_EQ(22, ac.getTemp()); +} + +/// Test temperature setting and getting with Fahrenheit. +TEST(TestEuromAc, SetAndGetTemperatureFahrenheit) { + IREuromAc ac(kGpioUnused); + + // The initial state is not using Fahrenheit + ASSERT_FALSE(ac.getTempIsFahrenheit()); + + // This corresponds to 16 C + ac.setTemp(70, true); + ASSERT_TRUE(ac.getTempIsFahrenheit()); + EXPECT_EQ(70, ac.getTemp()); +} + +/// Test fan speed setting and getting. +TEST(TestEuromAc, SetAndGetFan) { + IREuromAc ac(kGpioUnused); + + // The initial state is low fan + ASSERT_EQ(kEuromFanLow, ac.getFan()); + + ac.setFan(kEuromFanHigh); + EXPECT_EQ(kEuromFanHigh, ac.getFan()); +} + +/// Test swing setting and getting. +TEST(TestEuromAc, SetAndGetSwing) { + IREuromAc ac(kGpioUnused); + + // The initial state is swing disabled + ASSERT_FALSE(ac.getSwing()); + + ac.setSwing(true); + EXPECT_TRUE(ac.getSwing()); +} + +/// Test sleep mode setting and getting. +TEST(TestEuromAc, SetAndGetSleep) { + IREuromAc ac(kGpioUnused); + + // The initial state is sleep disabled + ASSERT_FALSE(ac.getSleep()); + + ac.setSleep(true); + EXPECT_TRUE(ac.getSleep()); +} + +/// Test "off timer" setting and getting. +TEST(TestEuromAc, SetAndGetOffTimer) { + IREuromAc ac(kGpioUnused); + + // The initial state is no timer + ASSERT_EQ(kEuromTimerMin, ac.getOffTimer()); + + ac.setOffTimer(kEuromTimerMax); + EXPECT_EQ(kEuromTimerMax, ac.getOffTimer()); +} + +/// Test "on timer" setting and getting. +TEST(TestEuromAc, SetAndGetOnTimer) { + IREuromAc ac(kGpioUnused); + + // The initial state is no timer + ASSERT_EQ(kEuromTimerMin, ac.getOnTimer()); + + ac.setOnTimer(kEuromTimerMax); + EXPECT_EQ(kEuromTimerMax, ac.getOnTimer()); +} + +/// Test checksumming for the initial state. +TEST(TestEuromAc, ChecksumInitial) { + IREuromAc ac(kGpioUnused); + + // The initial state is powered off, cooling mode, 23 C, low fan, swing and + // sleep disabled, no timers + const uint8_t *raw_state = ac.getRaw(); + ASSERT_EQ(0x19, raw_state[kEuromStateLength - 1]); +} + +/// Test checksumming with every "feature" set to the "highest" value, which +/// also includes the special case of using the max temperature. +TEST(TestEuromAc, ChecksumHigh) { + IREuromAc ac(kGpioUnused); + + // The initial state is powered off, cooling mode, 23 C, low fan, swing and + // sleep disabled, no timers + ac.setPower(true); + ac.setMode(kEuromHeat); + ac.setTemp(kEuromMaxTempF, true); + ac.setFan(kEuromFanHigh); + ac.setSwing(true); + ac.setSleep(true); + ac.setOffTimer(kEuromTimerMax); + ac.setOnTimer(kEuromTimerMax); + + const uint8_t expected_state[kEuromStateLength] = { + 0x18, 0x27, + 0x0C, // Heating mode, 32 C (changing Fahrenheit updates this too) + 0xC0, // Power on, swing on + 0x5E, // 90 F + 0x64, // Sleep enabled, "on timer" set to 24 hours + 0x00, + 0xA4, // "Off timer" set to 24 hours + 0x80, // "Off timer" enabled + 0x80, + 0x40, // High fan + 0x57, // Checksum + }; + + EXPECT_STATE_EQ(expected_state, ac.getRaw(), kEuromBits); +}