From 3a54284cbb0f557435b6d8db9889fd2e43badb1e Mon Sep 17 00:00:00 2001 From: StellaLupus Date: Sat, 10 Aug 2024 08:11:06 +0300 Subject: [PATCH 1/5] Support Electrolux eacm remote AC --- src/IRac.cpp | 65 +++++- src/IRac.h | 8 + src/IRrecv.cpp | 4 + src/IRrecv.h | 5 + src/IRremoteESP8266.h | 12 +- src/IRsend.h | 5 + src/IRtext.cpp | 2 + src/ir_Electrolux.cpp | 376 +++++++++++++++++++++++++++++++++ src/ir_Electrolux.h | 116 ++++++++++ src/locale/defaults.h | 3 + tools/calculate_check_bytes.py | 96 +++++++++ tools/serial_parser.py | 99 +++++++++ 12 files changed, 789 insertions(+), 2 deletions(-) create mode 100644 src/ir_Electrolux.cpp create mode 100644 src/ir_Electrolux.h create mode 100644 tools/calculate_check_bytes.py create mode 100644 tools/serial_parser.py diff --git a/src/IRac.cpp b/src/IRac.cpp index b500303d9..720c001f2 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. @@ -374,6 +375,9 @@ bool IRac::isProtocolSupported(const decode_type_t protocol) { #if SEND_WHIRLPOOL_AC case decode_type_t::WHIRLPOOL_AC: #endif +#if SEND_ELECTROLUX_AC + case decode_type_t::ELETROLUX_AC: +#endif // SEND_ELECTROLUX_AC return true; default: return false; @@ -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->setPowerToggle(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,14 @@ bool IRac::sendAc(const stdAc::state_t desired, const stdAc::state_t *prev) { break; } #endif // SEND_TRANSCOLD_AC +#if SEND_ELECTROLUX_AC + case ELETROLUX_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 +4564,13 @@ namespace IRAcUtils { return ac.toString(); } #endif // DECODE_YORK +#if DECODE_ELECTROLUX_AC + case decode_type_t::ELETROLUX_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 +5115,14 @@ namespace IRAcUtils { break; } #endif // DECODE_YORK +#if DECODE_ELECTROLUX_AC + case decode_type_t::ELETROLUX_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..3a32f61b0 100644 --- a/src/IRac.h +++ b/src/IRac.h @@ -50,6 +50,7 @@ #include "ir_Voltas.h" #include "ir_Whirlpool.h" #include "ir_York.h" +#include "ir_Electrolux.h" // Constants const int8_t kGpioUnused = -1; ///< A placeholder for not using an actual GPIO. @@ -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..8ed9cdd62 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..50094e43a 100644 --- a/src/IRrecv.h +++ b/src/IRrecv.h @@ -889,6 +889,11 @@ 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..06031396e 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, + ELETROLUX_AC, // Add new entries before this one, and update it to point to the last entry. - kLastDecodeType = BLUESTARHEAVY, + kLastDecodeType = ELETROLUX_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.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..a4dd97cbe --- /dev/null +++ b/src/ir_Electrolux.cpp @@ -0,0 +1,376 @@ +// 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; // Hz. (Guessing the most common frequency.) +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 #1 + // e.g. data = 0xED000004, nbits = 32 + sendData(kElectroluxAcBitMark, kElectroluxAcOneSpace, kElectroluxAcBitMark, kElectroluxAcZeroSpace, send_data, 32, true); + send_data >>= 32; + // Footer + mark(kElectroluxAcBitMark); + space(kDefaultMessageGap); // A 100% made up guess of the gap between messages. + } +} +#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) //rawlen = 68, nbits = 104 + return false; // Too short a message to match. + if (strict && nbits != kElectroluxAcBits) + return false; + + uint64_t data = 0; + match_result_t data_result; + + // 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 + 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::ELETROLUX_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) { stateReset(); } + +/// Reset the internal state to a fixed known good state. +/// @note The state is powered off. +void IRElectroluxAc::stateReset(void) { _.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(void) { _irsend.begin(); } + +/// Turn on/off the Power Airwell setting. +/// @param[in] on The desired setting state. +void IRElectroluxAc::setPowerToggle(const bool on) { _.PowerToggle = on; } + +/// Get the power toggle setting from the internal state. +/// @return A boolean indicating the setting. +bool IRElectroluxAc::getPowerToggle(void) const { return _.PowerToggle; } + +/// 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(void) 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 = std::max(kElectroluxAcMaxFTemp, degrees); + temp = std::min(kElectroluxAcMinFTemp, temp); + _.Temp = (temp - kElectroluxAcMinFTemp); + } + else { + uint8_t temp = std::max(kElectroluxAcMaxTemp, degrees); + temp = std::min(kElectroluxAcMinTemp, temp); + _.Temp = ((temp - kElectroluxAcMinTemp) * 1.8); //TODO: fix accuracy + } +} + +/// Get the current temperature from the internal state. +/// @return The current temperature in Celsius. +uint8_t IRElectroluxAc::getTemp(void) const { + if(getTempModeFahrenheit()) { + return _.Temp + kElectroluxAcMinFTemp; + } + else { + return (_.Temp / 1.8) + kElectroluxAcMinTemp; //TODO: fix accuracy + } +} + +/// 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(void) 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(void) 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_hours) { + uint8_t hours = std::min((uint8_t)(nr_of_hours / 60), kElectroluxTimerMax); + // The time can be changed in sleep mode, but doesn't set the flag. + _.TimerEnable = 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(void) const { + if (_.TimerEnable) + return _.Timer * 60; + else + return 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(void) 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(void) { + 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 = GETBITS32(state, 0, kElectroluxAcBits - 4); + uint8_t result = 0; + for (; data; data >>= 4) // Add each nibble together. + result += GETBITS32(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 != NULL) { + 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 = decode_type_t::ELETROLUX_AC; + result.power = _.PowerToggle; + 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(void) const { + String result = ""; + result.reserve(120); // Reserve some heap for the string to reduce fragging. + result += addBoolToString(_.PowerToggle, kPowerStr, false); + result += addModeToString(_.Mode, kElectroluxModeAuto, kElectroluxModeCool, + 0xFF, kElectroluxModeDry, kElectroluxModeFan); + result += addTempToString(getTemp()); + result += addFanToString(_.Fan, kElectroluxFanHigh, kElectroluxFanLow, + kElectroluxFanAuto, kElectroluxFanAuto, + kElectroluxFanMedium); + + result += addBoolToString(getQuiet(), kQuietStr); + + if(getPowerToggle()) { + 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(void) { + _.Sum = calcChecksum(_.raw); +} \ No newline at end of file diff --git a/src/ir_Electrolux.h b/src/ir_Electrolux.h new file mode 100644 index 000000000..4500c4c51 --- /dev/null +++ b/src/ir_Electrolux.h @@ -0,0 +1,116 @@ +// Copyright 2024 Andrey Kravchenko (stellalupus) +/// @file +/// @brief Support for the Electrolux EACM protocols. + +// Supports: +// Brand: Electrolux, Model: Electrolux EACM EZ/N3 + +#ifndef IR_ELECTROLUX_AC_H_ +#define IR_ELECTROLUX_AC_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 PowerToggle :1; + uint8_t Fan :2; + uint8_t Temp :5; + uint8_t Mode :3; + uint8_t TimerEnable :1; + uint8_t Timer :4; + uint8_t Quiet :1; + uint8_t :1; + uint8_t TempModeFahrenheit :1; + uint8_t :5; + uint8_t :4; + uint8_t Sum :4; + }; +}; + +// 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 = 28; +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(const uint16_t pin, const bool inverted = false, + const bool use_modulation = true); + void stateReset(); +#if SEND_ELECTROLUX_AC + void send(const 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(void) { return _irsend.calibrate(); } +#endif // SEND_ELECTROLUX_AC + void begin(); + void setPowerToggle(const bool on); + bool getPowerToggle(void) const; + void setTemp(const uint8_t temp); + uint8_t getTemp(void) const; + void setFan(const uint8_t speed); + uint8_t getFan(void) const; + void setMode(const uint8_t mode); + uint8_t getMode(void) const; + void setOnOffTimer(const uint16_t nr_of_mins); + uint16_t getOnOffTimer(void) const; + void setQuiet(const bool on); + bool getQuiet(void) const; + void setTempModeFahrenheit(const bool on); + bool getTempModeFahrenheit(void) const; + uint64_t getRaw(void); + void setRaw(const uint64_t state); + static uint8_t calcChecksum(const uint64_t state); + static bool validChecksum(const uint64_t state); + static uint8_t convertMode(const stdAc::opmode_t mode); + static uint8_t convertFan(const stdAc::fanspeed_t speed); + static stdAc::opmode_t toCommonMode(const uint8_t mode); + static stdAc::fanspeed_t toCommonFanSpeed(const uint8_t speed); + stdAc::state_t toCommon(const stdAc::state_t *prev = NULL) const; + String toString(void) 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(void); +}; + +#endif // IR_ELECTROLUX_AC_H_ \ No newline at end of file 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/tools/calculate_check_bytes.py b/tools/calculate_check_bytes.py new file mode 100644 index 000000000..d3a310ded --- /dev/null +++ b/tools/calculate_check_bytes.py @@ -0,0 +1,96 @@ +def calculate_check_bytes(data): + # Split the data into groups of 8 bits (1 byte each) + bytes_data = [data[i:i+4] for i in range(0, len(data) - 8, 4)] + + print(bytes_data) + + # Convert each byte to an integer + int_bytes = [int(byte, 2) for byte in bytes_data] + + # Perform XOR on all bytes + check_byte = 0 + for byte in int_bytes: + check_byte += byte + + check_byte = check_byte % 16 + check_byte ^= 15 + + # Convert check byte to a 4-bit binary string + check_bits = bin(check_byte & 0x0F)[2:].zfill(4) + + return check_bits + +# Example usage +data = ['11100000000000000000000000000001', +'11100011000000000000000000001110', +'11100100000000000000000000001101', +'11100110000000000000000000001011', +'11101000000000000000000000001001', +'11101010000000000000000000000111', +'11101100000000000000000000000101', +'11101101000000000000000000000100', +'11101111000000000000000000000010', +'11110001000000000000000000001111', +'11110011000000000000000000001101', +'11110100000000000000000000001100', +'11110110000000000000000000001010', +'11111000000000000000000000001000', +'11111010000000000000000000000110', +'11111100000000000000000000000100', +'11111110000000000000000000000010', +'11100000000000001000000000001001', +'11100011000000001000000000000110', +'11100100000000001000000000000101', +'11100110000000001000000000000011', +'11101000000000001000000000000001', +'11101010000000001000000000001111', +'11101100000000001000000000001101', +'11101101000000001000000000001100', +'11101111000000001000000000001010', +'11110001000000001000000000000111', +'11110011000000001000000000000101', +'11110100000000001000000000000100', +'11110110000000001000000000000010', +'11111000000000001000000000000000', +'11111010000000001000000000001110', +'11111100000000001000000000001100', +'11111110000000001000000000001010', +'10001011001000000000000000001010', +'10101011001000000000000000001000', +'11001011001000000000000000000110', +'10001011001000001000000000000010', +'10101011001000001000000000000000', +'11001011001000001000000000001110', +'10001011010000000000000000001000', +'10101011010000000000000000000110', +'11001011010000000000000000000100', +'10001011010000001000000000000000', +'10101011010000001000000000001110', +'11001011010000001000000000001100', +'11101011100000000000000000001110', +'11101011100000001000000000000110', +'01101011100000000000000000000110', +'11101011100111000000000000000001', +'11101011100110110000000000000010', +'11101011100110100000000000000011', +'11101011100110010000000000000100', +'11101011100110000000000000000101', +'11101011100101110000000000000110', +'11101011100101100000000000000111', +'11101011100101010000000000001000', +'11101011100101000000000000001001', +'11101011100100110000000000001010', +'11101011100100100000000000001011', +'11101011100100010000000000001100', +'11100000000000000010000000001111', +'11100001000000000010000000001110', +'11100010000000000010000000001101'] + + +for d in data: + check_bits = calculate_check_bytes(d) + if(check_bits == d[28:]): + print("OK -", end="") + else: + print("NO -", end="") + print(f"Calculated: {check_bits} real: {d[28:]}") \ No newline at end of file diff --git a/tools/serial_parser.py b/tools/serial_parser.py new file mode 100644 index 000000000..124915103 --- /dev/null +++ b/tools/serial_parser.py @@ -0,0 +1,99 @@ +import enum +import serial +import serial.tools +import serial.tools.list_ports + +def substring_after(str: str, searchStr: str): + return str[str.index(searchStr) + len(searchStr):] + +def getPort(): + ports = sorted(serial.tools.list_ports.comports()) + print("Available ports to listing:") + for id, portInfo in enumerate(ports): + print("{}. - {}: {} [{}]".format(id, portInfo.device, portInfo.description, portInfo.hwid)) + print("Select port: ", end="") + selectId = int(input()) + if selectId < len(ports) and selectId >= 0: + return ports[selectId] + else: + print("Unrecognized port number") + return getPort() + +zero = 756 +one = 2149 +space = 752 +interv = 0.2 + +def getBitFromInterv(value: int): + if(value > zero - zero * interv and value < zero + zero * interv): + return 0 + elif(value > one - one * interv and value < one + one * interv): + return 1 + else: + return 2 + +def bitListToInt(bitlist): + out = 0 + for bit in bitlist: + out = (out << 1) | bit + return out + + +def main(): + port = getPort() + print("Selected port:" + port.device) + + ser = serial.Serial( + port=port.device, + baudrate=115200, + ) + + zSum = 0 + zCnt = 0 + oSum = 0 + oCnt = 0 + sSum = 0 + sCnt = 0 + + while True: + try: + dataStr = ser.readline().decode() + except: + continue + #print(dataStr) + if("uint16_t rawData" in dataStr): + dataStrArray = str(dataStr[dataStr.index('{') + 1:dataStr.index('}')]).split(",") + data = [int(i.strip()) for i in dataStrArray] + data = data[2:] + clearData = [i for idi, i in enumerate(data) if idi % 2 == 1] + bitData = [getBitFromInterv(i) for i in data] + clearBitData = [getBitFromInterv(i) for i in clearData] + + + + for idd, d in enumerate(data): + if(idd % 2 == 0): + sSum += d + sCnt += 1 + else: + if(getBitFromInterv(d) == 0): + zSum += d + zCnt += 1 + elif(getBitFromInterv(d) == 1): + oSum += d + oCnt += 1 + + # print("zero = " + str(zSum/zCnt) + " one = " + str(oSum/oCnt) + " space = " + str(sSum/sCnt))\ + + + + # print("Data = ", end="") + # print(data) + # print("0b"+ "".join([str(i) for i in bitData])) + # print("ClearData = ", end="") + #print(clearData) + print("".join([str(i) for i in clearBitData])) + + +if __name__ == '__main__': + main() \ No newline at end of file From 40be342ca13895de030bf8fce0fc3efc7121259c Mon Sep 17 00:00:00 2001 From: StellaLupus Date: Sat, 10 Aug 2024 08:11:06 +0300 Subject: [PATCH 2/5] Support Electrolux eacm remote AC --- src/IRac.cpp | 65 +++++- src/IRac.h | 8 + src/IRrecv.cpp | 4 + src/IRrecv.h | 5 + src/IRremoteESP8266.h | 12 +- src/IRsend.h | 5 + src/IRtext.cpp | 2 + src/ir_Electrolux.cpp | 376 +++++++++++++++++++++++++++++++++ src/ir_Electrolux.h | 116 ++++++++++ src/locale/defaults.h | 3 + tools/calculate_check_bytes.py | 96 +++++++++ tools/serial_parser.py | 99 +++++++++ 12 files changed, 789 insertions(+), 2 deletions(-) create mode 100644 src/ir_Electrolux.cpp create mode 100644 src/ir_Electrolux.h create mode 100644 tools/calculate_check_bytes.py create mode 100644 tools/serial_parser.py diff --git a/src/IRac.cpp b/src/IRac.cpp index b500303d9..720c001f2 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. @@ -374,6 +375,9 @@ bool IRac::isProtocolSupported(const decode_type_t protocol) { #if SEND_WHIRLPOOL_AC case decode_type_t::WHIRLPOOL_AC: #endif +#if SEND_ELECTROLUX_AC + case decode_type_t::ELETROLUX_AC: +#endif // SEND_ELECTROLUX_AC return true; default: return false; @@ -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->setPowerToggle(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,14 @@ bool IRac::sendAc(const stdAc::state_t desired, const stdAc::state_t *prev) { break; } #endif // SEND_TRANSCOLD_AC +#if SEND_ELECTROLUX_AC + case ELETROLUX_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 +4564,13 @@ namespace IRAcUtils { return ac.toString(); } #endif // DECODE_YORK +#if DECODE_ELECTROLUX_AC + case decode_type_t::ELETROLUX_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 +5115,14 @@ namespace IRAcUtils { break; } #endif // DECODE_YORK +#if DECODE_ELECTROLUX_AC + case decode_type_t::ELETROLUX_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..3a32f61b0 100644 --- a/src/IRac.h +++ b/src/IRac.h @@ -50,6 +50,7 @@ #include "ir_Voltas.h" #include "ir_Whirlpool.h" #include "ir_York.h" +#include "ir_Electrolux.h" // Constants const int8_t kGpioUnused = -1; ///< A placeholder for not using an actual GPIO. @@ -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..8ed9cdd62 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..50094e43a 100644 --- a/src/IRrecv.h +++ b/src/IRrecv.h @@ -889,6 +889,11 @@ 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..06031396e 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, + ELETROLUX_AC, // Add new entries before this one, and update it to point to the last entry. - kLastDecodeType = BLUESTARHEAVY, + kLastDecodeType = ELETROLUX_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.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..a4dd97cbe --- /dev/null +++ b/src/ir_Electrolux.cpp @@ -0,0 +1,376 @@ +// 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; // Hz. (Guessing the most common frequency.) +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 #1 + // e.g. data = 0xED000004, nbits = 32 + sendData(kElectroluxAcBitMark, kElectroluxAcOneSpace, kElectroluxAcBitMark, kElectroluxAcZeroSpace, send_data, 32, true); + send_data >>= 32; + // Footer + mark(kElectroluxAcBitMark); + space(kDefaultMessageGap); // A 100% made up guess of the gap between messages. + } +} +#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) //rawlen = 68, nbits = 104 + return false; // Too short a message to match. + if (strict && nbits != kElectroluxAcBits) + return false; + + uint64_t data = 0; + match_result_t data_result; + + // 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 + 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::ELETROLUX_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) { stateReset(); } + +/// Reset the internal state to a fixed known good state. +/// @note The state is powered off. +void IRElectroluxAc::stateReset(void) { _.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(void) { _irsend.begin(); } + +/// Turn on/off the Power Airwell setting. +/// @param[in] on The desired setting state. +void IRElectroluxAc::setPowerToggle(const bool on) { _.PowerToggle = on; } + +/// Get the power toggle setting from the internal state. +/// @return A boolean indicating the setting. +bool IRElectroluxAc::getPowerToggle(void) const { return _.PowerToggle; } + +/// 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(void) 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 = std::max(kElectroluxAcMaxFTemp, degrees); + temp = std::min(kElectroluxAcMinFTemp, temp); + _.Temp = (temp - kElectroluxAcMinFTemp); + } + else { + uint8_t temp = std::max(kElectroluxAcMaxTemp, degrees); + temp = std::min(kElectroluxAcMinTemp, temp); + _.Temp = ((temp - kElectroluxAcMinTemp) * 1.8); //TODO: fix accuracy + } +} + +/// Get the current temperature from the internal state. +/// @return The current temperature in Celsius. +uint8_t IRElectroluxAc::getTemp(void) const { + if(getTempModeFahrenheit()) { + return _.Temp + kElectroluxAcMinFTemp; + } + else { + return (_.Temp / 1.8) + kElectroluxAcMinTemp; //TODO: fix accuracy + } +} + +/// 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(void) 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(void) 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_hours) { + uint8_t hours = std::min((uint8_t)(nr_of_hours / 60), kElectroluxTimerMax); + // The time can be changed in sleep mode, but doesn't set the flag. + _.TimerEnable = 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(void) const { + if (_.TimerEnable) + return _.Timer * 60; + else + return 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(void) 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(void) { + 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 = GETBITS32(state, 0, kElectroluxAcBits - 4); + uint8_t result = 0; + for (; data; data >>= 4) // Add each nibble together. + result += GETBITS32(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 != NULL) { + 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 = decode_type_t::ELETROLUX_AC; + result.power = _.PowerToggle; + 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(void) const { + String result = ""; + result.reserve(120); // Reserve some heap for the string to reduce fragging. + result += addBoolToString(_.PowerToggle, kPowerStr, false); + result += addModeToString(_.Mode, kElectroluxModeAuto, kElectroluxModeCool, + 0xFF, kElectroluxModeDry, kElectroluxModeFan); + result += addTempToString(getTemp()); + result += addFanToString(_.Fan, kElectroluxFanHigh, kElectroluxFanLow, + kElectroluxFanAuto, kElectroluxFanAuto, + kElectroluxFanMedium); + + result += addBoolToString(getQuiet(), kQuietStr); + + if(getPowerToggle()) { + 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(void) { + _.Sum = calcChecksum(_.raw); +} \ No newline at end of file diff --git a/src/ir_Electrolux.h b/src/ir_Electrolux.h new file mode 100644 index 000000000..4500c4c51 --- /dev/null +++ b/src/ir_Electrolux.h @@ -0,0 +1,116 @@ +// Copyright 2024 Andrey Kravchenko (stellalupus) +/// @file +/// @brief Support for the Electrolux EACM protocols. + +// Supports: +// Brand: Electrolux, Model: Electrolux EACM EZ/N3 + +#ifndef IR_ELECTROLUX_AC_H_ +#define IR_ELECTROLUX_AC_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 PowerToggle :1; + uint8_t Fan :2; + uint8_t Temp :5; + uint8_t Mode :3; + uint8_t TimerEnable :1; + uint8_t Timer :4; + uint8_t Quiet :1; + uint8_t :1; + uint8_t TempModeFahrenheit :1; + uint8_t :5; + uint8_t :4; + uint8_t Sum :4; + }; +}; + +// 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 = 28; +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(const uint16_t pin, const bool inverted = false, + const bool use_modulation = true); + void stateReset(); +#if SEND_ELECTROLUX_AC + void send(const 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(void) { return _irsend.calibrate(); } +#endif // SEND_ELECTROLUX_AC + void begin(); + void setPowerToggle(const bool on); + bool getPowerToggle(void) const; + void setTemp(const uint8_t temp); + uint8_t getTemp(void) const; + void setFan(const uint8_t speed); + uint8_t getFan(void) const; + void setMode(const uint8_t mode); + uint8_t getMode(void) const; + void setOnOffTimer(const uint16_t nr_of_mins); + uint16_t getOnOffTimer(void) const; + void setQuiet(const bool on); + bool getQuiet(void) const; + void setTempModeFahrenheit(const bool on); + bool getTempModeFahrenheit(void) const; + uint64_t getRaw(void); + void setRaw(const uint64_t state); + static uint8_t calcChecksum(const uint64_t state); + static bool validChecksum(const uint64_t state); + static uint8_t convertMode(const stdAc::opmode_t mode); + static uint8_t convertFan(const stdAc::fanspeed_t speed); + static stdAc::opmode_t toCommonMode(const uint8_t mode); + static stdAc::fanspeed_t toCommonFanSpeed(const uint8_t speed); + stdAc::state_t toCommon(const stdAc::state_t *prev = NULL) const; + String toString(void) 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(void); +}; + +#endif // IR_ELECTROLUX_AC_H_ \ No newline at end of file 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/tools/calculate_check_bytes.py b/tools/calculate_check_bytes.py new file mode 100644 index 000000000..d3a310ded --- /dev/null +++ b/tools/calculate_check_bytes.py @@ -0,0 +1,96 @@ +def calculate_check_bytes(data): + # Split the data into groups of 8 bits (1 byte each) + bytes_data = [data[i:i+4] for i in range(0, len(data) - 8, 4)] + + print(bytes_data) + + # Convert each byte to an integer + int_bytes = [int(byte, 2) for byte in bytes_data] + + # Perform XOR on all bytes + check_byte = 0 + for byte in int_bytes: + check_byte += byte + + check_byte = check_byte % 16 + check_byte ^= 15 + + # Convert check byte to a 4-bit binary string + check_bits = bin(check_byte & 0x0F)[2:].zfill(4) + + return check_bits + +# Example usage +data = ['11100000000000000000000000000001', +'11100011000000000000000000001110', +'11100100000000000000000000001101', +'11100110000000000000000000001011', +'11101000000000000000000000001001', +'11101010000000000000000000000111', +'11101100000000000000000000000101', +'11101101000000000000000000000100', +'11101111000000000000000000000010', +'11110001000000000000000000001111', +'11110011000000000000000000001101', +'11110100000000000000000000001100', +'11110110000000000000000000001010', +'11111000000000000000000000001000', +'11111010000000000000000000000110', +'11111100000000000000000000000100', +'11111110000000000000000000000010', +'11100000000000001000000000001001', +'11100011000000001000000000000110', +'11100100000000001000000000000101', +'11100110000000001000000000000011', +'11101000000000001000000000000001', +'11101010000000001000000000001111', +'11101100000000001000000000001101', +'11101101000000001000000000001100', +'11101111000000001000000000001010', +'11110001000000001000000000000111', +'11110011000000001000000000000101', +'11110100000000001000000000000100', +'11110110000000001000000000000010', +'11111000000000001000000000000000', +'11111010000000001000000000001110', +'11111100000000001000000000001100', +'11111110000000001000000000001010', +'10001011001000000000000000001010', +'10101011001000000000000000001000', +'11001011001000000000000000000110', +'10001011001000001000000000000010', +'10101011001000001000000000000000', +'11001011001000001000000000001110', +'10001011010000000000000000001000', +'10101011010000000000000000000110', +'11001011010000000000000000000100', +'10001011010000001000000000000000', +'10101011010000001000000000001110', +'11001011010000001000000000001100', +'11101011100000000000000000001110', +'11101011100000001000000000000110', +'01101011100000000000000000000110', +'11101011100111000000000000000001', +'11101011100110110000000000000010', +'11101011100110100000000000000011', +'11101011100110010000000000000100', +'11101011100110000000000000000101', +'11101011100101110000000000000110', +'11101011100101100000000000000111', +'11101011100101010000000000001000', +'11101011100101000000000000001001', +'11101011100100110000000000001010', +'11101011100100100000000000001011', +'11101011100100010000000000001100', +'11100000000000000010000000001111', +'11100001000000000010000000001110', +'11100010000000000010000000001101'] + + +for d in data: + check_bits = calculate_check_bytes(d) + if(check_bits == d[28:]): + print("OK -", end="") + else: + print("NO -", end="") + print(f"Calculated: {check_bits} real: {d[28:]}") \ No newline at end of file diff --git a/tools/serial_parser.py b/tools/serial_parser.py new file mode 100644 index 000000000..124915103 --- /dev/null +++ b/tools/serial_parser.py @@ -0,0 +1,99 @@ +import enum +import serial +import serial.tools +import serial.tools.list_ports + +def substring_after(str: str, searchStr: str): + return str[str.index(searchStr) + len(searchStr):] + +def getPort(): + ports = sorted(serial.tools.list_ports.comports()) + print("Available ports to listing:") + for id, portInfo in enumerate(ports): + print("{}. - {}: {} [{}]".format(id, portInfo.device, portInfo.description, portInfo.hwid)) + print("Select port: ", end="") + selectId = int(input()) + if selectId < len(ports) and selectId >= 0: + return ports[selectId] + else: + print("Unrecognized port number") + return getPort() + +zero = 756 +one = 2149 +space = 752 +interv = 0.2 + +def getBitFromInterv(value: int): + if(value > zero - zero * interv and value < zero + zero * interv): + return 0 + elif(value > one - one * interv and value < one + one * interv): + return 1 + else: + return 2 + +def bitListToInt(bitlist): + out = 0 + for bit in bitlist: + out = (out << 1) | bit + return out + + +def main(): + port = getPort() + print("Selected port:" + port.device) + + ser = serial.Serial( + port=port.device, + baudrate=115200, + ) + + zSum = 0 + zCnt = 0 + oSum = 0 + oCnt = 0 + sSum = 0 + sCnt = 0 + + while True: + try: + dataStr = ser.readline().decode() + except: + continue + #print(dataStr) + if("uint16_t rawData" in dataStr): + dataStrArray = str(dataStr[dataStr.index('{') + 1:dataStr.index('}')]).split(",") + data = [int(i.strip()) for i in dataStrArray] + data = data[2:] + clearData = [i for idi, i in enumerate(data) if idi % 2 == 1] + bitData = [getBitFromInterv(i) for i in data] + clearBitData = [getBitFromInterv(i) for i in clearData] + + + + for idd, d in enumerate(data): + if(idd % 2 == 0): + sSum += d + sCnt += 1 + else: + if(getBitFromInterv(d) == 0): + zSum += d + zCnt += 1 + elif(getBitFromInterv(d) == 1): + oSum += d + oCnt += 1 + + # print("zero = " + str(zSum/zCnt) + " one = " + str(oSum/oCnt) + " space = " + str(sSum/sCnt))\ + + + + # print("Data = ", end="") + # print(data) + # print("0b"+ "".join([str(i) for i in bitData])) + # print("ClearData = ", end="") + #print(clearData) + print("".join([str(i) for i in clearBitData])) + + +if __name__ == '__main__': + main() \ No newline at end of file From 8e8e8e67f4c3533d7209a7762a5c11d9c37e38c8 Mon Sep 17 00:00:00 2001 From: StellaLupus Date: Sat, 10 Aug 2024 12:06:39 +0300 Subject: [PATCH 3/5] fix: set temp accuracy fix: checksum --- src/IRac.cpp | 2 +- src/ir_Electrolux.cpp | 39 ++++++++------ src/ir_Electrolux.h | 55 +++++++++++++------ tools/calculate_check_bytes.py | 96 ---------------------------------- 4 files changed, 64 insertions(+), 128 deletions(-) delete mode 100644 tools/calculate_check_bytes.py diff --git a/src/IRac.cpp b/src/IRac.cpp index 720c001f2..430814887 100644 --- a/src/IRac.cpp +++ b/src/IRac.cpp @@ -2873,7 +2873,7 @@ void IRac::electrolux(IRElectroluxAc *ac, const bool quiet) { ac->begin(); ac->stateReset(); - ac->setPowerToggle(on); + ac->setPower(on); ac->setMode(ac->convertMode(mode)); ac->setTempModeFahrenheit(!celsius); ac->setTemp(degrees); diff --git a/src/ir_Electrolux.cpp b/src/ir_Electrolux.cpp index a4dd97cbe..ef332a456 100644 --- a/src/ir_Electrolux.cpp +++ b/src/ir_Electrolux.cpp @@ -6,6 +6,7 @@ // Brand: Electrolux, Model: Electrolux EACM EZ/N3 #include "ir_Electrolux.h" +#include #include #include "IRac.h" #include "IRrecv.h" @@ -124,11 +125,11 @@ void IRElectroluxAc::begin(void) { _irsend.begin(); } /// Turn on/off the Power Airwell setting. /// @param[in] on The desired setting state. -void IRElectroluxAc::setPowerToggle(const bool on) { _.PowerToggle = on; } +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::getPowerToggle(void) const { return _.PowerToggle; } +bool IRElectroluxAc::getPower(void) const { return _.Power; } /// Turn on/off the fahrenheit temp mode. /// @param[in] on The desired setting state. @@ -142,14 +143,15 @@ bool IRElectroluxAc::getTempModeFahrenheit(void) const { return _.TempModeFahren /// @param[in] degrees The temperature in celsius or fahrenheit. void IRElectroluxAc::setTemp(const uint8_t degrees) { if(getTempModeFahrenheit()) { - uint8_t temp = std::max(kElectroluxAcMaxFTemp, degrees); - temp = std::min(kElectroluxAcMinFTemp, temp); + uint8_t temp = max(kElectroluxAcMinFTemp, degrees); + temp = min(kElectroluxAcMaxFTemp, temp); _.Temp = (temp - kElectroluxAcMinFTemp); } else { - uint8_t temp = std::max(kElectroluxAcMaxTemp, degrees); - temp = std::min(kElectroluxAcMinTemp, temp); - _.Temp = ((temp - kElectroluxAcMinTemp) * 1.8); //TODO: fix accuracy + uint8_t temp = max(kElectroluxAcMinTemp, degrees); + temp = min(kElectroluxAcMaxTemp, temp); + temp = map(temp, kElectroluxAcMinTemp, kElectroluxAcMaxTemp, kElectroluxAcMinFTemp, kElectroluxAcMaxFTemp); + _.Temp = temp - kElectroluxAcMinFTemp; } } @@ -160,7 +162,8 @@ uint8_t IRElectroluxAc::getTemp(void) const { return _.Temp + kElectroluxAcMinFTemp; } else { - return (_.Temp / 1.8) + kElectroluxAcMinTemp; //TODO: fix accuracy + uint8_t temp = map(_.Temp + kElectroluxAcMinFTemp, kElectroluxAcMinFTemp, kElectroluxAcMaxFTemp, kElectroluxAcMinTemp, kElectroluxAcMaxTemp); + return temp; } } @@ -239,10 +242,10 @@ void IRElectroluxAc::setRaw(const uint64_t state) { _.raw = 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 = GETBITS32(state, 0, kElectroluxAcBits - 4); + uint32_t data = GETBITS64(state, kElectroluxAcChecksumSize + kElectroluxAcChecksumOffset, kElectroluxAcBits - 4); uint8_t result = 0; for (; data; data >>= 4) // Add each nibble together. - result += GETBITS32(data, 0, 4); + result += GETBITS8(data, 0, 4); return (result ^ 0xF) & 0xF; } @@ -325,7 +328,7 @@ stdAc::state_t IRElectroluxAc::toCommon(const stdAc::state_t *prev) const { result.power = false; } result.protocol = decode_type_t::ELETROLUX_AC; - result.power = _.PowerToggle; + result.power = _.Power; result.mode = toCommonMode(_.Mode); result.celsius = !getTempModeFahrenheit(); result.degrees = getTemp(); @@ -351,17 +354,17 @@ stdAc::state_t IRElectroluxAc::toCommon(const stdAc::state_t *prev) const { String IRElectroluxAc::toString(void) const { String result = ""; result.reserve(120); // Reserve some heap for the string to reduce fragging. - result += addBoolToString(_.PowerToggle, kPowerStr, false); + result += addBoolToString(_.Power, kPowerStr, false); result += addModeToString(_.Mode, kElectroluxModeAuto, kElectroluxModeCool, 0xFF, kElectroluxModeDry, kElectroluxModeFan); - result += addTempToString(getTemp()); + result += addTempToString(getTemp(), !getTempModeFahrenheit()); result += addFanToString(_.Fan, kElectroluxFanHigh, kElectroluxFanLow, kElectroluxFanAuto, kElectroluxFanAuto, kElectroluxFanMedium); result += addBoolToString(getQuiet(), kQuietStr); - if(getPowerToggle()) { + if(getPower()) { result += irutils::addLabeledString(irutils::minsToString(getOnOffTimer()), kOffTimerStr); } else { @@ -373,4 +376,10 @@ String IRElectroluxAc::toString(void) const { /// Calculate and set the checksum values for the internal state. void IRElectroluxAc::checksum(void) { _.Sum = calcChecksum(_.raw); -} \ No newline at end of file +} + +/// Set the requested power state of the A/C to on. +void IRElectroluxAc::on(void) { setPower(true); } + +/// Set the requested power state of the A/C to off. +void IRElectroluxAc::off(void) { setPower(false); } \ No newline at end of file diff --git a/src/ir_Electrolux.h b/src/ir_Electrolux.h index 4500c4c51..69d8574a1 100644 --- a/src/ir_Electrolux.h +++ b/src/ir_Electrolux.h @@ -15,25 +15,46 @@ #endif #include "IRremoteESP8266.h" #include "IRsend.h" +#include "map" #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 PowerToggle :1; - uint8_t Fan :2; - uint8_t Temp :5; - uint8_t Mode :3; - uint8_t TimerEnable :1; - uint8_t Timer :4; - uint8_t Quiet :1; - uint8_t :1; - uint8_t TempModeFahrenheit :1; - uint8_t :5; - uint8_t :4; - uint8_t Sum :4; +// struct { +// //Byte 0 +// uint8_t Power :1; +// uint8_t Fan :2; +// uint8_t Temp :5; +// //Byte 1 +// uint8_t Mode :3; +// uint8_t TimerEnable :1; +// uint8_t Timer :4; +// //Byte 2 +// uint8_t Quiet :1; +// uint8_t :1; +// uint8_t TempModeFahrenheit :1; +// uint8_t :5; +// //Byte 3 +// uint8_t :4; +// uint8_t Sum :4; +// uint64_t :0; +// }; + 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 TimerEnable :1; + uint8_t Mode :3; + uint8_t Temp :5; + uint8_t Fan :2; + uint8_t Power :1; + uint64_t :0; }; }; @@ -45,7 +66,7 @@ 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 = 28; +const uint8_t kElectroluxAcChecksumOffset = 0; const uint8_t kElectroluxAcChecksumSize = 4; // Fan @@ -75,8 +96,10 @@ class IRElectroluxAc { int8_t calibrate(void) { return _irsend.calibrate(); } #endif // SEND_ELECTROLUX_AC void begin(); - void setPowerToggle(const bool on); - bool getPowerToggle(void) const; + void on(void); + void off(void); + void setPower(const bool on); + bool getPower(void) const; void setTemp(const uint8_t temp); uint8_t getTemp(void) const; void setFan(const uint8_t speed); diff --git a/tools/calculate_check_bytes.py b/tools/calculate_check_bytes.py deleted file mode 100644 index d3a310ded..000000000 --- a/tools/calculate_check_bytes.py +++ /dev/null @@ -1,96 +0,0 @@ -def calculate_check_bytes(data): - # Split the data into groups of 8 bits (1 byte each) - bytes_data = [data[i:i+4] for i in range(0, len(data) - 8, 4)] - - print(bytes_data) - - # Convert each byte to an integer - int_bytes = [int(byte, 2) for byte in bytes_data] - - # Perform XOR on all bytes - check_byte = 0 - for byte in int_bytes: - check_byte += byte - - check_byte = check_byte % 16 - check_byte ^= 15 - - # Convert check byte to a 4-bit binary string - check_bits = bin(check_byte & 0x0F)[2:].zfill(4) - - return check_bits - -# Example usage -data = ['11100000000000000000000000000001', -'11100011000000000000000000001110', -'11100100000000000000000000001101', -'11100110000000000000000000001011', -'11101000000000000000000000001001', -'11101010000000000000000000000111', -'11101100000000000000000000000101', -'11101101000000000000000000000100', -'11101111000000000000000000000010', -'11110001000000000000000000001111', -'11110011000000000000000000001101', -'11110100000000000000000000001100', -'11110110000000000000000000001010', -'11111000000000000000000000001000', -'11111010000000000000000000000110', -'11111100000000000000000000000100', -'11111110000000000000000000000010', -'11100000000000001000000000001001', -'11100011000000001000000000000110', -'11100100000000001000000000000101', -'11100110000000001000000000000011', -'11101000000000001000000000000001', -'11101010000000001000000000001111', -'11101100000000001000000000001101', -'11101101000000001000000000001100', -'11101111000000001000000000001010', -'11110001000000001000000000000111', -'11110011000000001000000000000101', -'11110100000000001000000000000100', -'11110110000000001000000000000010', -'11111000000000001000000000000000', -'11111010000000001000000000001110', -'11111100000000001000000000001100', -'11111110000000001000000000001010', -'10001011001000000000000000001010', -'10101011001000000000000000001000', -'11001011001000000000000000000110', -'10001011001000001000000000000010', -'10101011001000001000000000000000', -'11001011001000001000000000001110', -'10001011010000000000000000001000', -'10101011010000000000000000000110', -'11001011010000000000000000000100', -'10001011010000001000000000000000', -'10101011010000001000000000001110', -'11001011010000001000000000001100', -'11101011100000000000000000001110', -'11101011100000001000000000000110', -'01101011100000000000000000000110', -'11101011100111000000000000000001', -'11101011100110110000000000000010', -'11101011100110100000000000000011', -'11101011100110010000000000000100', -'11101011100110000000000000000101', -'11101011100101110000000000000110', -'11101011100101100000000000000111', -'11101011100101010000000000001000', -'11101011100101000000000000001001', -'11101011100100110000000000001010', -'11101011100100100000000000001011', -'11101011100100010000000000001100', -'11100000000000000010000000001111', -'11100001000000000010000000001110', -'11100010000000000010000000001101'] - - -for d in data: - check_bits = calculate_check_bytes(d) - if(check_bits == d[28:]): - print("OK -", end="") - else: - print("NO -", end="") - print(f"Calculated: {check_bits} real: {d[28:]}") \ No newline at end of file From 68aff0f26e1f3699cc57343f13f890824b0c4c33 Mon Sep 17 00:00:00 2001 From: StellaLupus Date: Sat, 10 Aug 2024 12:06:39 +0300 Subject: [PATCH 4/5] fix: set temp accuracy fix: checksum --- src/IRac.cpp | 2 +- src/ir_Electrolux.cpp | 42 +++++++++------ src/ir_Electrolux.h | 36 +++++++------ tools/calculate_check_bytes.py | 96 ---------------------------------- 4 files changed, 46 insertions(+), 130 deletions(-) delete mode 100644 tools/calculate_check_bytes.py diff --git a/src/IRac.cpp b/src/IRac.cpp index 720c001f2..430814887 100644 --- a/src/IRac.cpp +++ b/src/IRac.cpp @@ -2873,7 +2873,7 @@ void IRac::electrolux(IRElectroluxAc *ac, const bool quiet) { ac->begin(); ac->stateReset(); - ac->setPowerToggle(on); + ac->setPower(on); ac->setMode(ac->convertMode(mode)); ac->setTempModeFahrenheit(!celsius); ac->setTemp(degrees); diff --git a/src/ir_Electrolux.cpp b/src/ir_Electrolux.cpp index a4dd97cbe..a7a8406c2 100644 --- a/src/ir_Electrolux.cpp +++ b/src/ir_Electrolux.cpp @@ -6,6 +6,7 @@ // Brand: Electrolux, Model: Electrolux EACM EZ/N3 #include "ir_Electrolux.h" +#include #include #include "IRac.h" #include "IRrecv.h" @@ -36,8 +37,7 @@ void IRsend::sendElectroluxAc(const uint64_t data, const uint16_t nbits, const u // Header mark(kElectroluxAcHdrMark); space(kElectroluxAcHdrSpace); - // Data Section #1 - // e.g. data = 0xED000004, nbits = 32 + // Data Section sendData(kElectroluxAcBitMark, kElectroluxAcOneSpace, kElectroluxAcBitMark, kElectroluxAcZeroSpace, send_data, 32, true); send_data >>= 32; // Footer @@ -124,11 +124,11 @@ void IRElectroluxAc::begin(void) { _irsend.begin(); } /// Turn on/off the Power Airwell setting. /// @param[in] on The desired setting state. -void IRElectroluxAc::setPowerToggle(const bool on) { _.PowerToggle = on; } +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::getPowerToggle(void) const { return _.PowerToggle; } +bool IRElectroluxAc::getPower(void) const { return _.Power; } /// Turn on/off the fahrenheit temp mode. /// @param[in] on The desired setting state. @@ -142,14 +142,15 @@ bool IRElectroluxAc::getTempModeFahrenheit(void) const { return _.TempModeFahren /// @param[in] degrees The temperature in celsius or fahrenheit. void IRElectroluxAc::setTemp(const uint8_t degrees) { if(getTempModeFahrenheit()) { - uint8_t temp = std::max(kElectroluxAcMaxFTemp, degrees); - temp = std::min(kElectroluxAcMinFTemp, temp); + uint8_t temp = max(kElectroluxAcMinFTemp, degrees); + temp = min(kElectroluxAcMaxFTemp, temp); _.Temp = (temp - kElectroluxAcMinFTemp); } else { - uint8_t temp = std::max(kElectroluxAcMaxTemp, degrees); - temp = std::min(kElectroluxAcMinTemp, temp); - _.Temp = ((temp - kElectroluxAcMinTemp) * 1.8); //TODO: fix accuracy + uint8_t temp = max(kElectroluxAcMinTemp, degrees); + temp = min(kElectroluxAcMaxTemp, temp); + temp = map(temp, kElectroluxAcMinTemp, kElectroluxAcMaxTemp, kElectroluxAcMinFTemp, kElectroluxAcMaxFTemp); + _.Temp = temp - kElectroluxAcMinFTemp; } } @@ -160,7 +161,8 @@ uint8_t IRElectroluxAc::getTemp(void) const { return _.Temp + kElectroluxAcMinFTemp; } else { - return (_.Temp / 1.8) + kElectroluxAcMinTemp; //TODO: fix accuracy + uint8_t temp = map(_.Temp + kElectroluxAcMinFTemp, kElectroluxAcMinFTemp, kElectroluxAcMaxFTemp, kElectroluxAcMinTemp, kElectroluxAcMaxTemp); + return temp; } } @@ -239,10 +241,10 @@ void IRElectroluxAc::setRaw(const uint64_t state) { _.raw = 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 = GETBITS32(state, 0, kElectroluxAcBits - 4); + uint32_t data = GETBITS64(state, kElectroluxAcChecksumSize + kElectroluxAcChecksumOffset, kElectroluxAcBits - 4); uint8_t result = 0; for (; data; data >>= 4) // Add each nibble together. - result += GETBITS32(data, 0, 4); + result += GETBITS8(data, 0, 4); return (result ^ 0xF) & 0xF; } @@ -325,7 +327,7 @@ stdAc::state_t IRElectroluxAc::toCommon(const stdAc::state_t *prev) const { result.power = false; } result.protocol = decode_type_t::ELETROLUX_AC; - result.power = _.PowerToggle; + result.power = _.Power; result.mode = toCommonMode(_.Mode); result.celsius = !getTempModeFahrenheit(); result.degrees = getTemp(); @@ -351,17 +353,17 @@ stdAc::state_t IRElectroluxAc::toCommon(const stdAc::state_t *prev) const { String IRElectroluxAc::toString(void) const { String result = ""; result.reserve(120); // Reserve some heap for the string to reduce fragging. - result += addBoolToString(_.PowerToggle, kPowerStr, false); + result += addBoolToString(_.Power, kPowerStr, false); result += addModeToString(_.Mode, kElectroluxModeAuto, kElectroluxModeCool, 0xFF, kElectroluxModeDry, kElectroluxModeFan); - result += addTempToString(getTemp()); + result += addTempToString(getTemp(), !getTempModeFahrenheit()); result += addFanToString(_.Fan, kElectroluxFanHigh, kElectroluxFanLow, kElectroluxFanAuto, kElectroluxFanAuto, kElectroluxFanMedium); result += addBoolToString(getQuiet(), kQuietStr); - if(getPowerToggle()) { + if(getPower()) { result += irutils::addLabeledString(irutils::minsToString(getOnOffTimer()), kOffTimerStr); } else { @@ -373,4 +375,10 @@ String IRElectroluxAc::toString(void) const { /// Calculate and set the checksum values for the internal state. void IRElectroluxAc::checksum(void) { _.Sum = calcChecksum(_.raw); -} \ No newline at end of file +} + +/// Set the requested power state of the A/C to on. +void IRElectroluxAc::on(void) { setPower(true); } + +/// Set the requested power state of the A/C to off. +void IRElectroluxAc::off(void) { setPower(false); } \ No newline at end of file diff --git a/src/ir_Electrolux.h b/src/ir_Electrolux.h index 4500c4c51..d3f7ee92e 100644 --- a/src/ir_Electrolux.h +++ b/src/ir_Electrolux.h @@ -15,25 +15,27 @@ #endif #include "IRremoteESP8266.h" #include "IRsend.h" +#include "map" #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 PowerToggle :1; - uint8_t Fan :2; - uint8_t Temp :5; - uint8_t Mode :3; - uint8_t TimerEnable :1; - uint8_t Timer :4; - uint8_t Quiet :1; - uint8_t :1; - uint8_t TempModeFahrenheit :1; - uint8_t :5; - uint8_t :4; - uint8_t Sum :4; + 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 TimerEnable :1; + uint8_t Mode :3; + uint8_t Temp :5; + uint8_t Fan :2; + uint8_t Power :1; + uint64_t :0; }; }; @@ -45,7 +47,7 @@ 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 = 28; +const uint8_t kElectroluxAcChecksumOffset = 0; const uint8_t kElectroluxAcChecksumSize = 4; // Fan @@ -75,8 +77,10 @@ class IRElectroluxAc { int8_t calibrate(void) { return _irsend.calibrate(); } #endif // SEND_ELECTROLUX_AC void begin(); - void setPowerToggle(const bool on); - bool getPowerToggle(void) const; + void on(void); + void off(void); + void setPower(const bool on); + bool getPower(void) const; void setTemp(const uint8_t temp); uint8_t getTemp(void) const; void setFan(const uint8_t speed); diff --git a/tools/calculate_check_bytes.py b/tools/calculate_check_bytes.py deleted file mode 100644 index d3a310ded..000000000 --- a/tools/calculate_check_bytes.py +++ /dev/null @@ -1,96 +0,0 @@ -def calculate_check_bytes(data): - # Split the data into groups of 8 bits (1 byte each) - bytes_data = [data[i:i+4] for i in range(0, len(data) - 8, 4)] - - print(bytes_data) - - # Convert each byte to an integer - int_bytes = [int(byte, 2) for byte in bytes_data] - - # Perform XOR on all bytes - check_byte = 0 - for byte in int_bytes: - check_byte += byte - - check_byte = check_byte % 16 - check_byte ^= 15 - - # Convert check byte to a 4-bit binary string - check_bits = bin(check_byte & 0x0F)[2:].zfill(4) - - return check_bits - -# Example usage -data = ['11100000000000000000000000000001', -'11100011000000000000000000001110', -'11100100000000000000000000001101', -'11100110000000000000000000001011', -'11101000000000000000000000001001', -'11101010000000000000000000000111', -'11101100000000000000000000000101', -'11101101000000000000000000000100', -'11101111000000000000000000000010', -'11110001000000000000000000001111', -'11110011000000000000000000001101', -'11110100000000000000000000001100', -'11110110000000000000000000001010', -'11111000000000000000000000001000', -'11111010000000000000000000000110', -'11111100000000000000000000000100', -'11111110000000000000000000000010', -'11100000000000001000000000001001', -'11100011000000001000000000000110', -'11100100000000001000000000000101', -'11100110000000001000000000000011', -'11101000000000001000000000000001', -'11101010000000001000000000001111', -'11101100000000001000000000001101', -'11101101000000001000000000001100', -'11101111000000001000000000001010', -'11110001000000001000000000000111', -'11110011000000001000000000000101', -'11110100000000001000000000000100', -'11110110000000001000000000000010', -'11111000000000001000000000000000', -'11111010000000001000000000001110', -'11111100000000001000000000001100', -'11111110000000001000000000001010', -'10001011001000000000000000001010', -'10101011001000000000000000001000', -'11001011001000000000000000000110', -'10001011001000001000000000000010', -'10101011001000001000000000000000', -'11001011001000001000000000001110', -'10001011010000000000000000001000', -'10101011010000000000000000000110', -'11001011010000000000000000000100', -'10001011010000001000000000000000', -'10101011010000001000000000001110', -'11001011010000001000000000001100', -'11101011100000000000000000001110', -'11101011100000001000000000000110', -'01101011100000000000000000000110', -'11101011100111000000000000000001', -'11101011100110110000000000000010', -'11101011100110100000000000000011', -'11101011100110010000000000000100', -'11101011100110000000000000000101', -'11101011100101110000000000000110', -'11101011100101100000000000000111', -'11101011100101010000000000001000', -'11101011100101000000000000001001', -'11101011100100110000000000001010', -'11101011100100100000000000001011', -'11101011100100010000000000001100', -'11100000000000000010000000001111', -'11100001000000000010000000001110', -'11100010000000000010000000001101'] - - -for d in data: - check_bits = calculate_check_bytes(d) - if(check_bits == d[28:]): - print("OK -", end="") - else: - print("NO -", end="") - print(f"Calculated: {check_bits} real: {d[28:]}") \ No newline at end of file From a9e750c5795acee18bfeeb67c1f1c8316cc3ca8c Mon Sep 17 00:00:00 2001 From: StellaLupus Date: Sat, 10 Aug 2024 12:52:37 +0300 Subject: [PATCH 5/5] fix: build tests fix: code cleanup and formatting fix: test build --- src/IRac.cpp | 16 +- src/IRac.h | 2 +- src/IRrecv.cpp | 2 +- src/IRrecv.h | 9 +- src/IRremoteESP8266.h | 4 +- src/IRsend.cpp | 7 + src/ir_Electrolux.cpp | 478 ++++++++++++++++++++---------------- src/ir_Electrolux.h | 185 ++++++++------ test/IRac_test.cpp | 26 ++ test/ir_Electrolux_test.cpp | 19 ++ tools/serial_parser.py | 99 -------- 11 files changed, 449 insertions(+), 398 deletions(-) create mode 100644 test/ir_Electrolux_test.cpp delete mode 100644 tools/serial_parser.py diff --git a/src/IRac.cpp b/src/IRac.cpp index 430814887..11d161e2f 100644 --- a/src/IRac.cpp +++ b/src/IRac.cpp @@ -244,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 @@ -375,9 +378,6 @@ bool IRac::isProtocolSupported(const decode_type_t protocol) { #if SEND_WHIRLPOOL_AC case decode_type_t::WHIRLPOOL_AC: #endif -#if SEND_ELECTROLUX_AC - case decode_type_t::ELETROLUX_AC: -#endif // SEND_ELECTROLUX_AC return true; default: return false; @@ -3678,10 +3678,12 @@ bool IRac::sendAc(const stdAc::state_t desired, const stdAc::state_t *prev) { } #endif // SEND_TRANSCOLD_AC #if SEND_ELECTROLUX_AC - case ELETROLUX_AC: + case ELECTROLUX_AC: { IRElectroluxAc ac(_pin, _inverted, _modulation); - electrolux(&ac, send.power, send.mode, send.celsius, send.degrees, send.fanspeed, send.quiet); + electrolux(&ac, send.power, send.mode, + send.celsius, send.degrees, + send.fanspeed, send.quiet); break; } #endif // SEND_ELECTROLUX_AC @@ -4565,7 +4567,7 @@ namespace IRAcUtils { } #endif // DECODE_YORK #if DECODE_ELECTROLUX_AC - case decode_type_t::ELETROLUX_AC: { + case decode_type_t::ELECTROLUX_AC: { IRElectroluxAc ac(kGpioUnused); ac.setRaw(result->value); // ELETROLUX_AC uses value instead of state. return ac.toString(); @@ -5116,7 +5118,7 @@ namespace IRAcUtils { } #endif // DECODE_YORK #if DECODE_ELECTROLUX_AC - case decode_type_t::ELETROLUX_AC: { + case decode_type_t::ELECTROLUX_AC: { IRCarrierAc64 ac(kGpioUnused); ac.setRaw(decode->value); // Uses value instead of state. *result = ac.toCommon(); diff --git a/src/IRac.h b/src/IRac.h index 3a32f61b0..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" @@ -50,7 +51,6 @@ #include "ir_Voltas.h" #include "ir_Whirlpool.h" #include "ir_York.h" -#include "ir_Electrolux.h" // Constants const int8_t kGpioUnused = -1; ///< A placeholder for not using an actual GPIO. diff --git a/src/IRrecv.cpp b/src/IRrecv.cpp index 8ed9cdd62..c61d721cd 100644 --- a/src/IRrecv.cpp +++ b/src/IRrecv.cpp @@ -1192,7 +1192,7 @@ bool IRrecv::decode(decode_results *results, irparams_t *save, #if DECODE_ELECTROLUX_AC DPRINTLN("Attempting Electrolux AC decode"); if (decodeElectroluxAc(results, offset)) return true; -#endif //DECODE_ELECTROLUX_AC +#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 50094e43a..cd6784108 100644 --- a/src/IRrecv.h +++ b/src/IRrecv.h @@ -890,10 +890,11 @@ class IRrecv { 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 + 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 06031396e..95ddf3a5d 100644 --- a/src/IRremoteESP8266.h +++ b/src/IRremoteESP8266.h @@ -1152,9 +1152,9 @@ enum decode_type_t { CARRIER_AC84, // 125 YORK, BLUESTARHEAVY, - ELETROLUX_AC, + ELECTROLUX_AC, // Add new entries before this one, and update it to point to the last entry. - kLastDecodeType = ELETROLUX_AC, + kLastDecodeType = ELECTROLUX_AC, }; // Message lengths & required repeat values 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/ir_Electrolux.cpp b/src/ir_Electrolux.cpp index a7a8406c2..b30675395 100644 --- a/src/ir_Electrolux.cpp +++ b/src/ir_Electrolux.cpp @@ -6,7 +6,6 @@ // Brand: Electrolux, Model: Electrolux EACM EZ/N3 #include "ir_Electrolux.h" -#include #include #include "IRac.h" #include "IRrecv.h" @@ -20,7 +19,7 @@ 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; // Hz. (Guessing the most common frequency.) +const uint16_t kElectroluxAcFreq = 38000; const uint16_t kElectroluxAcOverhead = 3; #if SEND_ELECTROLUX_AC @@ -30,20 +29,29 @@ const uint16_t kElectroluxAcOverhead = 3; /// @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, 32, true); - send_data >>= 32; - // Footer - mark(kElectroluxAcBitMark); - space(kDefaultMessageGap); // A 100% made up guess of the gap between messages. - } +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 @@ -57,45 +65,49 @@ void IRsend::sendElectroluxAc(const uint64_t data, const uint16_t nbits, const u /// @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) //rawlen = 68, nbits = 104 - return false; // Too short a message to match. - if (strict && nbits != kElectroluxAcBits) - return false; - - uint64_t data = 0; - match_result_t data_result; - - // 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 - 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::ELETROLUX_AC; - results->bits = nbits; - results->value = data; - results->command = data & 0xFFF; - results->address = 0; - return true; +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 @@ -103,119 +115,138 @@ bool IRrecv::decodeElectroluxAc(decode_results *results, uint16_t offset, const /// @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) { stateReset(); } +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(void) { _.raw = 0xF3008005; } +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); + _irsend.sendElectroluxAc(getRaw(), kElectroluxAcBits, repeat); } #endif // SEND_ELECTROLUX_AC /// Set up hardware to be able to send a message. -void IRElectroluxAc::begin(void) { _irsend.begin(); } +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; } +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(void) const { return _.Power; } +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; } +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(void) const { return _.TempModeFahrenheit; } +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); - temp = map(temp, kElectroluxAcMinTemp, kElectroluxAcMaxTemp, kElectroluxAcMinFTemp, kElectroluxAcMaxFTemp); - _.Temp = temp - kElectroluxAcMinFTemp; - } + 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(void) const { - if(getTempModeFahrenheit()) { - return _.Temp + kElectroluxAcMinFTemp; - } - else { - uint8_t temp = map(_.Temp + kElectroluxAcMinFTemp, kElectroluxAcMinFTemp, kElectroluxAcMaxFTemp, kElectroluxAcMinTemp, kElectroluxAcMaxTemp); - return temp; - } +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); + _.Fan = (_.Mode == kElectroluxModeAuto) + ? kElectroluxFanAuto + : std::min(speed, kElectroluxFanAuto); } /// Get the current fan speed setting. /// @return The current fan speed. -uint8_t IRElectroluxAc::getFan(void) const { return _.Fan; } +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; - } + 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(void) const { return _.Mode; } +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_hours) { - uint8_t hours = std::min((uint8_t)(nr_of_hours / 60), kElectroluxTimerMax); - // The time can be changed in sleep mode, but doesn't set the flag. - _.TimerEnable = hours > 0; - _.Timer = std::max(kElectroluxTimerMin, hours); // Hours +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(void) const { - if (_.TimerEnable) - return _.Timer * 60; - else - return 0; +uint16_t IRElectroluxAc::getOnOffTimer() const { + return _.TimerEnabled > 0 ? _.Timer * 60 : 0; } /// Set the Quiet setting of the A/C. @@ -224,13 +255,13 @@ 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(void) const { return _.Quiet; } +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(void) { - checksum(); // Ensure correct settings before sending. - return _.raw; +uint64_t IRElectroluxAc::getRaw() { + checksum(); // Ensure correct settings before sending. + return _.raw; } /// Set the internal state from a valid code for this protocol. @@ -241,144 +272,181 @@ void IRElectroluxAc::setRaw(const uint64_t state) { _.raw = 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; + 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)); + // 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; - } + 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; - } + 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; - } + 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; - } + 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 != NULL) { - 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 = decode_type_t::ELETROLUX_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; + 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(void) const { - String result = ""; - result.reserve(120); // Reserve some 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; +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(void) { - _.Sum = calcChecksum(_.raw); +void IRElectroluxAc::checksum() { + _.Sum = calcChecksum(_.raw); } /// Set the requested power state of the A/C to on. -void IRElectroluxAc::on(void) { setPower(true); } +void IRElectroluxAc::on() { setPower(true); } /// Set the requested power state of the A/C to off. -void IRElectroluxAc::off(void) { setPower(false); } \ No newline at end of file +void IRElectroluxAc::off() { setPower(false); } diff --git a/src/ir_Electrolux.h b/src/ir_Electrolux.h index d3f7ee92e..a9f6958f8 100644 --- a/src/ir_Electrolux.h +++ b/src/ir_Electrolux.h @@ -5,116 +5,143 @@ // Supports: // Brand: Electrolux, Model: Electrolux EACM EZ/N3 -#ifndef IR_ELECTROLUX_AC_H_ -#define IR_ELECTROLUX_AC_H_ +#ifndef IR_ELECTROLUX_H_ +#define IR_ELECTROLUX_H_ -#define __STDC_LIMIT_MACROS -#include +#define STDC_LIMIT_MACROS +#include #ifndef UNIT_TEST #include #endif #include "IRremoteESP8266.h" #include "IRsend.h" -#include "map" #ifdef UNIT_TEST #include "IRsend_test.h" #endif -union ElectroluxAcProtocol{ +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 TimerEnable :1; - uint8_t Mode :3; - uint8_t Temp :5; - uint8_t Fan :2; - uint8_t Power :1; - uint64_t :0; + 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 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 +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 - +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(const uint16_t pin, const bool inverted = false, - const bool use_modulation = true); - void stateReset(); + public: + explicit IRElectroluxAc(uint16_t pin, bool inverted = false, + bool use_modulation = true); + + void stateReset(); #if SEND_ELECTROLUX_AC - void send(const 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(void) { return _irsend.calibrate(); } + 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); - void off(void); - void setPower(const bool on); - bool getPower(void) const; - void setTemp(const uint8_t temp); - uint8_t getTemp(void) const; - void setFan(const uint8_t speed); - uint8_t getFan(void) const; - void setMode(const uint8_t mode); - uint8_t getMode(void) const; - void setOnOffTimer(const uint16_t nr_of_mins); - uint16_t getOnOffTimer(void) const; - void setQuiet(const bool on); - bool getQuiet(void) const; - void setTempModeFahrenheit(const bool on); - bool getTempModeFahrenheit(void) const; - uint64_t getRaw(void); - void setRaw(const uint64_t state); - static uint8_t calcChecksum(const uint64_t state); - static bool validChecksum(const uint64_t state); - static uint8_t convertMode(const stdAc::opmode_t mode); - static uint8_t convertFan(const stdAc::fanspeed_t speed); - static stdAc::opmode_t toCommonMode(const uint8_t mode); - static stdAc::fanspeed_t toCommonFanSpeed(const uint8_t speed); - stdAc::state_t toCommon(const stdAc::state_t *prev = NULL) const; - String toString(void) const; + 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 + + 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(void); +#endif // UNIT_TEST + ElectroluxAcProtocol _{}; + + void checksum(); }; -#endif // IR_ELECTROLUX_AC_H_ \ No newline at end of file +#endif // IR_ELECTROLUX_H_ 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 diff --git a/tools/serial_parser.py b/tools/serial_parser.py deleted file mode 100644 index 124915103..000000000 --- a/tools/serial_parser.py +++ /dev/null @@ -1,99 +0,0 @@ -import enum -import serial -import serial.tools -import serial.tools.list_ports - -def substring_after(str: str, searchStr: str): - return str[str.index(searchStr) + len(searchStr):] - -def getPort(): - ports = sorted(serial.tools.list_ports.comports()) - print("Available ports to listing:") - for id, portInfo in enumerate(ports): - print("{}. - {}: {} [{}]".format(id, portInfo.device, portInfo.description, portInfo.hwid)) - print("Select port: ", end="") - selectId = int(input()) - if selectId < len(ports) and selectId >= 0: - return ports[selectId] - else: - print("Unrecognized port number") - return getPort() - -zero = 756 -one = 2149 -space = 752 -interv = 0.2 - -def getBitFromInterv(value: int): - if(value > zero - zero * interv and value < zero + zero * interv): - return 0 - elif(value > one - one * interv and value < one + one * interv): - return 1 - else: - return 2 - -def bitListToInt(bitlist): - out = 0 - for bit in bitlist: - out = (out << 1) | bit - return out - - -def main(): - port = getPort() - print("Selected port:" + port.device) - - ser = serial.Serial( - port=port.device, - baudrate=115200, - ) - - zSum = 0 - zCnt = 0 - oSum = 0 - oCnt = 0 - sSum = 0 - sCnt = 0 - - while True: - try: - dataStr = ser.readline().decode() - except: - continue - #print(dataStr) - if("uint16_t rawData" in dataStr): - dataStrArray = str(dataStr[dataStr.index('{') + 1:dataStr.index('}')]).split(",") - data = [int(i.strip()) for i in dataStrArray] - data = data[2:] - clearData = [i for idi, i in enumerate(data) if idi % 2 == 1] - bitData = [getBitFromInterv(i) for i in data] - clearBitData = [getBitFromInterv(i) for i in clearData] - - - - for idd, d in enumerate(data): - if(idd % 2 == 0): - sSum += d - sCnt += 1 - else: - if(getBitFromInterv(d) == 0): - zSum += d - zCnt += 1 - elif(getBitFromInterv(d) == 1): - oSum += d - oCnt += 1 - - # print("zero = " + str(zSum/zCnt) + " one = " + str(oSum/oCnt) + " space = " + str(sSum/sCnt))\ - - - - # print("Data = ", end="") - # print(data) - # print("0b"+ "".join([str(i) for i in bitData])) - # print("ClearData = ", end="") - #print(clearData) - print("".join([str(i) for i in clearBitData])) - - -if __name__ == '__main__': - main() \ No newline at end of file