diff --git a/src/IRac.cpp b/src/IRac.cpp index b500303d9..11d161e2f 100644 --- a/src/IRac.cpp +++ b/src/IRac.cpp @@ -58,6 +58,7 @@ #include "ir_Vestel.h" #include "ir_Voltas.h" #include "ir_Whirlpool.h" +#include "ir_Electrolux.h" // On the ESP8266 platform we need to use a special version of string handling // functions to handle the strings stored in the flash address space. @@ -243,6 +244,9 @@ bool IRac::isProtocolSupported(const decode_type_t protocol) { #if SEND_ELECTRA_AC case decode_type_t::ELECTRA_AC: #endif +#if SEND_ELECTROLUX_AC + case decode_type_t::ELECTROLUX_AC: +#endif // SEND_ELECTROLUX_AC #if SEND_FUJITSU_AC case decode_type_t::FUJITSU_AC: #endif @@ -817,7 +821,7 @@ void IRac::corona(IRCoronaAc *ac, // No Sleep setting available. ac->send(); } -#endif // SEND_CARRIER_AC64 +#endif // SEND_CORONA_AC #if SEND_DAIKIN /// Send a Daikin A/C message with the supplied settings. @@ -2852,6 +2856,42 @@ void IRac::rhoss(IRRhossAc *ac, } #endif // SEND_RHOSS +#if SEND_ELECTROLUX_AC +/// Send a Samsung A/C message with the supplied settings. +/// @note Multiple IR messages may be generated & sent. +/// @param[in, out] ac A Ptr to an IRSamsungAc object to use. +/// @param[in] on The power setting. +/// @param[in] mode The operation mode setting. +/// @param[in] celsius The celsius temperature mode. +/// @param[in] degrees The temperature setting in degrees. +/// @param[in] fan The speed setting for the fan. +/// @param[in] quiet Run the device in quiet/silent mode. +void IRac::electrolux(IRElectroluxAc *ac, + const bool on, const stdAc::opmode_t mode, + const bool celsius, const float degrees, + const stdAc::fanspeed_t fan, + const bool quiet) { + ac->begin(); + ac->stateReset(); + ac->setPower(on); + ac->setMode(ac->convertMode(mode)); + ac->setTempModeFahrenheit(!celsius); + ac->setTemp(degrees); + ac->setFan(ac->convertFan(fan)); + ac->setQuiet(quiet); + ac->setMode(ac->convertMode(mode)); + ac->send(); + // No Swing setting available. + // No Light setting available. + // No Filter setting available. + // No Turbo setting available. + // No Economy setting available. + // No Clean setting available. + // No Beep setting available. + // No Sleep setting available. +} +#endif // SEND_ELECTROLUX_AC + /// Create a new state base on the provided state that has been suitably fixed. /// @note This is for use with Home Assistant, which requires mode to be off if /// the power is off. @@ -3637,6 +3677,16 @@ bool IRac::sendAc(const stdAc::state_t desired, const stdAc::state_t *prev) { break; } #endif // SEND_TRANSCOLD_AC +#if SEND_ELECTROLUX_AC + case ELECTROLUX_AC: + { + IRElectroluxAc ac(_pin, _inverted, _modulation); + electrolux(&ac, send.power, send.mode, + send.celsius, send.degrees, + send.fanspeed, send.quiet); + break; + } +#endif // SEND_ELECTROLUX_AC default: return false; // Fail, didn't match anything. } @@ -4516,6 +4566,13 @@ namespace IRAcUtils { return ac.toString(); } #endif // DECODE_YORK +#if DECODE_ELECTROLUX_AC + case decode_type_t::ELECTROLUX_AC: { + IRElectroluxAc ac(kGpioUnused); + ac.setRaw(result->value); // ELETROLUX_AC uses value instead of state. + return ac.toString(); + } +#endif // DECODE_ELECTROLUX_AC default: return ""; } @@ -5060,6 +5117,14 @@ namespace IRAcUtils { break; } #endif // DECODE_YORK +#if DECODE_ELECTROLUX_AC + case decode_type_t::ELECTROLUX_AC: { + IRCarrierAc64 ac(kGpioUnused); + ac.setRaw(decode->value); // Uses value instead of state. + *result = ac.toCommon(); + break; + } +#endif // DECODE_CARRIER_AC64 default: return false; } diff --git a/src/IRac.h b/src/IRac.h index e3c261d0d..3951c63a4 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_Electrolux.h" #include "ir_Goodweather.h" #include "ir_Gree.h" #include "ir_Haier.h" @@ -567,6 +568,13 @@ void electra(IRElectraAc *ac, const stdAc::fanspeed_t fan, const stdAc::swingv_t swingv, const stdAc::swingh_t swingh); #endif // SEND_TRANSCOLD +#if SEND_ELECTROLUX_AC + void electrolux(IRElectroluxAc *ac, + const bool on, const stdAc::opmode_t mode, + const bool celsius, const float degrees, + const stdAc::fanspeed_t fan, + const bool quiet); +#endif // SEND_ELECTROLUX_AC static stdAc::state_t cleanState(const stdAc::state_t state); static stdAc::state_t handleToggles(const stdAc::state_t desired, const stdAc::state_t *prev = NULL); diff --git a/src/IRrecv.cpp b/src/IRrecv.cpp index 316dfc149..c61d721cd 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_ELECTROLUX_AC + DPRINTLN("Attempting Electrolux AC decode"); + if (decodeElectroluxAc(results, offset)) return true; +#endif // DECODE_ELECTROLUX_AC // Typically new protocols are added above this line. } #if DECODE_HASH diff --git a/src/IRrecv.h b/src/IRrecv.h index a9cbce610..cd6784108 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_ELECTROLUX_AC + bool decodeElectroluxAc(decode_results *results, + uint16_t offset = kStartOffset, + const uint16_t nbits = kElectroluxAcBits, + const bool strict = true); +#endif // DECODE_ELECTROLUX_AC }; #endif // IRRECV_H_ diff --git a/src/IRremoteESP8266.h b/src/IRremoteESP8266.h index b5daffaf8..95ddf3a5d 100644 --- a/src/IRremoteESP8266.h +++ b/src/IRremoteESP8266.h @@ -567,6 +567,13 @@ #define SEND_ELECTRA_AC _IR_ENABLE_DEFAULT_ #endif // SEND_ELECTRA_AC +#ifndef DECODE_ELECTROLUX_AC +#define DECODE_ELECTROLUX_AC _IR_ENABLE_DEFAULT_ +#endif // DECODE_ELECTROLUX_AC +#ifndef SEND_ELECTROLUX_AC +#define SEND_ELECTROLUX_AC _IR_ENABLE_DEFAULT_ +#endif // SEND_ELECTROLUX_AC + #ifndef DECODE_PANASONIC_AC #define DECODE_PANASONIC_AC _IR_ENABLE_DEFAULT_ #endif // DECODE_PANASONIC_AC @@ -1145,8 +1152,9 @@ enum decode_type_t { CARRIER_AC84, // 125 YORK, BLUESTARHEAVY, + ELECTROLUX_AC, // Add new entries before this one, and update it to point to the last entry. - kLastDecodeType = BLUESTARHEAVY, + kLastDecodeType = ELECTROLUX_AC, }; // Message lengths & required repeat values @@ -1244,6 +1252,8 @@ const uint16_t kEpsonMinRepeat = 2; const uint16_t kElectraAcStateLength = 13; const uint16_t kElectraAcBits = kElectraAcStateLength * 8; const uint16_t kElectraAcMinRepeat = kNoRepeat; +const uint16_t kElectroluxAcBits = 32; +const uint16_t kElectroluxAcDefaultRepeat = kNoRepeat; const uint16_t kEliteScreensBits = 32; const uint16_t kEliteScreensDefaultRepeat = kSingleRepeat; const uint16_t kFujitsuAcMinRepeat = kNoRepeat; diff --git a/src/IRsend.cpp b/src/IRsend.cpp index 1864ee2a3..e02900365 100644 --- a/src/IRsend.cpp +++ b/src/IRsend.cpp @@ -562,6 +562,7 @@ uint16_t IRsend::minRepeats(const decode_type_t protocol) { case COOLIX: case COOLIX48: case ELITESCREENS: + case ELECTROLUX_AC: case GICABLE: case INAX: case MIDEA24: @@ -646,6 +647,7 @@ uint16_t IRsend::defaultBits(const decode_type_t protocol) { case ARRIS: case CARRIER_AC: case ELITESCREENS: + case ELECTROLUX_AC: case EPSON: case NEC: case NEC_LIKE: @@ -915,6 +917,11 @@ bool IRsend::send(const decode_type_t type, const uint64_t data, sendEpson(data, nbits, min_repeat); break; #endif +#if SEND_ELECTROLUX_AC + case ELECTROLUX_AC: + sendElectroluxAc(data, nbits, min_repeat); + break; +#endif #if SEND_GICABLE case GICABLE: sendGICable(data, nbits, min_repeat); diff --git a/src/IRsend.h b/src/IRsend.h index 56e9a3d35..bdd9b0993 100644 --- a/src/IRsend.h +++ b/src/IRsend.h @@ -705,6 +705,11 @@ class IRsend { const uint16_t nbytes = kElectraAcStateLength, const uint16_t repeat = kNoRepeat); #endif +#if SEND_ELECTROLUX_AC + void sendElectroluxAc(const uint64_t data, + const uint16_t nbytes = kElectroluxAcBits, + const uint16_t repeat = kElectroluxAcDefaultRepeat); +#endif #if SEND_PANASONIC_AC void sendPanasonicAC(const unsigned char data[], const uint16_t nbytes = kPanasonicAcStateLength, diff --git a/src/IRtext.cpp b/src/IRtext.cpp index f8a3290bb..e6d90cdc3 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_ELECTROLUX_EACM || SEND_ELECTROLUX_EACM, + D_STR_ELECTROLUX_AC, 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/ir_Electrolux.cpp b/src/ir_Electrolux.cpp new file mode 100644 index 000000000..b30675395 --- /dev/null +++ b/src/ir_Electrolux.cpp @@ -0,0 +1,452 @@ +// Copyright 2024 Andrey Kravchenko (stellalupus) +/// @file +/// @brief Support for the Electrolux EACM protocols. + +// Supports: +// Brand: Electrolux, Model: Electrolux EACM EZ/N3 + +#include "ir_Electrolux.h" +#include +#include "IRac.h" +#include "IRrecv.h" +#include "IRsend.h" +#include "IRtext.h" +#include "IRutils.h" + +// Constants +const uint16_t kElectroluxAcHdrMark = 2850; +const uint16_t kElectroluxAcBitMark = 752; +const uint16_t kElectroluxAcHdrSpace = 2700; +const uint16_t kElectroluxAcOneSpace = 2149; +const uint16_t kElectroluxAcZeroSpace = 756; +const uint16_t kElectroluxAcFreq = 38000; +const uint16_t kElectroluxAcOverhead = 3; + +#if SEND_ELECTROLUX_AC +// Function should be safe up to 64 bits. +/// Send a Electrolux formatted message. +/// Status: ALPHA / Untested. +/// @param[in] data containing the IR command. +/// @param[in] nbits Nr. of bits to send. usually kElectroluxBits +/// @param[in] repeat Nr. of times the message is to be repeated. +void IRsend::sendElectroluxAc( + const uint64_t data, + const uint16_t nbits, + const uint16_t repeat +) { + enableIROut(kElectroluxAcFreq); + for (uint16_t r = 0; r <= repeat; r++) { + uint64_t send_data = data; + // Header + mark(kElectroluxAcHdrMark); + space(kElectroluxAcHdrSpace); + // Data Section + sendData(kElectroluxAcBitMark, kElectroluxAcOneSpace, + kElectroluxAcBitMark, kElectroluxAcZeroSpace, + send_data, nbits, true); + + send_data >>= 32; + // Footer + mark(kElectroluxAcBitMark); + + // A 100% made up guess of the gap between messages. + space(kDefaultMessageGap); + } +} +#endif // SEND_ELECTROLUX + +#if DECODE_ELECTROLUX_AC +// Function should be safe up to 64 bits. +/// Decode the supplied Electrolux message. +/// Status: ALPHA / Untested. +/// @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. +/// @param[in] nbits The number of data bits to expect. +/// @param[in] strict Flag indicating if we should perform strict matching. +/// @return A boolean. True if it can decode it, false if it can't. +bool IRrecv::decodeElectroluxAc( + decode_results *results, + uint16_t offset, + const uint16_t nbits, + const bool strict +) { + if (results->rawlen < 2 * nbits + kElectroluxAcOverhead - offset) + return false; // Too short a message to match. + if (strict && nbits != kElectroluxAcBits) + return false; + + uint64_t data = 0; + + // Header + if (!matchMark(results->rawbuf[offset++], kElectroluxAcHdrMark)) + return false; + if (!matchSpace(results->rawbuf[offset++], kElectroluxAcHdrSpace)) + return false; + + // Data Section #1 + // e.g. data_result.data = 0xED000004, nbits = 32 + match_result_t data_result = matchData( + &(results->rawbuf[offset]), 32, + kElectroluxAcBitMark, kElectroluxAcOneSpace, + kElectroluxAcBitMark, kElectroluxAcZeroSpace); + + offset += data_result.used; + if (data_result.success == false) + return false; // Fail + data <<= 32; // Make room for the new bits of data. + data |= data_result.data; + + // Footer + if (!matchMark(results->rawbuf[offset++], kElectroluxAcBitMark)) + return false; + + // Success + results->decode_type = decode_type_t::ELECTROLUX_AC; + results->bits = nbits; + results->value = data; + results->command = data & 0xFFF; + results->address = 0; + return true; +} +#endif // DECODE_ELECTROLUX + +/// 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? +IRElectroluxAc::IRElectroluxAc( + const uint16_t pin, + const bool inverted, + const bool use_modulation +): _irsend(pin, inverted, use_modulation) { + _ = ElectroluxAcProtocol(); + stateReset(); +} + +/// Reset the internal state to a fixed known good state. +/// @note The state is powered off. +void IRElectroluxAc::stateReset() { _.raw = 0xF3008005; } + +#if SEND_ELECTROLUX_AC +/// Send the current internal state as an IR message. +/// @param[in] repeat Nr. of times the message will be repeated. +void IRElectroluxAc::send(const uint16_t repeat) { + _irsend.sendElectroluxAc(getRaw(), kElectroluxAcBits, repeat); +} +#endif // SEND_ELECTROLUX_AC + +/// Set up hardware to be able to send a message. +void IRElectroluxAc::begin() { _irsend.begin(); } + +/// Turn on/off the Power Airwell setting. +/// @param[in] on The desired setting state. +void IRElectroluxAc::setPower(const bool on) { _.Power = on; } + +/// Get the power toggle setting from the internal state. +/// @return A boolean indicating the setting. +bool IRElectroluxAc::getPower() const { return _.Power; } + +/// Turn on/off the fahrenheit temp mode. +/// @param[in] on The desired setting state. +void IRElectroluxAc::setTempModeFahrenheit(const bool on) { + _.TempModeFahrenheit = on; +} + +/// Get the fahrenheit temp mode set from the internal state. +/// @return A boolean indicating the setting. +bool IRElectroluxAc::getTempModeFahrenheit() const { + return _.TempModeFahrenheit; +} + +/// Set the temperature. +/// @param[in] degrees The temperature in celsius or fahrenheit. +void IRElectroluxAc::setTemp(const uint8_t degrees) { + if (getTempModeFahrenheit()) { + uint8_t temp = max(kElectroluxAcMinFTemp, degrees); + temp = min(kElectroluxAcMaxFTemp, temp); + _.Temp = (temp - kElectroluxAcMinFTemp); + } else { + uint8_t temp = max(kElectroluxAcMinTemp, degrees); + temp = min(kElectroluxAcMaxTemp, temp); +#ifndef UNIT_TEST + temp = map(temp, kElectroluxAcMinTemp, kElectroluxAcMaxTemp, + kElectroluxAcMinFTemp, kElectroluxAcMaxFTemp); +#else + temp = temp * 9 / 5 + 32; +#endif + _.Temp = temp - kElectroluxAcMinFTemp; + } +} + +/// Get the current temperature from the internal state. +/// @return The current temperature in Celsius. +uint8_t IRElectroluxAc::getTemp() const { + if (getTempModeFahrenheit()) { + return _.Temp + kElectroluxAcMinFTemp; + } else { +#ifndef UNIT_TEST + uint8_t temp = map(_.Temp + kElectroluxAcMinFTemp, + kElectroluxAcMinFTemp, kElectroluxAcMaxFTemp, + kElectroluxAcMinTemp, kElectroluxAcMaxTemp); +#else + uint8_t temp = ((_.Temp + kElectroluxAcMinFTemp) - 32) * 5 / 9; +#endif + return temp; + } +} + +/// Set the speed of the fan. +/// @param[in] speed The desired setting. +/// @note The speed is locked to Low when in Dry mode. +void IRElectroluxAc::setFan(const uint8_t speed) { + _.Fan = (_.Mode == kElectroluxModeAuto) + ? kElectroluxFanAuto + : std::min(speed, kElectroluxFanAuto); +} + +/// Get the current fan speed setting. +/// @return The current fan speed. +uint8_t IRElectroluxAc::getFan() const { return _.Fan; } + +/// Set the desired operation mode. +/// @param[in] mode The desired operation mode. +void IRElectroluxAc::setMode(const uint8_t mode) { + switch (mode) { + case kElectroluxModeCool: + case kElectroluxModeDry: + case kElectroluxModeFan: + case kElectroluxModeAuto: + _.Mode = mode; + break; + default: + _.Mode = kElectroluxModeAuto; + } +} + +/// Get the operating mode setting of the A/C. +/// @return The current operating mode setting. +uint8_t IRElectroluxAc::getMode() const { return _.Mode; } + +/// Set the On/Off Timer time. +/// @param[in] nr_of_mins Number of minutes to set the timer to. +/// (< 60 is disable). +/// @note The A/C protocol only supports one hour increments. +void IRElectroluxAc::setOnOffTimer(const uint16_t nr_of_mins) { + const uint8_t hours = std::min( + static_cast(nr_of_mins / 60), + kElectroluxTimerMax); + + // The time can be changed in sleep mode, but doesn't set the flag. + _.TimerEnabled = hours > 0; + _.Timer = std::max(kElectroluxTimerMin, hours); // Hours +} + +/// Get the current On/Off Timer time. +/// @return The number of minutes it is set for. 0 means it's off. +/// @note The A/C protocol only supports one hour increments. +uint16_t IRElectroluxAc::getOnOffTimer() const { + return _.TimerEnabled > 0 ? _.Timer * 60 : 0; +} + +/// Set the Quiet setting of the A/C. +/// @param[in] on true, the setting is on. false, the setting is off. +void IRElectroluxAc::setQuiet(const bool on) { _.Quiet = on; } + +/// Get the Quiet setting of the A/C. +/// @return true, the setting is on. false, the setting is off. +bool IRElectroluxAc::getQuiet() const { return _.Quiet; } + +/// Get a copy of the internal state as a valid code for this protocol. +/// @return A valid code for this protocol based on the current internal state. +uint64_t IRElectroluxAc::getRaw() { + checksum(); // Ensure correct settings before sending. + return _.raw; +} + +/// Set the internal state from a valid code for this protocol. +/// @param[in] state A valid code for this protocol. +void IRElectroluxAc::setRaw(const uint64_t state) { _.raw = state; } + +/// Calculate the checksum for a given state. +/// @param[in] state The value to calc the checksum of. +/// @return The 4-bit checksum stored in a uint_8. +uint8_t IRElectroluxAc::calcChecksum(const uint64_t state) { + uint32_t data = GETBITS64( + state, + kElectroluxAcChecksumSize + kElectroluxAcChecksumOffset, + kElectroluxAcBits - 4); + + uint8_t result = 0; + for (; data; data >>= 4) // Add each nibble together. + result += GETBITS8(data, 0, 4); + return (result ^ 0xF) & 0xF; +} + +/// Verify the checksum is valid for a given state. +/// @param[in] state The array to verify the checksum of. +/// @return true, if the state has a valid checksum. Otherwise, false. +bool IRElectroluxAc::validChecksum(const uint64_t state) { + // Validate the checksum of the given state. + return (GETBITS8(state, kElectroluxAcChecksumOffset, + kElectroluxAcChecksumSize) == calcChecksum(state)); +} + +/// Convert a stdAc::opmode_t enum into its native mode. +/// @param[in] mode The enum to be converted. +/// @return The native equivalent of the enum. +uint8_t IRElectroluxAc::convertMode(const stdAc::opmode_t mode) { + switch (mode) { + case stdAc::opmode_t::kCool: + return kElectroluxModeCool; + case stdAc::opmode_t::kDry: + return kElectroluxModeDry; + case stdAc::opmode_t::kFan: + return kElectroluxModeFan; + default: + return kElectroluxModeAuto; + } +} + +/// Convert a stdAc::fanspeed_t enum into it's native speed. +/// @param[in] speed The enum to be converted. +/// @return The native equivalent of the enum. +uint8_t IRElectroluxAc::convertFan(const stdAc::fanspeed_t speed) { + switch (speed) { + case stdAc::fanspeed_t::kMin: + case stdAc::fanspeed_t::kLow: + return kElectroluxFanLow; + case stdAc::fanspeed_t::kMedium: + case stdAc::fanspeed_t::kMediumHigh: + return kElectroluxFanMedium; + case stdAc::fanspeed_t::kHigh: + case stdAc::fanspeed_t::kMax: + return kElectroluxFanHigh; + default: + return kElectroluxFanAuto; + } +} + +/// Convert a native mode into its stdAc equivalent. +/// @param[in] mode The native setting to be converted. +/// @return The stdAc equivalent of the native setting. +stdAc::opmode_t IRElectroluxAc::toCommonMode(const uint8_t mode) { + switch (mode) { + case kElectroluxModeCool: + return stdAc::opmode_t::kCool; + case kElectroluxModeDry: + return stdAc::opmode_t::kDry; + case kElectroluxModeFan: + return stdAc::opmode_t::kFan; + default: + return stdAc::opmode_t::kAuto; + } +} + +/// Convert a native fan speed into its stdAc equivalent. +/// @param[in] speed The native setting to be converted. +/// @return The stdAc equivalent of the native setting. +stdAc::fanspeed_t IRElectroluxAc::toCommonFanSpeed(const uint8_t speed) { + switch (speed) { + case kElectroluxFanHigh: + return stdAc::fanspeed_t::kMax; + case kElectroluxFanMedium: + return stdAc::fanspeed_t::kMedium; + case kElectroluxFanLow: + return stdAc::fanspeed_t::kMin; + default: + return stdAc::fanspeed_t::kAuto; + } +} + +/// Convert the current internal state into its stdAc::state_t equivalent. +/// @param[in] prev Ptr to the previous state if required. +/// @return The stdAc equivalent of the native settings. +stdAc::state_t IRElectroluxAc::toCommon(const stdAc::state_t *prev) const { + stdAc::state_t result{}; + // Start with the previous state if given it. + if (prev != nullptr) { + result = *prev; + } else { + // Set defaults for non-zero values that are not implicitly set for when + // there is no previous state. + // e.g. Any setting that toggles should probably go here. + result.power = false; + } + result.protocol = ELECTROLUX_AC; + result.power = _.Power; + result.mode = toCommonMode(_.Mode); + result.celsius = !getTempModeFahrenheit(); + result.degrees = getTemp(); + result.fanspeed = toCommonFanSpeed(_.Fan); + // Not supported. + result.model = -1; + result.turbo = false; + result.swingv = stdAc::swingv_t::kOff; + result.swingh = stdAc::swingh_t::kOff; + result.light = false; + result.filter = false; + result.econo = false; + result.quiet = getQuiet(); + result.clean = false; + result.beep = false; + result.sleep = -1; + result.clock = -1; + return result; +} + +/// Convert the internal state into a human readable string. +/// @return The current internal state expressed as a human readable String. +String IRElectroluxAc::toString() const { + String result = ""; + result.reserve(120); // Reserve heap for the string to reduce fragging. + + result += addBoolToString( + _.Power, + kPowerStr, + false); + + result += addModeToString( + _.Mode, + kElectroluxModeAuto, + kElectroluxModeCool, + 0xFF, + kElectroluxModeDry, + kElectroluxModeFan); + + result += addTempToString( + getTemp(), + !getTempModeFahrenheit()); + + result += addFanToString( + _.Fan, + kElectroluxFanHigh, + kElectroluxFanLow, + kElectroluxFanAuto, + kElectroluxFanAuto, + kElectroluxFanMedium); + + result += addBoolToString(getQuiet(), kQuietStr); + + if (getPower()) { + result += irutils::addLabeledString( + irutils::minsToString(getOnOffTimer()), + kOffTimerStr); + } else { + result += irutils::addLabeledString( + irutils::minsToString(getOnOffTimer()), + kOnTimerStr); + } + return result; +} + +/// Calculate and set the checksum values for the internal state. +void IRElectroluxAc::checksum() { + _.Sum = calcChecksum(_.raw); +} + +/// Set the requested power state of the A/C to on. +void IRElectroluxAc::on() { setPower(true); } + +/// Set the requested power state of the A/C to off. +void IRElectroluxAc::off() { setPower(false); } diff --git a/src/ir_Electrolux.h b/src/ir_Electrolux.h new file mode 100644 index 000000000..a9f6958f8 --- /dev/null +++ b/src/ir_Electrolux.h @@ -0,0 +1,147 @@ +// Copyright 2024 Andrey Kravchenko (stellalupus) +/// @file +/// @brief Support for the Electrolux EACM protocols. + +// Supports: +// Brand: Electrolux, Model: Electrolux EACM EZ/N3 + +#ifndef IR_ELECTROLUX_H_ +#define IR_ELECTROLUX_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 ElectroluxAcProtocol { + uint64_t raw; // The state of the IR remote in native IR code form. + struct { + uint8_t Sum: 4; + uint8_t : 4; + uint8_t : 5; + uint8_t TempModeFahrenheit: 1; + uint8_t : 1; + uint8_t Quiet: 1; + uint8_t Timer: 4; + uint8_t TimerEnabled: 1; + uint8_t Mode: 3; + uint8_t Temp: 5; + uint8_t Fan: 2; + uint8_t Power: 1; + uint64_t : 0; + }; +}; + +// Constants +const uint8_t kElectroluxAcMinTemp = 16; // 16C +const uint8_t kElectroluxAcMaxTemp = 32; // 32C +const uint8_t kElectroluxAcMinFTemp = 60; // 60F +const uint8_t kElectroluxAcMaxFTemp = 90; // 90F +const uint8_t kElectroluxTimerMax = 12; // 12H +const uint8_t kElectroluxTimerMin = 1; // 1H +const uint64_t kElectroluxAcKnownGoodState = 0xF3008005; +const uint8_t kElectroluxAcChecksumOffset = 0; +const uint8_t kElectroluxAcChecksumSize = 4; + +// Fan +const uint8_t kElectroluxFanLow = 2; // 0b11 +const uint8_t kElectroluxFanMedium = 1; // 0b01 +const uint8_t kElectroluxFanHigh = 0; // 0b00 +const uint8_t kElectroluxFanAuto = 3; // 0b11 + +// Modes +const uint8_t kElectroluxModeCool = 0; // 0b000 +const uint8_t kElectroluxModeDry = 1; // 0b001 +const uint8_t kElectroluxModeFan = 2; // 0b010 +const uint8_t kElectroluxModeAuto = 4; // 0b100 + +class IRElectroluxAc { + public: + explicit IRElectroluxAc(uint16_t pin, bool inverted = false, + bool use_modulation = true); + + void stateReset(); +#if SEND_ELECTROLUX_AC + void send(uint16_t repeat = kElectroluxAcDefaultRepeat); + + /// Run the calibration to calculate uSec timing offsets for this platform. + /// @return The uSec timing offset needed per modulation of the IR Led. + /// @note This will produce a 65ms IR signal pulse at 38kHz. + /// Only ever needs to be run once per object instantiation, if at all. + int8_t calibrate() { return _irsend.calibrate(); } +#endif // SEND_ELECTROLUX_AC + void begin(); + + void on(); + + void off(); + + void setPower(bool on); + + bool getPower() const; + + void setTemp(uint8_t degrees); + + uint8_t getTemp() const; + + void setFan(uint8_t speed); + + uint8_t getFan() const; + + void setMode(uint8_t mode); + + uint8_t getMode() const; + + void setOnOffTimer(uint16_t nr_of_mins); + + uint16_t getOnOffTimer() const; + + void setQuiet(bool on); + + bool getQuiet() const; + + void setTempModeFahrenheit(bool on); + + bool getTempModeFahrenheit() const; + + uint64_t getRaw(); + + void setRaw(uint64_t state); + + static uint8_t calcChecksum(uint64_t state); + + static bool validChecksum(uint64_t state); + + static uint8_t convertMode(stdAc::opmode_t mode); + + static uint8_t convertFan(stdAc::fanspeed_t speed); + + static stdAc::opmode_t toCommonMode(uint8_t mode); + + static stdAc::fanspeed_t toCommonFanSpeed(uint8_t speed); + + stdAc::state_t toCommon(const stdAc::state_t *prev = nullptr) const; + + String toString() const; + +#ifndef UNIT_TEST + + private: + IRsend _irsend; ///< Instance of the IR send class +#else // UNIT_TEST + /// @cond IGNORE + IRsendTest _irsend; ///< Instance of the testing IR send class + /// @endcond +#endif // UNIT_TEST + ElectroluxAcProtocol _{}; + + void checksum(); +}; + +#endif // IR_ELECTROLUX_H_ diff --git a/src/locale/defaults.h b/src/locale/defaults.h index a1329a97c..ed209d662 100644 --- a/src/locale/defaults.h +++ b/src/locale/defaults.h @@ -838,6 +838,9 @@ D_STR_INDIRECT " " D_STR_MODE #ifndef D_STR_ELECTRA_AC #define D_STR_ELECTRA_AC "ELECTRA_AC" #endif // D_STR_ELECTRA_AC +#ifndef D_STR_ELECTROLUX_AC +#define D_STR_ELECTROLUX_AC "ELECTROLUX AC" +#endif // D_STR_ELECTROLUX_AC #ifndef D_STR_ELITESCREENS #define D_STR_ELITESCREENS "ELITESCREENS" #endif // D_STR_ELITESCREENS diff --git a/test/IRac_test.cpp b/test/IRac_test.cpp index 428eab3c2..3770e1ad1 100644 --- a/test/IRac_test.cpp +++ b/test/IRac_test.cpp @@ -12,6 +12,7 @@ #include "ir_Delonghi.h" #include "ir_Ecoclim.h" #include "ir_Electra.h" +#include "ir_Electrolux.h" #include "ir_Fujitsu.h" #include "ir_Goodweather.h" #include "ir_Gree.h" @@ -668,6 +669,31 @@ TEST(TestIRac, Electra) { ASSERT_EQ(expected, ac.toString()); } +TEST(TestIRac, Electrolux) { + IRElectroluxAc ac(kGpioUnused); + IRac irac(kGpioUnused); + IRrecv capture(kGpioUnused); + char expected[] = + "Power: Off, Mode: 0 (Cool), Temp: 24C, " + "Fan: 3 (Auto), Quiet: Off, On Timer: 00:00"; + + ac.begin(); + irac.electrolux(&ac, + true, // Power + stdAc::opmode_t::kCool, // Mode + true, // Celsius + 24, // Sensor Temp. + stdAc::fanspeed_t::kAuto, // Fan speed + false ); // Quiet + ASSERT_EQ(expected, ac.toString()); + ac._irsend.makeDecodeResult(); + EXPECT_TRUE(capture.decode(&ac._irsend.capture)); + ASSERT_EQ(ELECTROLUX_AC, ac._irsend.capture.decode_type); + ASSERT_EQ(kElectroluxAcBits, ac._irsend.capture.bits); + ac.setRaw(ac._irsend.capture.state); + ASSERT_EQ(expected, ac.toString()); +} + TEST(TestIRac, Fujitsu) { IRFujitsuAC ac(kGpioUnused); IRac irac(kGpioUnused); diff --git a/test/ir_Electrolux_test.cpp b/test/ir_Electrolux_test.cpp new file mode 100644 index 000000000..4feca1194 --- /dev/null +++ b/test/ir_Electrolux_test.cpp @@ -0,0 +1,19 @@ +// Copyright 2024 Andrey Kravchenko (StellaLupus) + +#include "ir_Electrolux.h" +#include "IRac.h" +#include "IRrecv.h" +#include "IRrecv_test.h" +#include "IRsend.h" +#include "IRsend_test.h" +#include "gtest/gtest.h" + + +TEST(TestUtils, Housekeeping) { + ASSERT_EQ("ELETROLUX_AC", typeToString(decode_type_t::ELECTROLUX_AC)); + ASSERT_EQ(decode_type_t::ELECTROLUX_AC, strToDecodeType("ELETROLUX_AC")); + ASSERT_FALSE(hasACState(decode_type_t::ELECTROLUX_AC)); + ASSERT_TRUE(IRac::isProtocolSupported(decode_type_t::ELECTROLUX_AC)); + ASSERT_EQ(kElectroluxAcBits, IRsend::defaultBits(decode_type_t::ELECTROLUX_AC)); + ASSERT_EQ(kElectroluxAcDefaultRepeat, IRsend::minRepeats(decode_type_t::ELECTROLUX_AC)); +} \ No newline at end of file