Add support for the Eurom A/C protocol (#2208)

* Add support for the Eurom A/C protocol

* Fix some doc comments

* One more doc comment ;_;

* Also include the cmath and cstring headers

* Use roundf() instead of just round()

* Remove support section from the .cpp file
This commit is contained in:
Gottem
2025-12-15 05:06:31 +01:00
committed by GitHub
parent d9bc0ffa01
commit 97b92d3ccb
13 changed files with 1210 additions and 1 deletions

View File

@@ -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:
{
@@ -4224,6 +4262,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);
@@ -4724,6 +4769,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);

View File

@@ -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,

View File

@@ -1219,6 +1219,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

View File

@@ -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_

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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.
};

View File

@@ -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:

489
src/ir_Eurom.cpp Normal file
View File

@@ -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 <algorithm>
#include <cmath>
#include <cstring>
#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<uint8_t>(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;
}

244
src/ir_Eurom.h Normal file
View File

@@ -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 <stdint.h>
#ifndef UNIT_TEST
#include <Arduino.h>
#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_

View File

@@ -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

377
test/ir_Eurom_test.cpp Normal file
View File

@@ -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);
}