From 77ba26b4d9a53a61b243251aec6d8f7183518613 Mon Sep 17 00:00:00 2001 From: David Conran Date: Sun, 4 Apr 2021 18:08:34 +1000 Subject: [PATCH] Experimental detailed support for Truma A/Cs. (#1449) * add `sendTruma()` & `decodeTruma()`. * update ancillary routines as needed. * Support settings of: - Power - Temperature - Operating Mode - Fan Speed - Quiet - Checksums. * Update `IRac` class to add support as described. * Add unit test coverage for new protocol & class. Fixes #1440 --- src/IRac.cpp | 58 +++++++ src/IRac.h | 6 + src/IRrecv.cpp | 5 + src/IRrecv.h | 4 + src/IRremoteESP8266.h | 11 +- src/IRsend.cpp | 6 + src/IRsend.h | 4 + src/IRtext.cpp | 1 + src/ir_Truma.cpp | 340 +++++++++++++++++++++++++++++++++++++++++ src/ir_Truma.h | 126 +++++++++++++++ src/locale/defaults.h | 3 + test/IRac_test.cpp | 30 ++++ test/ir_Truma_test.cpp | 287 ++++++++++++++++++++++++++++++++++ 13 files changed, 880 insertions(+), 1 deletion(-) create mode 100644 src/ir_Truma.cpp create mode 100644 src/ir_Truma.h create mode 100644 test/ir_Truma_test.cpp diff --git a/src/IRac.cpp b/src/IRac.cpp index aa55f0fce..87a285504 100644 --- a/src/IRac.cpp +++ b/src/IRac.cpp @@ -44,6 +44,7 @@ #include "ir_Toshiba.h" #include "ir_Transcold.h" #include "ir_Trotec.h" +#include "ir_Truma.h" #include "ir_Vestel.h" #include "ir_Voltas.h" #include "ir_Whirlpool.h" @@ -275,6 +276,9 @@ bool IRac::isProtocolSupported(const decode_type_t protocol) { #if SEND_TROTEC case decode_type_t::TROTEC: #endif +#if SEND_TRUMA + case decode_type_t::TRUMA: +#endif // SEND_TRUMA #if SEND_VESTEL_AC case decode_type_t::VESTEL_AC: #endif @@ -1980,6 +1984,37 @@ void IRac::trotec(IRTrotecESP *ac, } #endif // SEND_TROTEC +#if SEND_TRUMA +/// Send a Truma A/C message with the supplied settings. +/// @param[in, out] ac A Ptr to an IRTrumaAc object to use. +/// @param[in] on The power setting. +/// @param[in] mode The operation mode setting. +/// @param[in] degrees The temperature setting in degrees. +/// @param[in] fan The speed setting for the fan. +/// @param[in] quiet Run the device quietly if we can. +void IRac::truma(IRTrumaAc *ac, + const bool on, const stdAc::opmode_t mode, + const float degrees, const stdAc::fanspeed_t fan, + const bool quiet) { + ac->begin(); + ac->setPower(on); + ac->setMode(ac->convertMode(mode)); + ac->setTemp(degrees); + ac->setFan(ac->convertFan(fan)); + ac->setQuiet(quiet); // Only available in Cool mode. + // No Vertical swing setting available. + // No Horizontal swing setting available. + // No Turbo setting available. + // No Light setting available. + // No Filter setting available. + // No Clean setting available. + // No Beep setting available. + // No Sleep setting available. + // No Clock setting available. + ac->send(); +} +#endif // SEND_TRUMA + #if SEND_VESTEL_AC /// Send a Vestel A/C message with the supplied settings. /// @param[in, out] ac A Ptr to an IRVestelAc object to use. @@ -2713,6 +2748,14 @@ bool IRac::sendAc(const stdAc::state_t desired, const stdAc::state_t *prev) { break; } #endif // SEND_TROTEC +#if SEND_TRUMA + case TRUMA: + { + IRTrumaAc ac(_pin, _inverted, _modulation); + truma(&ac, send.power, send.mode, degC, send.fanspeed, send.quiet); + break; + } +#endif // SEND_TRUMA #if SEND_VESTEL_AC case VESTEL_AC: { @@ -3298,6 +3341,13 @@ namespace IRAcUtils { return ac.toString(); } #endif // DECODE_TROTEC +#if DECODE_TRUMA + case decode_type_t::TRUMA: { + IRTrumaAc ac(kGpioUnused); + ac.setRaw(result->value); // Truma uses value instead of state. + return ac.toString(); + } +#endif // DECODE_TRUMA #if DECODE_GOODWEATHER case decode_type_t::GOODWEATHER: { IRGoodweatherAc ac(kGpioUnused); @@ -3878,6 +3928,14 @@ namespace IRAcUtils { break; } #endif // DECODE_TROTEC +#if DECODE_TRUMA + case decode_type_t::TRUMA: { + IRTrumaAc ac(kGpioUnused); + ac.setRaw(decode->value); // Uses value instead of state. + *result = ac.toCommon(); + break; + } +#endif // DECODE_TRUMA #if DECODE_VESTEL_AC case decode_type_t::VESTEL_AC: { IRVestelAc ac(kGpioUnused); diff --git a/src/IRac.h b/src/IRac.h index 10e9879a1..95bb01259 100644 --- a/src/IRac.h +++ b/src/IRac.h @@ -38,6 +38,7 @@ #include "ir_Toshiba.h" #include "ir_Transcold.h" #include "ir_Trotec.h" +#include "ir_Truma.h" #include "ir_Vestel.h" #include "ir_Voltas.h" #include "ir_Whirlpool.h" @@ -420,6 +421,11 @@ void electra(IRElectraAc *ac, const bool on, const stdAc::opmode_t mode, const float degrees, const stdAc::fanspeed_t fan, const int16_t sleep = -1); #endif // SEND_TROTEC +#if SEND_TRUMA + void truma(IRTrumaAc *ac, + const bool on, const stdAc::opmode_t mode, const float degrees, + const stdAc::fanspeed_t fan, const bool quiet); +#endif // SEND_TRUMA #if SEND_VESTEL_AC void vestel(IRVestelAc *ac, const bool on, const stdAc::opmode_t mode, const float degrees, diff --git a/src/IRrecv.cpp b/src/IRrecv.cpp index 5822836d8..36e0c0e83 100644 --- a/src/IRrecv.cpp +++ b/src/IRrecv.cpp @@ -933,6 +933,11 @@ bool IRrecv::decode(decode_results *results, irparams_t *save, DPRINTLN("Attempting Doshisha decode"); if (decodeDoshisha(results, offset)) return true; #endif // DECODE_DOSHISHA +#if DECODE_TRUMA + // Needs to happen before decodeMultibrackets() as they can appear similar. + DPRINTLN("Attempting Truma decode"); + if (decodeTruma(results, offset)) return true; +#endif // DECODE_TRUMA #if DECODE_MULTIBRACKETS DPRINTLN("Attempting Multibrackets decode"); if (decodeMultibrackets(results, offset)) return true; diff --git a/src/IRrecv.h b/src/IRrecv.h index 9ce9385e8..b0b43aa9b 100644 --- a/src/IRrecv.h +++ b/src/IRrecv.h @@ -729,6 +729,10 @@ class IRrecv { bool decodeXmp(decode_results *results, uint16_t offset = kStartOffset, const uint16_t nbits = kXmpBits, const bool strict = true); #endif // DECODE_XMP +#if DECODE_TRUMA + bool decodeTruma(decode_results *results, uint16_t offset = kStartOffset, + const uint16_t nbits = kTrumaBits, const bool strict = true); +#endif // DECODE_TRUMA }; #endif // IRRECV_H_ diff --git a/src/IRremoteESP8266.h b/src/IRremoteESP8266.h index 8346933c1..1391601de 100644 --- a/src/IRremoteESP8266.h +++ b/src/IRremoteESP8266.h @@ -740,6 +740,13 @@ #define SEND_XMP _IR_ENABLE_DEFAULT_ #endif // SEND_XMP +#ifndef DECODE_TRUMA +#define DECODE_TRUMA _IR_ENABLE_DEFAULT_ +#endif // DECODE_TRUMA +#ifndef SEND_TRUMA +#define SEND_TRUMA _IR_ENABLE_DEFAULT_ +#endif // SEND_TRUMA + #if (DECODE_ARGO || DECODE_DAIKIN || DECODE_FUJITSU_AC || DECODE_GREE || \ DECODE_KELVINATOR || DECODE_MITSUBISHI_AC || DECODE_TOSHIBA_AC || \ DECODE_TROTEC || DECODE_HAIER_AC || DECODE_HITACHI_AC || \ @@ -891,8 +898,9 @@ enum decode_type_t { MILESTAG2, ECOCLIM, XMP, + TRUMA, // 100 // Add new entries before this one, and update it to point to the last entry. - kLastDecodeType = XMP, + kLastDecodeType = TRUMA, }; // Message lengths & required repeat values @@ -1111,6 +1119,7 @@ const uint16_t kTranscoldDefaultRepeat = kNoRepeat; const uint16_t kTrotecStateLength = 9; const uint16_t kTrotecBits = kTrotecStateLength * 8; const uint16_t kTrotecDefaultRepeat = kNoRepeat; +const uint16_t kTrumaBits = 56; const uint16_t kWhirlpoolAcStateLength = 21; const uint16_t kWhirlpoolAcBits = kWhirlpoolAcStateLength * 8; const uint16_t kWhirlpoolAcDefaultRepeat = kNoRepeat; diff --git a/src/IRsend.cpp b/src/IRsend.cpp index e4a6bbd46..c32eedea4 100644 --- a/src/IRsend.cpp +++ b/src/IRsend.cpp @@ -665,6 +665,7 @@ uint16_t IRsend::defaultBits(const decode_type_t protocol) { case MAGIQUEST: case VESTEL_AC: case TECHNIBEL_AC: + case TRUMA: return 56; case AMCOR: case CARRIER_AC64: @@ -1022,6 +1023,11 @@ bool IRsend::send(const decode_type_t type, const uint64_t data, sendTranscold(data, nbits, min_repeat); break; #endif // SEND_TRANSCOLD +#if SEND_TRUMA + case TRUMA: + sendTruma(data, nbits, min_repeat); + break; +#endif // SEND_TRUMA #if SEND_VESTEL_AC case VESTEL_AC: sendVestelAc(data, nbits, min_repeat); diff --git a/src/IRsend.h b/src/IRsend.h index 1893d96eb..d95ee7e79 100644 --- a/src/IRsend.h +++ b/src/IRsend.h @@ -702,6 +702,10 @@ class IRsend { void sendXmp(const uint64_t data, const uint16_t nbits = kXmpBits, const uint16_t repeat = kNoRepeat); #endif // SEND_XMP +#if SEND_TRUMA + void sendTruma(const uint64_t data, const uint16_t nbits = kTrumaBits, + const uint16_t repeat = kNoRepeat); +#endif // SEND_TRUMA protected: #ifdef UNIT_TEST diff --git a/src/IRtext.cpp b/src/IRtext.cpp index ec405f731..0eadf2533 100644 --- a/src/IRtext.cpp +++ b/src/IRtext.cpp @@ -282,5 +282,6 @@ const PROGMEM char *kAllProtocolNamesStr = D_STR_MILESTAG2 "\x0" D_STR_ECOCLIM "\x0" D_STR_XMP "\x0" + D_STR_TRUMA "\x0" ///< New protocol strings should be added just above this line. "\x0"; ///< This string requires double null termination. diff --git a/src/ir_Truma.cpp b/src/ir_Truma.cpp new file mode 100644 index 000000000..853c640e1 --- /dev/null +++ b/src/ir_Truma.cpp @@ -0,0 +1,340 @@ +// Copyright 2021 David Conran (crankyoldgit) + +/// @file +/// @brief Support for Truma protocol. +/// This protocol uses mark length bit encoding. +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1440 +/// @see https://docs.google.com/spreadsheets/d/1k-RHu0vSIB6IweiTZSa3Rxy3Z_qPUtqwcqot8uXVO6I/edit?usp=sharing + + +#include "ir_Truma.h" +#include +#include "IRrecv.h" +#include "IRsend.h" +#include "IRtext.h" +#include "IRutils.h" + +using irutils::addBoolToString; +using irutils::addFanToString; +using irutils::addModeToString; +using irutils::addTempToString; + +// Constants + +const uint16_t kTrumaLdrMark = 20200; +const uint16_t kTrumaLdrSpace = 1000; +const uint16_t kTrumaHdrMark = 1800; +const uint16_t kTrumaSpace = 630; +const uint16_t kTrumaOneMark = 600; +const uint16_t kTrumaZeroMark = 1200; +const uint16_t kTrumaFooterMark = kTrumaOneMark; +const uint32_t kTrumaGap = kDefaultMessageGap; // Just a guess. + + +#if SEND_TRUMA +/// Send a Truma formatted message. +/// Status: STABLE / Confirmed working. +/// @param[in] data The message to be sent. +/// @param[in] nbits The bit size of the message being sent. +/// @param[in] repeat The number of times the message is to be repeated. +void IRsend::sendTruma(const uint64_t data, const uint16_t nbits, + const uint16_t repeat) { + for (uint16_t r = 0; r <= repeat; r++) { + enableIROut(38000); + mark(kTrumaLdrMark); + space(kTrumaLdrSpace); + sendGeneric(kTrumaHdrMark, kTrumaSpace, // Header + kTrumaOneMark, kTrumaSpace, // Data + kTrumaZeroMark, kTrumaSpace, + kTrumaFooterMark, kTrumaGap, // Footer + data, nbits, 38, false, 0, kDutyDefault); + } +} +#endif // SEND_TRUMA + +#if DECODE_TRUMA +/// Decode the supplied Truma message. +/// Status: STABLE / Confirmed working with real device. +/// @param[in,out] results Ptr to the data to decode & where to store the decode +/// result. +/// @param[in] offset The starting index to use when attempting to decode the +/// raw data. Typically/Defaults to kStartOffset. +/// @param[in] nbits The number of data bits to expect. Typically kTrumaBits. +/// @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::decodeTruma(decode_results *results, uint16_t offset, + const uint16_t nbits, const bool strict) { + if (results->rawlen < 2 * nbits + kHeader - 1 + offset) + return false; // Can't possibly be a valid message. + if (strict && nbits != kTrumaBits) + return false; // Not strictly a message. + + // Leader. + if (!matchMark(results->rawbuf[offset++], kTrumaLdrMark)) return false; + if (!matchSpace(results->rawbuf[offset++], kTrumaLdrSpace)) return false; + + uint64_t data = 0; + uint16_t used; + used = matchGeneric(results->rawbuf + offset, &data, + results->rawlen - offset, nbits, + kTrumaHdrMark, kTrumaSpace, + kTrumaOneMark, kTrumaSpace, + kTrumaZeroMark, kTrumaSpace, + kTrumaFooterMark, kTrumaGap, + true, kUseDefTol, kMarkExcess, false); + if (!used) return false; + + // Compliance + if (strict && !IRTrumaAc::validChecksum(data)) return false; // Checksum. + + // Success + results->value = data; + results->decode_type = decode_type_t::TRUMA; + results->bits = nbits; + results->address = 0; + results->command = 0; + return true; +} +#endif // DECODE_TRUMA + + +/// 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? +IRTrumaAc::IRTrumaAc(const uint16_t pin, const bool inverted, + const bool use_modulation) + : _irsend(pin, inverted, use_modulation) { stateReset(); } + +/// Set up hardware to be able to send a message. +void IRTrumaAc::begin(void) { _irsend.begin(); } + +#if SEND_TRUMA +/// Send the current internal state as an IR message. +/// @param[in] repeat Nr. of times the message will be repeated. +void IRTrumaAc::send(const uint16_t repeat) { + _irsend.sendTruma(getRaw(), kTrumaBits, repeat); +} +#endif // SEND_TRUMA + +/// Calculate the checksum for a given state. +/// @param[in] state The value to calc the checksum of. +/// @return The calculated checksum value. +uint8_t IRTrumaAc::calcChecksum(const uint64_t state) { + uint8_t sum = kTrumaChecksumInit; + uint64_t to_checksum = state; + for (uint16_t i = 8; i < kTrumaBits; i += 8) { + sum += (to_checksum & 0xFF); + to_checksum >>= 8; + } + return sum; +} + +/// Verify the checksum is valid for a given state. +/// @param[in] state The value to verify the checksum of. +/// @return true, if the state has a valid checksum. Otherwise, false. +bool IRTrumaAc::validChecksum(const uint64_t state) { + TrumaProtocol state_copy; + state_copy.raw = state; + return state_copy.Sum == calcChecksum(state); +} + +/// Calculate & set the checksum for the current internal state of the remote. +void IRTrumaAc::checksum(void) { _.Sum = calcChecksum(_.raw); } + +/// Reset the state of the remote to a known good state/sequence. +void IRTrumaAc::stateReset(void) { setRaw(kTrumaDefaultState); } + +/// Get a copy of the internal state/code for this protocol. +/// @return The code for this protocol based on the current internal state. +uint64_t IRTrumaAc::getRaw(void) { + checksum(); + return _.raw; +} + +/// Set the internal state from a valid code for this protocol. +/// @param[in] state A valid code for this protocol. +void IRTrumaAc::setRaw(const uint64_t state) { + _.raw = state; + _lastfan = _.Fan; + _lastmode = _.Mode; +} + +/// Set the requested power state of the A/C to on. +void IRTrumaAc::on(void) { setPower(true); } + +/// Set the requested power state of the A/C to off. +void IRTrumaAc::off(void) { setPower(false); } + +/// Change the power setting. +/// @param[in] on true, the setting is on. false, the setting is off. +void IRTrumaAc::setPower(const bool on) { + _.PowerOff = !on; + _.Mode = on ? _lastmode : kTrumaFan; // Off temporarily sets mode to Fan. +} + +/// Get the value of the current power setting. +/// @return true, the setting is on. false, the setting is off. +bool IRTrumaAc::getPower(void) const { return !_.PowerOff; } + +/// Set the speed of the fan. +/// @param[in] speed The desired setting. +void IRTrumaAc::setFan(const uint8_t speed) { + switch (speed) { + case kTrumaFanHigh: + case kTrumaFanMed: + case kTrumaFanLow: + _lastfan = speed; // Never allow _lastfan to be Quiet. + _.Fan = speed; + break; + case kTrumaFanQuiet: + if (_.Mode == kTrumaCool) _.Fan = kTrumaFanQuiet; // Only in Cool mode. + break; + default: + setFan(kTrumaFanHigh); + } +} + +/// Get the current fan speed setting. +/// @return The current fan speed/mode. +uint8_t IRTrumaAc::getFan(void) const { return _.Fan; } + +/// Set the operating mode of the A/C. +/// @param[in] mode The desired operating mode. +void IRTrumaAc::setMode(const uint8_t mode) { + switch (mode) { + case kTrumaAuto: + case kTrumaFan: + if (getQuiet()) setFan(kTrumaFanHigh); // Can only have quiet in Cool. + // FALL THRU + case kTrumaCool: + _.Mode = _.PowerOff ? kTrumaFan : mode; // When Off, only set Fan mode. + _lastmode = mode; + break; + default: + setMode(kTrumaAuto); + } +} + +/// Get the operating mode setting of the A/C. +/// @return The current operating mode setting. +uint8_t IRTrumaAc::getMode(void) const { return _.Mode; } + +/// Set the temperature. +/// @param[in] celsius The temperature in degrees celsius. +void IRTrumaAc::setTemp(const uint8_t celsius) { + uint8_t temp = std::max(celsius, kTrumaMinTemp); + temp = std::min(temp, kTrumaMaxTemp); + _.Temp = temp - kTrumaTempOffset; +} + +/// Get the current temperature setting. +/// @return The current setting for temp. in degrees celsius. +uint8_t IRTrumaAc::getTemp(void) const { return _.Temp + kTrumaTempOffset; } + +/// Change the Quiet setting. +/// @param[in] on true, the setting is on. false, the setting is off. +/// @note Quiet is only available in Cool mode. +void IRTrumaAc::setQuiet(const bool on) { + if (on && _.Mode == kTrumaCool) + setFan(kTrumaFanQuiet); + else + setFan(_lastfan); +} + +/// Get the value of the current quiet setting. +/// @return true, the setting is on. false, the setting is off. +bool IRTrumaAc::getQuiet(void) const { return _.Fan == kTrumaFanQuiet; } + +/// 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 IRTrumaAc::convertMode(const stdAc::opmode_t mode) { + switch (mode) { + case stdAc::opmode_t::kCool: return kTrumaCool; + case stdAc::opmode_t::kFan: return kTrumaFan; + default: return kTrumaAuto; + } +} + +/// 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 IRTrumaAc::convertFan(const stdAc::fanspeed_t speed) { + switch (speed) { + case stdAc::fanspeed_t::kMin: return kTrumaFanQuiet; + case stdAc::fanspeed_t::kLow: return kTrumaFanLow; + case stdAc::fanspeed_t::kMedium: return kTrumaFanMed; + default: return kTrumaFanHigh; + } +} + +/// 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 IRTrumaAc::toCommonMode(const uint8_t mode) { + switch (mode) { + case kTrumaCool: return stdAc::opmode_t::kCool; + case kTrumaFan: return stdAc::opmode_t::kFan; + default: return stdAc::opmode_t::kAuto; + } +} + +/// Convert a native fan speed into its stdAc equivalent. +/// @param[in] spd The native setting to be converted. +/// @return The stdAc equivalent of the native setting. +stdAc::fanspeed_t IRTrumaAc::toCommonFanSpeed(const uint8_t spd) { + switch (spd) { + case kTrumaFanMed: return stdAc::fanspeed_t::kMedium; + case kTrumaFanLow: return stdAc::fanspeed_t::kLow; + case kTrumaFanQuiet: return stdAc::fanspeed_t::kMin; + default: return stdAc::fanspeed_t::kHigh; + } +} + +/// Convert the current internal state into its stdAc::state_t equivalent. +/// @return The stdAc equivalent of the native settings. +stdAc::state_t IRTrumaAc::toCommon(void) const { + stdAc::state_t result; + + result.protocol = decode_type_t::TRUMA; + result.model = -1; // Not supported. + // Do we have enough current state info to override any previous state? + // i.e. Was the class just setRaw()'ed with a short "swing" message. + // This should enables us to also ignore the Swing msg's special 17C setting. + result.power = getPower(); + result.mode = toCommonMode(getMode()); + result.celsius = true; + result.degrees = getTemp(); + result.fanspeed = toCommonFanSpeed(getFan()); + result.quiet = getQuiet(); + // Not supported. + result.turbo = false; + result.econo = false; + result.light = false; + result.filter = false; + result.swingv = stdAc::swingv_t::kOff; + result.swingh = stdAc::swingh_t::kOff; + result.clean = false; + result.beep = false; + result.sleep = -1; + result.clock = -1; + return result; +} + +/// Convert the current internal state into a human readable string. +/// @return A human readable string. +String IRTrumaAc::toString(void) const { + String result = ""; + result.reserve(80); + result += addBoolToString(getPower(), kPowerStr, false); + if (getPower()) // Only show the Operating Mode if the unit is on. + result += addModeToString(_.Mode, kTrumaAuto, kTrumaCool, + kTrumaAuto, kTrumaAuto, kTrumaFan); + + result += addTempToString(getTemp()); + result += addFanToString(_.Fan, kTrumaFanHigh, kTrumaFanLow, kTrumaFanHigh, + kTrumaFanQuiet, kTrumaFanMed); + result += addBoolToString(getQuiet(), kQuietStr); + return result; +} diff --git a/src/ir_Truma.h b/src/ir_Truma.h new file mode 100644 index 000000000..9946a0322 --- /dev/null +++ b/src/ir_Truma.h @@ -0,0 +1,126 @@ +// Copyright 2021 David Conran (crankyoldgit) + +/// @file +/// @brief Support for Truma protocol. +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1440 +/// @see https://docs.google.com/spreadsheets/d/1k-RHu0vSIB6IweiTZSa3Rxy3Z_qPUtqwcqot8uXVO6I/edit?usp=sharing + +// Supports: +// Brand: Truma, Model: Aventa A/C +// Brand: Truma, Model: 40091-86700 remote + +#ifndef IR_TRUMA_H_ +#define IR_TRUMA_H_ + +#ifndef UNIT_TEST +#include +#endif +#include "IRremoteESP8266.h" +#include "IRsend.h" +#ifdef UNIT_TEST +#include "IRsend_test.h" +#endif + +/// Native representation of a Truma A/C message. +union TrumaProtocol{ + uint64_t raw; ///< Remote state in IR code form. + struct { + // Byte 0 (least significant byte) + uint8_t :8; // fixed value (0x81) + // Byte 1 + uint8_t Mode :2; + uint8_t PowerOff :1; + uint8_t Fan :3; + uint8_t :2; // fixed value (0b11) + // Byte 2 + uint8_t Temp:5; ///< Temp in DegC minus 10(DEC). + uint8_t :3; + // Byte 3 + uint8_t :8; // fixed value (0xFF) + // Byte 4 + uint8_t :8; // fixed value (0xFF) + // Byte 5 + uint8_t :8; // fixed value (0xFF) + // Byte 6 + uint8_t Sum:8; ///< Checksum value + }; +}; + +// Constants +const uint64_t kTrumaDefaultState = 0x50FFFFFFE6E781; ///< Off, Auto, 16C, High +const uint8_t kTrumaChecksumInit = 5; + +const uint8_t kTrumaAuto = 0; // 0b00 +const uint8_t kTrumaCool = 2; // 0b10 +const uint8_t kTrumaFan = 3; // 0b11 + +const uint8_t kTrumaFanQuiet = 3; // 0b011 +const uint8_t kTrumaFanHigh = 4; // 0b100 +const uint8_t kTrumaFanMed = 5; // 0b101 +const uint8_t kTrumaFanLow = 6; // 0b110 + +const uint8_t kTrumaTempOffset = 10; +const uint8_t kTrumaMinTemp = 16; +const uint8_t kTrumaMaxTemp = 31; + + +// Class +/// Class for handling detailed Truma A/C messages. +class IRTrumaAc { + public: + explicit IRTrumaAc(const uint16_t pin, const bool inverted = false, + const bool use_modulation = true); +#if SEND_TRUMA + void send(const uint16_t repeat = kNoRepeat); + /// Run the calibration to calculate uSec timing offsets for this platform. + /// @return The uSec timing offset needed per modulation of the IR Led. + /// @note This will produce a 65ms IR signal pulse at 38kHz. + /// Only ever needs to be run once per object instantiation, if at all. + int8_t calibrate(void) { return _irsend.calibrate(); } +#endif // SEND_TRUMA + void begin(void); + void stateReset(void); + + void on(void); + void off(void); + void setPower(const bool on); + bool getPower(void) const; + + void setTemp(const uint8_t celsius); + uint8_t getTemp(void) const; + + void setFan(const uint8_t speed); + uint8_t getFan(void) const; + + uint8_t getMode(void) const; + void setMode(const uint8_t mode); + + void setQuiet(const bool on); + bool getQuiet(void) const; + + uint64_t getRaw(void); + void setRaw(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(void) 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 + TrumaProtocol _; + uint8_t _lastfan; // Last user chosen/valid fan speed. + uint8_t _lastmode; // Last user chosen operation mode. + static uint8_t calcChecksum(const uint64_t state); + void checksum(void); +}; + +#endif // IR_TRUMA_H_ diff --git a/src/locale/defaults.h b/src/locale/defaults.h index 5b5002b4c..476daeb3a 100644 --- a/src/locale/defaults.h +++ b/src/locale/defaults.h @@ -766,6 +766,9 @@ #ifndef D_STR_TROTEC #define D_STR_TROTEC "TROTEC" #endif // D_STR_TROTEC +#ifndef D_STR_TRUMA +#define D_STR_TRUMA "TRUMA" +#endif // D_STR_TRUMA #ifndef D_STR_UNUSED #define D_STR_UNUSED "UNUSED" #endif // D_STR_UNUSED diff --git a/test/IRac_test.cpp b/test/IRac_test.cpp index 3b7ac838a..0874802a3 100644 --- a/test/IRac_test.cpp +++ b/test/IRac_test.cpp @@ -29,6 +29,7 @@ #include "ir_Teco.h" #include "ir_Toshiba.h" #include "ir_Trotec.h" +#include "ir_Truma.h" #include "ir_Vestel.h" #include "ir_Voltas.h" #include "ir_Whirlpool.h" @@ -1603,6 +1604,35 @@ TEST(TestIRac, Trotec) { ASSERT_TRUE(IRAcUtils::decodeToState(&ac._irsend.capture, &r, &p)); } +TEST(TestIRac, Truma) { + IRTrumaAc ac(kGpioUnused); + IRac irac(kGpioUnused); + IRrecv capture(kGpioUnused); + char expected[] = + "Power: On, Mode: 2 (Cool), Temp: 22C, Fan: 3 (Quiet), Quiet: On"; + + ac.begin(); + irac.truma(&ac, + true, // Power + stdAc::opmode_t::kCool, // Mode + 22, // Celsius + stdAc::fanspeed_t::kHigh, // Fan speed + true); // Quiet (will override fan speed) + EXPECT_TRUE(ac.getPower()); + EXPECT_EQ(kTrumaCool, ac.getMode()); + EXPECT_EQ(22, ac.getTemp()); + EXPECT_EQ(kTrumaFanQuiet, ac.getFan()); + EXPECT_TRUE(ac.getQuiet()); + ASSERT_EQ(expected, ac.toString()); + ac._irsend.makeDecodeResult(); + EXPECT_TRUE(capture.decode(&ac._irsend.capture)); + ASSERT_EQ(TRUMA, ac._irsend.capture.decode_type); + ASSERT_EQ(kTrumaBits, ac._irsend.capture.bits); + ASSERT_EQ(expected, IRAcUtils::resultAcToString(&ac._irsend.capture)); + stdAc::state_t r, p; + ASSERT_TRUE(IRAcUtils::decodeToState(&ac._irsend.capture, &r, &p)); +} + TEST(TestIRac, Vestel) { IRVestelAc ac(kGpioUnused); IRac irac(kGpioUnused); diff --git a/test/ir_Truma_test.cpp b/test/ir_Truma_test.cpp new file mode 100644 index 000000000..0d26fb6db --- /dev/null +++ b/test/ir_Truma_test.cpp @@ -0,0 +1,287 @@ +// Copyright 2021 David Conran (crankyoldgit) + +#include "IRac.h" +#include "IRrecv.h" +#include "IRrecv_test.h" +#include "IRsend.h" +#include "IRsend_test.h" +#include "gtest/gtest.h" + +// Tests for decodeTruma(). + +TEST(TestDecodeTruma, RealExample) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + // 16 / AUTO / - + const uint16_t rawData[117] = { + 20194, 1028, 1798, 628, 558, 662, 1190, 640, + 1190, 638, 1190, 634, 1190, 638, 1188, 640, + 1164, 662, 558, 658, 1192, 636, 1166, 662, + 1190, 638, 1164, 658, 1190, 638, 582, 640, + 556, 662, 558, 660, 1188, 638, 558, 662, + 558, 662, 1190, 634, 1188, 638, 556, 664, + 558, 662, 556, 660, 556, 664, 558, 662, + 556, 664, 556, 662, 556, 664, 558, 662, + 558, 662, 558, 660, 556, 664, 556, 664, + 556, 664, 556, 660, 558, 664, 578, 640, + 580, 640, 556, 660, 556, 664, 556, 664, + 556, 664, 580, 636, 556, 664, 556, 664, + 556, 664, 556, 660, 556, 664, 1188, 638, + 1188, 640, 554, 662, 1188, 640, 1188, 638, + 554, 666, 1164, 654, 582}; + irsend.begin(); + irsend.reset(); + irsend.sendRaw(rawData, 117, 38); + irsend.makeDecodeResult(); + + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + ASSERT_EQ(decode_type_t::TRUMA, irsend.capture.decode_type); + ASSERT_EQ(kTrumaBits, irsend.capture.bits); + EXPECT_EQ(0x49ffffffe6e081, irsend.capture.value); + EXPECT_EQ(0x0, irsend.capture.address); + EXPECT_EQ(0x0, irsend.capture.command); + EXPECT_EQ( + "Power: On, Mode: 0 (Auto), Temp: 16C, Fan: 4 (High), Quiet: Off", + IRAcUtils::resultAcToString(&irsend.capture)); + stdAc::state_t r, p; + ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &r, &p)); +} + +TEST(TestDecodeTruma, SyntheticExample) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + irsend.begin(); + irsend.reset(); + + // 16 / AUTO / - + irsend.sendTruma(0x49ffffffe6e081); + irsend.makeDecodeResult(); + + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(decode_type_t::TRUMA, irsend.capture.decode_type); + EXPECT_EQ(kTrumaBits, irsend.capture.bits); + EXPECT_EQ(0x49ffffffe6e081, irsend.capture.value); + EXPECT_EQ(0x0, irsend.capture.address); + EXPECT_EQ(0x0, irsend.capture.command); + + EXPECT_EQ( + "f38000d50" + "m20200s1000" + "m1800s630" + "m600s630m1200s630m1200s630m1200s630m1200s630m1200s630m1200s630m600s630" + "m1200s630m1200s630m1200s630m1200s630m1200s630m600s630m600s630m600s630" + "m1200s630m600s630m600s630m1200s630m1200s630m600s630m600s630m600s630" + "m600s630m600s630m600s630m600s630m600s630m600s630m600s630m600s630" + "m600s630m600s630m600s630m600s630m600s630m600s630m600s630m600s630" + "m600s630m600s630m600s630m600s630m600s630m600s630m600s630m600s630" + "m600s630m1200s630m1200s630m600s630m1200s630m1200s630m600s630m1200s630" + "m600s100000", + irsend.outputStr()); +} + +TEST(TestUtils, Housekeeping) { + ASSERT_EQ("TRUMA", typeToString(decode_type_t::TRUMA)); + ASSERT_EQ(decode_type_t::TRUMA, strToDecodeType("TRUMA")); + ASSERT_FALSE(hasACState(decode_type_t::TRUMA)); + ASSERT_TRUE(IRac::isProtocolSupported(decode_type_t::TRUMA)); + ASSERT_EQ(kTrumaBits, IRsend::defaultBits(decode_type_t::TRUMA)); + ASSERT_EQ(kNoRepeat, IRsend::minRepeats(decode_type_t::TRUMA)); +} + +// Tests for IRTrumaAc class. + +TEST(TestTrumaAcClass, Power) { + IRTrumaAc ac(kGpioUnused); + ac.begin(); + + ac.on(); + EXPECT_TRUE(ac.getPower()); + + ac.off(); + EXPECT_FALSE(ac.getPower()); + + ac.setPower(true); + EXPECT_TRUE(ac.getPower()); + + ac.setPower(false); + EXPECT_FALSE(ac.getPower()); +} + +TEST(TestTrumaAcClass, toCommon) { + IRTrumaAc ac(kGpioUnused); + ac.setPower(true); + ac.setMode(kTrumaCool); + ac.setTemp(20); + ac.setFan(kTrumaFanHigh); + ac.setQuiet(false); + // Now test it. + ASSERT_EQ(decode_type_t::TRUMA, ac.toCommon().protocol); + ASSERT_EQ(-1, ac.toCommon().model); + ASSERT_TRUE(ac.toCommon().power); + ASSERT_TRUE(ac.toCommon().celsius); + ASSERT_EQ(20, ac.toCommon().degrees); + ASSERT_EQ(stdAc::opmode_t::kCool, ac.toCommon().mode); + ASSERT_EQ(stdAc::fanspeed_t::kHigh, ac.toCommon().fanspeed); + ASSERT_FALSE(ac.toCommon().quiet); + // Unsupported. + ASSERT_EQ(stdAc::swingv_t::kOff, ac.toCommon().swingv); + ASSERT_EQ(stdAc::swingh_t::kOff, ac.toCommon().swingh); + ASSERT_FALSE(ac.toCommon().turbo); + ASSERT_FALSE(ac.toCommon().econo); + ASSERT_FALSE(ac.toCommon().light); + ASSERT_FALSE(ac.toCommon().filter); + ASSERT_FALSE(ac.toCommon().clean); + ASSERT_FALSE(ac.toCommon().beep); + ASSERT_EQ(-1, ac.toCommon().sleep); + ASSERT_EQ(-1, ac.toCommon().clock); +} + +TEST(TestTrumaAcClass, HumanReadableOutput) { + IRTrumaAc ac(kGpioUnused); + ac.begin(); + + // Data from https://docs.google.com/spreadsheets/d/1k-RHu0vSIB6IweiTZSa3Rxy3Z_qPUtqwcqot8uXVO6I/edit#gid=0 + const uint64_t on_25_auto = 0x52FFFFFFEFE081; + const uint64_t on_17_cool_med = 0x54FFFFFFE7EA81; + + ac.setRaw(on_25_auto); + EXPECT_EQ("Power: On, Mode: 0 (Auto), Temp: 25C, Fan: 4 (High), Quiet: Off", + ac.toString()); + ac.setRaw(on_17_cool_med); + EXPECT_EQ("Power: On, Mode: 2 (Cool), Temp: 17C, Fan: 5 (Medium), Quiet: Off", + ac.toString()); + ac.setTemp(25); + ac.setFan(kTrumaFanLow); + ac.setMode(kTrumaFan); + EXPECT_EQ("Power: On, Mode: 3 (Fan), Temp: 25C, Fan: 6 (Low), Quiet: Off", + ac.toString()); + ac.off(); + EXPECT_EQ("Power: Off, Temp: 25C, Fan: 6 (Low), Quiet: Off", + ac.toString()); +} + +TEST(TestTrumaAcClass, FanSpeed) { + IRTrumaAc ac(kGpioUnused); + ac.begin(); + ac.on(); + ac.setMode(kTrumaCool); // Cool allows all options. + ac.setFan(kTrumaFanLow); + EXPECT_EQ(kTrumaFanLow, ac.getFan()); + + ac.setFan(0); + EXPECT_EQ(kTrumaFanHigh, ac.getFan()); + ac.setFan(255); + EXPECT_EQ(kTrumaFanHigh, ac.getFan()); + + ac.setFan(kTrumaFanMed); + EXPECT_EQ(kTrumaFanMed, ac.getFan()); + + ac.setFan(kTrumaFanHigh); + EXPECT_EQ(kTrumaFanHigh, ac.getFan()); + + ac.setFan(kTrumaFanQuiet); + EXPECT_EQ(kTrumaFanQuiet, ac.getFan()); + + ac.setMode(kTrumaAuto); // Quiet should not be available/an option now. + EXPECT_NE(kTrumaFanQuiet, ac.getFan()); + ac.setFan(kTrumaFanQuiet); + EXPECT_NE(kTrumaFanQuiet, ac.getFan()); +} + +TEST(TestTrumaAcClass, OperatingMode) { + IRTrumaAc ac(kGpioUnused); + ac.begin(); + + ac.on(); // Power on state allows all modes. + ac.setMode(kTrumaAuto); + EXPECT_EQ(kTrumaAuto, ac.getMode()); + + ac.setMode(kTrumaCool); + EXPECT_EQ(kTrumaCool, ac.getMode()); + + ac.setMode(kTrumaFan); + EXPECT_EQ(kTrumaFan, ac.getMode()); + + ac.setMode(255); + EXPECT_EQ(kTrumaAuto, ac.getMode()); + + ac.off(); // Off, only allows the fan mode. + EXPECT_EQ(kTrumaFan, ac.getMode()); + ac.setMode(kTrumaCool); + EXPECT_EQ(kTrumaFan, ac.getMode()); +} + +TEST(TestTrumaAcClass, Temperature) { + IRTrumaAc ac(kGpioUnused); + ac.begin(); + + ac.setTemp(0); + EXPECT_EQ(kTrumaMinTemp, ac.getTemp()); + + ac.setTemp(255); + EXPECT_EQ(kTrumaMaxTemp, ac.getTemp()); + + ac.setTemp(kTrumaMinTemp); + EXPECT_EQ(kTrumaMinTemp, ac.getTemp()); + + ac.setTemp(kTrumaMaxTemp); + EXPECT_EQ(kTrumaMaxTemp, ac.getTemp()); + + ac.setTemp(kTrumaMinTemp - 1); + EXPECT_EQ(kTrumaMinTemp, ac.getTemp()); + + ac.setTemp(kTrumaMaxTemp + 1); + EXPECT_EQ(kTrumaMaxTemp, ac.getTemp()); + + ac.setTemp(17); + EXPECT_EQ(17, ac.getTemp()); + + ac.setTemp(21); + EXPECT_EQ(21, ac.getTemp()); + + ac.setTemp(25); + EXPECT_EQ(25, ac.getTemp()); + + ac.setTemp(30); + EXPECT_EQ(30, ac.getTemp()); +} + +TEST(TestTrumaAcClass, Checksums) { + IRTrumaAc ac(kGpioUnused); + ac.begin(); + + uint64_t valid = 0x52FFFFFFEFE081; + uint64_t invalid = 0x51FFFFFFEFE081; + + EXPECT_EQ(0x52, ac.calcChecksum(valid)); + EXPECT_EQ(0x52, ac.calcChecksum(invalid)); + // Check we can call it without instantiating the object. + EXPECT_EQ(0x52, IRTrumaAc::calcChecksum(valid)); + + EXPECT_TRUE(IRTrumaAc::validChecksum(valid)); + EXPECT_FALSE(IRTrumaAc::validChecksum(invalid)); + + ac.setRaw(invalid); + EXPECT_EQ(valid, ac.getRaw()); +} + +TEST(TestTrumaAcClass, KnownMessageConstuction) { + IRTrumaAc ac(kGpioUnused); + ac.begin(); + + const uint64_t on_17_cool_med = 0x54FFFFFFE7EA81; + ac.on(); + ac.setMode(kTrumaCool); + ac.setFan(kTrumaFanMed); + ac.setTemp(17); + ac.setQuiet(false); + EXPECT_EQ(on_17_cool_med, ac.getRaw()); + + const uint64_t off_16_auto = 0x50FFFFFFE6E781; // 4DFFFFFFE6E481 + ac.setMode(kTrumaAuto); + ac.setTemp(16); + ac.setFan(kTrumaFanHigh); + ac.off(); + EXPECT_EQ(off_16_auto, ac.getRaw()); + EXPECT_EQ("Power: Off, Temp: 16C, Fan: 4 (High), Quiet: Off", ac.toString()); +}