diff --git a/Doxyfile b/Doxyfile index 8714b0476..1c1617947 100644 --- a/Doxyfile +++ b/Doxyfile @@ -10,7 +10,7 @@ EXTRACT_ALL = YES EXTRACT_PRIVATE = YES EXTRACT_LOCAL_CLASSES = NO GENERATE_LATEX = NO -ENABLE_PREPROCESSING = NO +ENABLE_PREPROCESSING = YES QUIET = YES WARN_NO_PARAMDOC = YES WARN_AS_ERROR = YES diff --git a/examples/IRMQTTServer/IRMQTTServer.h b/examples/IRMQTTServer/IRMQTTServer.h index 716aab60b..e69b891e4 100644 --- a/examples/IRMQTTServer/IRMQTTServer.h +++ b/examples/IRMQTTServer/IRMQTTServer.h @@ -253,6 +253,9 @@ const uint16_t kMinUnknownSize = 2 * 10; #define KEY_JSON "json" #define KEY_RESEND "resend" #define KEY_VCC "vcc" +#define KEY_COMMAND "command" +#define KEY_ROOMTEMP "roomtemp" +#define KEY_IFEEL "ifeel" // HTML arguments we will parse for IR code information. #define KEY_TYPE "type" // KEY_PROTOCOL is also checked too. @@ -358,7 +361,8 @@ static const char kClimateTopics[] PROGMEM = "(" KEY_PROTOCOL "|" KEY_MODEL "|" KEY_POWER "|" KEY_MODE "|" KEY_TEMP "|" KEY_FANSPEED "|" KEY_SWINGV "|" KEY_SWINGH "|" KEY_QUIET "|" KEY_TURBO "|" KEY_LIGHT "|" KEY_BEEP "|" KEY_ECONO "|" KEY_SLEEP "|" - KEY_FILTER "|" KEY_CLEAN "|" KEY_CELSIUS "|" KEY_RESEND + KEY_FILTER "|" KEY_CLEAN "|" KEY_CELSIUS "|" KEY_RESEND "|" KEY_COMMAND "|" + "|" KEY_ROOMTEMP "|" KEY_IFEEL #if MQTT_CLIMATE_JSON "|" KEY_JSON #endif // MQTT_CLIMATE_JSON @@ -367,6 +371,7 @@ static const char* const kMqttTopics[] = { KEY_PROTOCOL, KEY_MODEL, KEY_POWER, KEY_MODE, KEY_TEMP, KEY_FANSPEED, KEY_SWINGV, KEY_SWINGH, KEY_QUIET, KEY_TURBO, KEY_LIGHT, KEY_BEEP, KEY_ECONO, KEY_SLEEP, KEY_FILTER, KEY_CLEAN, KEY_CELSIUS, KEY_RESEND, + KEY_COMMAND, KEY_ROOMTEMP, KEY_IFEEL KEY_JSON}; // KEY_JSON needs to be the last one. diff --git a/examples/IRMQTTServer/IRMQTTServer.ino b/examples/IRMQTTServer/IRMQTTServer.ino index 6701ef4bf..bf9b31596 100644 --- a/examples/IRMQTTServer/IRMQTTServer.ino +++ b/examples/IRMQTTServer/IRMQTTServer.ino @@ -984,6 +984,18 @@ String htmlSelectModel(const String name, const int16_t def) { return html; } +String htmlSelectCommandType(const String name, const stdAc::ac_command_t def) { + String html = F(""); + return html; +} + String htmlSelectUint(const String name, const uint16_t max, const uint16_t def) { String html = F("" + "" D_STR_SENSORTEMP "" + "" "" D_STR_FAN "") + htmlSelectFanspeed(KEY_FANSPEED, climate[chan]->next.fanspeed) + F("" @@ -1138,6 +1158,9 @@ void handleAirCon(void) { "" D_STR_QUIET "") + htmlSelectBool(KEY_QUIET, climate[chan]->next.quiet) + F("" + "" D_STR_IFEEL "") + + htmlSelectBool(KEY_IFEEL, climate[chan]->next.iFeel) + + F("" "" D_STR_TURBO "") + htmlSelectBool(KEY_TURBO, climate[chan]->next.turbo) + F("" @@ -2976,6 +2999,7 @@ void sendJsonState(const stdAc::state_t state, const String topic, DynamicJsonDocument json(kJsonAcStateMaxSize); json[KEY_PROTOCOL] = typeToString(state.protocol); json[KEY_MODEL] = state.model; + json[KEY_COMMAND] = IRac::commandToString(state.command); json[KEY_POWER] = IRac::boolToString(state.power); json[KEY_MODE] = IRac::opmodeToString(state.mode, ha_mode); // Home Assistant wants mode to be off if power is also off & vice-versa. @@ -2985,10 +3009,12 @@ void sendJsonState(const stdAc::state_t state, const String topic, } json[KEY_CELSIUS] = IRac::boolToString(state.celsius); json[KEY_TEMP] = state.degrees; + json[KEY_ROOMTEMP] = state.roomTemp; json[KEY_FANSPEED] = IRac::fanspeedToString(state.fanspeed); json[KEY_SWINGV] = IRac::swingvToString(state.swingv); json[KEY_SWINGH] = IRac::swinghToString(state.swingh); json[KEY_QUIET] = IRac::boolToString(state.quiet); + json[KEY_IFEEL] = IRac::boolToString(state.iFeel); json[KEY_TURBO] = IRac::boolToString(state.turbo); json[KEY_ECONO] = IRac::boolToString(state.econo); json[KEY_LIGHT] = IRac::boolToString(state.light); @@ -3026,6 +3052,10 @@ stdAc::state_t jsonToState(const stdAc::state_t current, const char *str) { result.model = IRac::strToModel(json[KEY_MODEL].as()); else if (validJsonInt(json, KEY_MODEL)) result.model = json[KEY_MODEL]; + if (validJsonStr(json, KEY_COMMAND)) + result.command = IRac::strToCommand(json[KEY_COMMAND].as()); + else if (validJsonInt(json, KEY_COMMAND)) + result.command = json[KEY_COMMAND]; if (validJsonStr(json, KEY_MODE)) result.mode = IRac::strToOpmode(json[KEY_MODE]); if (validJsonStr(json, KEY_FANSPEED)) @@ -3036,10 +3066,14 @@ stdAc::state_t jsonToState(const stdAc::state_t current, const char *str) { result.swingh = IRac::strToSwingH(json[KEY_SWINGH]); if (json.containsKey(KEY_TEMP)) result.degrees = json[KEY_TEMP]; + if (json.containsKey(KEY_ROOMTEMP)) + result.roomTemp = json[KEY_ROOMTEMP]; if (validJsonInt(json, KEY_SLEEP)) result.sleep = json[KEY_SLEEP]; if (validJsonStr(json, KEY_POWER)) result.power = IRac::strToBool(json[KEY_POWER]); + if (validJsonStr(json, KEY_IFEEL)) + result.iFeel = IRac::strToBool(json[KEY_IFEEL]); if (validJsonStr(json, KEY_QUIET)) result.quiet = IRac::strToBool(json[KEY_QUIET]); if (validJsonStr(json, KEY_TURBO)) @@ -3071,6 +3105,8 @@ void updateClimate(stdAc::state_t *state, const String str, state->protocol = strToDecodeType(payload.c_str()); } else if (str.equals(prefix + F(KEY_MODEL))) { state->model = IRac::strToModel(payload.c_str()); + } else if (str.equals(prefix + F(KEY_COMMAND))) { + state->command = IRac::strToCommandType(payload.c_str()); } else if (str.equals(prefix + F(KEY_POWER))) { state->power = IRac::strToBool(payload.c_str()); #if MQTT_CLIMATE_HA_MODE @@ -3085,12 +3121,16 @@ void updateClimate(stdAc::state_t *state, const String str, #endif // MQTT_CLIMATE_HA_MODE } else if (str.equals(prefix + F(KEY_TEMP))) { state->degrees = payload.toFloat(); + } else if (str.equals(prefix + F(KEY_ROOMTEMP))) { + state->roomTemperature = payload.toFloat(); } else if (str.equals(prefix + F(KEY_FANSPEED))) { state->fanspeed = IRac::strToFanspeed(payload.c_str()); } else if (str.equals(prefix + F(KEY_SWINGV))) { state->swingv = IRac::strToSwingV(payload.c_str()); } else if (str.equals(prefix + F(KEY_SWINGH))) { state->swingh = IRac::strToSwingH(payload.c_str()); + } else if (str.equals(prefix + F(KEY_IFEEL))) { + state->iFeel = IRac::strToBool(payload.c_str()); } else if (str.equals(prefix + F(KEY_QUIET))) { state->quiet = IRac::strToBool(payload.c_str()); } else if (str.equals(prefix + F(KEY_TURBO))) { @@ -3128,6 +3168,11 @@ bool sendClimate(const String topic_prefix, const bool retain, diff = true; success &= sendInt(topic_prefix + KEY_MODEL, next.model, retain); } + if (prev.command != next.command || forceMQTT) { + String command_str = IRac::commandTypeToString(next.command); + diff = true; + success &= sendString(topic_prefix + KEY_COMMAND, command_str, retain); + } #ifdef MQTT_CLIMATE_HA_MODE String mode_str = IRac::opmodeToString(next.mode, MQTT_CLIMATE_HA_MODE); #else // MQTT_CLIMATE_HA_MODE @@ -3161,6 +3206,11 @@ bool sendClimate(const String topic_prefix, const bool retain, diff = true; success &= sendBool(topic_prefix + KEY_CELSIUS, next.celsius, retain); } + if (prev.roomTemperature != next.roomTemperature || forceMQTT) { + diff = true; + success &= sendFloat(topic_prefix + KEY_ROOMTEMP, next.roomTemperature, + retain); + } if (prev.fanspeed != next.fanspeed || forceMQTT) { diff = true; success &= sendString(topic_prefix + KEY_FANSPEED, @@ -3176,6 +3226,10 @@ bool sendClimate(const String topic_prefix, const bool retain, success &= sendString(topic_prefix + KEY_SWINGH, IRac::swinghToString(next.swingh), retain); } + if (prev.iFeel != next.iFeel || forceMQTT) { + diff = true; + success &= sendBool(topic_prefix + KEY_IFEEL, next.iFeel, retain); + } if (prev.quiet != next.quiet || forceMQTT) { diff = true; success &= sendBool(topic_prefix + KEY_QUIET, next.quiet, retain); diff --git a/src/IRac.cpp b/src/IRac.cpp index 97fe05916..644a97072 100644 --- a/src/IRac.cpp +++ b/src/IRac.cpp @@ -64,6 +64,34 @@ #endif // ESP8266 #endif // STRCASECMP +#ifndef UNIT_TEST +#define OUTPUT_DECODE_RESULTS_FOR_UT(ac) +#else +/// If compiling for UT *and* a test receiver @c IRrecv is provided via the +/// @c _utReceived param, this injects an "output" gadget @c _lastDecodeResults +/// into the @c IRAc::sendAc method, so that the UT code may parse the "sent" +/// value and drive further assertions +/// +/// @note The @c decode_results "returned" is a shallow copy (empty rawbuf), +/// mostly b/c the class does not have a custom/deep copy c-tor +/// and defining it would be an overkill for this purpose +/// @note For future maintainers: If @c IRAc class is ever refactored to use +/// polymorphism (static or dynamic)... this macro should be removed +/// and replaced with proper GMock injection. +#define OUTPUT_DECODE_RESULTS_FOR_UT(ac) \ + { \ + if (_utReceiver) { \ + _lastDecodeResults = nullptr; \ + (ac)._irsend.makeDecodeResult(); \ + if (_utReceiver->decode(&(ac)._irsend.capture)) { \ + _lastDecodeResults = std::unique_ptr( \ + new decode_results((ac)._irsend.capture)); \ + _lastDecodeResults->rawbuf = nullptr; \ + } \ + } \ + } +#endif // UNIT_TEST + /// Class constructor /// @param[in] pin Gpio pin to use when transmitting IR messages. /// @param[in] inverted true, gpio output defaults to high. false, to low. @@ -464,6 +492,109 @@ void IRac::argo(IRArgoAC *ac, ac->setNight(sleep >= 0); // Convert to a boolean. ac->send(); } + +/// Send an Argo A/C WREM-3 AC **control** message with the supplied settings. +/// @param[in, out] ac A Ptr to an IRArgoAC_WREM3 object to use. +/// @param[in] on The power setting. +/// @param[in] mode The operation mode setting. +/// @param[in] degrees The set temperature setting in degrees Celsius. +/// @param[in] roomTemp The room (iFeel) temperature setting in degrees Celsius. +/// @param[in] fan The speed setting for the fan. +/// @param[in] swingv The vertical swing setting. +/// @param[in] iFeel Whether to enable iFeel mode on the A/C unit. +/// @param[in] night Enable night mode (raises temp by +1*C after 1h). +/// @param[in] econo Enable eco mode (limits power consumed). +/// @param[in] turbo Run the device in turbo/powerful mode. +/// @param[in] filter Enable filter mode +/// @param[in] light Enable device display/LEDs +void IRac::argoWrem3_ACCommand(IRArgoAC_WREM3 *ac, const bool on, + const stdAc::opmode_t mode, const float degrees, const float roomTemp, + const stdAc::fanspeed_t fan, const stdAc::swingv_t swingv, const bool iFeel, + const bool night, const bool econo, const bool turbo, const bool filter, + const bool light) { + ac->begin(); + ac->setMessageType(argoIrMessageType_t::AC_CONTROL); + ac->setPower(on); + ac->setMode(ac->convertMode(mode)); + ac->setTemp(degrees); + ac->setRoomTemp(roomTemp); + ac->setFan(ac->convertFan(fan)); + ac->setFlap(ac->convertSwingV(swingv)); + ac->setiFeel(iFeel); + ac->setNight(night); + ac->setEco(econo); + ac->setMax(turbo); + ac->setFilter(filter); + ac->setLight(light); + // No Clean setting available. + // No Beep setting available - always beeps in this mode :) + ac->send(); +} + +/// Send an Argo A/C WREM-3 iFeel (room temp) silent (no beep) report. +/// @param[in, out] ac A Ptr to an IRArgoAC_WREM3 object to use. +/// @param[in] roomTemp The room (iFeel) temperature setting in degrees Celsius. +void IRac::argoWrem3_iFeelReport(IRArgoAC_WREM3 *ac, const float roomTemp) { + ac->begin(); + ac->setMessageType(argoIrMessageType_t::IFEEL_TEMP_REPORT); + ac->setRoomTemp(roomTemp); + ac->send(); +} + +/// Send an Argo A/C WREM-3 Config command. +/// @param[in, out] ac A Ptr to an IRArgoAC_WREM3 object to use. +/// @param[in] param The parameter ID. +/// @param[in] value The parameter value. +/// @param[in] safe If true, will only allow setting the below parameters +/// in order to avoid accidentally setting a restricted +/// vendor-specific param and breaking the A/C device +/// @note Known parameters (P, where xx is the @c param) +/// P05 - Temperature Scale (0-Celsius, 1-Fahrenheit) +/// P06 - Transmission channel (0..3) +/// P12 - ECO mode power input limit (30..99, default: 75) +void IRac::argoWrem3_ConfigSet(IRArgoAC_WREM3 *ac, const uint8_t param, + const uint8_t value, bool safe /*= true*/) { + if (safe) { + switch (param) { + case 5: // temp. scale (note this is likely excess as not transmitted) + if (value > 1) { return; /* invalid */ } + break; + case 6: // channel (note this is likely excess as not transmitted) + if (value > 3) { return; /* invalid */ } + break; + case 12: // eco power limit + if (value < 30 || value > 99) { return; /* invalid */ } + break; + default: + return; /* invalid */ + } + } + ac->begin(); + ac->setMessageType(argoIrMessageType_t::CONFIG_PARAM_SET); + ac->setConfigEntry(param, value); + ac->send(); +} + +/// Send an Argo A/C WREM-3 Delay timer command. +/// @param[in, out] ac A Ptr to an IRArgoAC_WREM3 object to use. +/// @param[in] on Whether the unit is currently on. The timer, upon elapse +/// will toggle this state +/// @param[in] currentTime currentTime in minutes, starting from 00:00 +/// @note For timer mode, this value is not really used much so can be zero. +/// @param[in] delayMinutes Number of minutes after which the @c on state should +/// be toggled +/// @note Schedule timers are not exposed via this interface +void IRac::argoWrem3_SetTimer(IRArgoAC_WREM3 *ac, bool on, + const uint16_t currentTime, const uint16_t delayMinutes) { + ac->begin(); + ac->setMessageType(argoIrMessageType_t::TIMER_COMMAND); + ac->setPower(on); + ac->setTimerType(argoTimerType_t::DELAY_TIMER); + ac->setCurrentTimeMinutes(currentTime); + // Note: Day of week is not set (no need) + ac->setDelayTimerMinutes(delayMinutes); + ac->send(); +} #endif // SEND_ARGO #if SEND_BOSCH144 @@ -2654,6 +2785,7 @@ stdAc::state_t IRac::cleanState(const stdAc::state_t state) { // A hack for Home Assistant, it appears to need/want an Off opmode. // So enforce the power is off if the mode is also off. if (state.mode == stdAc::opmode_t::kOff) result.power = false; + if (state.roomTemperature == -1.0) result.roomTemperature = state.degrees; return result; } @@ -2850,9 +2982,36 @@ bool IRac::sendAc(const stdAc::state_t desired, const stdAc::state_t *prev) { #if SEND_ARGO case ARGO: { - IRArgoAC ac(_pin, _inverted, _modulation); - argo(&ac, send.power, send.mode, degC, send.fanspeed, send.swingv, - send.turbo, send.sleep); + if (send.model == argo_ac_remote_model_t::SAC_WREM3) { + IRArgoAC_WREM3 ac(_pin, _inverted, _modulation); + switch (send.command) { + case stdAc::ac_command_t::kTemperatureReport: + argoWrem3_iFeelReport(&ac, send.roomTemperature); + break; + case stdAc::ac_command_t::kConfigCommand: + /// @warning: this is ABUSING current **common** parameters: + /// @c clock and @c sleep as config key and value + /// Hence, value pre-validation is performed (safe-mode) + /// to avoid accidental device misconfiguration + argoWrem3_ConfigSet(&ac, send.clock, send.sleep, true); + break; + case stdAc::ac_command_t::kTimerCommand: + argoWrem3_SetTimer(&ac, send.power, send.clock, send.sleep); + break; + case stdAc::ac_command_t::kControlCommand: + default: + argoWrem3_ACCommand(&ac, send.power, send.mode, send.degrees, + send.roomTemperature, send.fanspeed, send.swingv, send.iFeel, + send.quiet, send.econo, send.turbo, send.filter, send.light); + break; + } + OUTPUT_DECODE_RESULTS_FOR_UT(ac); + } else { + IRArgoAC ac(_pin, _inverted, _modulation); + argo(&ac, send.power, send.mode, degC, send.fanspeed, send.swingv, + send.turbo, send.sleep); + OUTPUT_DECODE_RESULTS_FOR_UT(ac); + } break; } #endif // SEND_ARGO @@ -3421,7 +3580,9 @@ bool IRac::cmpStates(const stdAc::state_t a, const stdAc::state_t b) { a.fanspeed != b.fanspeed || a.swingv != b.swingv || a.swingh != b.swingh || a.quiet != b.quiet || a.turbo != b.turbo || a.econo != b.econo || a.light != b.light || a.filter != b.filter || - a.clean != b.clean || a.beep != b.beep || a.sleep != b.sleep; + a.clean != b.clean || a.beep != b.beep || a.sleep != b.sleep || + a.mode != b.mode || a.roomTemperature != b.roomTemperature || + a.iFeel != b.iFeel; } /// Check if the internal state has changed from what was previously sent. @@ -3429,6 +3590,26 @@ bool IRac::cmpStates(const stdAc::state_t a, const stdAc::state_t b) { /// @return True if it has changed, False if not. bool IRac::hasStateChanged(void) { return cmpStates(next, _prev); } +/// Convert the supplied str into the appropriate enum. +/// @param[in] str A Ptr to a C-style string to be converted. +/// @param[in] def The enum to return if no conversion was possible. +/// @return The equivalent enum. +stdAc::ac_command_t IRac::strToCommandType(const char *str, + const stdAc::ac_command_t def) { + if (!STRCASECMP(str, kControlCommandStr)) + return stdAc::ac_command_t::kControlCommand; + else if (!STRCASECMP(str, kTemperatureReportStr) || + !STRCASECMP(str, kIFeelStr)) + return stdAc::ac_command_t::kTemperatureReport; + else if (!STRCASECMP(str, kTimerCommandStr) || + !STRCASECMP(str, kTimerStr)) + return stdAc::ac_command_t::kTimerCommand; + else if (!STRCASECMP(str, kConfigCommandStr)) + return stdAc::ac_command_t::kConfigCommand; + else + return def; +} + /// Convert the supplied str into the appropriate enum. /// @param[in] str A Ptr to a C-style string to be converted. /// @param[in] def The enum to return if no conversion was possible. @@ -3492,6 +3673,8 @@ stdAc::fanspeed_t IRac::strToFanspeed(const char *str, !STRCASECMP(str, kMaximumStr) || !STRCASECMP(str, kHighestStr)) return stdAc::fanspeed_t::kMax; + else if (!STRCASECMP(str, kMedHighStr)) + return stdAc::fanspeed_t::kMediumHigh; else return def; } @@ -3666,6 +3849,11 @@ int16_t IRac::strToModel(const char *str, const int16_t def) { return whirlpool_ac_remote_model_t::DG11J13A; } else if (!STRCASECMP(str, kDg11j191Str)) { return whirlpool_ac_remote_model_t::DG11J191; + // Argo A/C models + } else if (!STRCASECMP(str, kArgoWrem2Str)) { + return argo_ac_remote_model_t::SAC_WREM2; + } else if (!STRCASECMP(str, kArgoWrem3Str)) { + return argo_ac_remote_model_t::SAC_WREM3; } else { int16_t number = atoi(str); if (number > 0) @@ -3701,6 +3889,20 @@ String IRac::boolToString(const bool value) { return value ? kOnStr : kOffStr; } +/// Convert the supplied operation mode into the appropriate String. +/// @param[in] mode The enum to be converted. +/// @param[in] ha A flag to indicate we want GoogleHome/HomeAssistant output. +/// @return The equivalent String for the locale. +String IRac::commandTypeToString(const stdAc::ac_command_t cmdType) { + switch (cmdType) { + case stdAc::ac_command_t::kControlCommand: return kControlCommandStr; + case stdAc::ac_command_t::kTemperatureReport: return kTemperatureReportStr; + case stdAc::ac_command_t::kTimerCommand: return kTimerCommandStr; + case stdAc::ac_command_t::kConfigCommand: return kConfigCommandStr; + default: return kUnknownStr; + } +} + /// Convert the supplied operation mode into the appropriate String. /// @param[in] mode The enum to be converted. /// @param[in] ha A flag to indicate we want GoogleHome/HomeAssistant output. @@ -3737,14 +3939,15 @@ String IRac::fanspeedToString(const stdAc::fanspeed_t speed) { /// @return The equivalent String for the locale. String IRac::swingvToString(const stdAc::swingv_t swingv) { switch (swingv) { - case stdAc::swingv_t::kOff: return kOffStr; - case stdAc::swingv_t::kAuto: return kAutoStr; - case stdAc::swingv_t::kHighest: return kHighestStr; - case stdAc::swingv_t::kHigh: return kHighStr; - case stdAc::swingv_t::kMiddle: return kMiddleStr; - case stdAc::swingv_t::kLow: return kLowStr; - case stdAc::swingv_t::kLowest: return kLowestStr; - default: return kUnknownStr; + case stdAc::swingv_t::kOff: return kOffStr; + case stdAc::swingv_t::kAuto: return kAutoStr; + case stdAc::swingv_t::kHighest: return kHighestStr; + case stdAc::swingv_t::kHigh: return kHighStr; + case stdAc::swingv_t::kMiddle: return kMiddleStr; + case stdAc::swingv_t::kUpperMiddle: return kUpperMiddleStr; + case stdAc::swingv_t::kLow: return kLowStr; + case stdAc::swingv_t::kLowest: return kLowestStr; + default: return kUnknownStr; } } @@ -3796,6 +3999,12 @@ namespace IRAcUtils { #endif // DECODE_AMCOR #if DECODE_ARGO case decode_type_t::ARGO: { + if (IRArgoAC_WREM3::isValidWrem3Message(result->state, result->bits, + true)) { + IRArgoAC_WREM3 ac(kGpioUnused); + ac.setRaw(result->state, result->bits / 8); + return ac.toString(); + } IRArgoAC ac(kGpioUnused); ac.setRaw(result->state, result->bits / 8); return ac.toString(); @@ -4258,15 +4467,23 @@ namespace IRAcUtils { #endif // DECODE_AMCOR #if DECODE_ARGO case decode_type_t::ARGO: { - IRArgoAC ac(kGpioUnused); const uint16_t length = decode->bits / 8; - switch (length) { - case kArgoStateLength: - ac.setRaw(decode->state, length); - *result = ac.toCommon(); - break; - default: - return false; + if (IRArgoAC_WREM3::isValidWrem3Message(decode->state, + decode->bits, true)) { + IRArgoAC_WREM3 ac(kGpioUnused); + ac.setRaw(decode->state, length); + *result = ac.toCommon(); + } else { + IRArgoAC ac(kGpioUnused); + switch (length) { + case kArgoStateLength: + case kArgoShortStateLength: + ac.setRaw(decode->state, length); + *result = ac.toCommon(); + break; + default: + return false; + } } break; } diff --git a/src/IRac.h b/src/IRac.h index 9193a531d..9a0ca3e53 100644 --- a/src/IRac.h +++ b/src/IRac.h @@ -5,6 +5,8 @@ #ifndef UNIT_TEST #include +#else +#include #endif #include "IRremoteESP8266.h" #include "ir_Airton.h" @@ -84,6 +86,8 @@ class IRac { static bool cmpStates(const stdAc::state_t a, const stdAc::state_t b); static bool strToBool(const char *str, const bool def = false); static int16_t strToModel(const char *str, const int16_t def = -1); + static stdAc::ac_command_t strToCommandType(const char *str, + const stdAc::ac_command_t def = stdAc::ac_command_t::kControlCommand); static stdAc::opmode_t strToOpmode( const char *str, const stdAc::opmode_t def = stdAc::opmode_t::kAuto); static stdAc::fanspeed_t strToFanspeed( @@ -94,6 +98,7 @@ class IRac { static stdAc::swingh_t strToSwingH( const char *str, const stdAc::swingh_t def = stdAc::swingh_t::kOff); static String boolToString(const bool value); + static String commandTypeToString(const stdAc::ac_command_t cmdType); static String opmodeToString(const stdAc::opmode_t mode, const bool ha = false); static String fanspeedToString(const stdAc::fanspeed_t speed); @@ -103,10 +108,17 @@ class IRac { stdAc::state_t getStatePrev(void); bool hasStateChanged(void); stdAc::state_t next; ///< The state we want the device to be in after we send -#ifndef UNIT_TEST +#ifdef UNIT_TEST + /// @cond IGNORE + /// UT-specific + /// See @c OUTPUT_DECODE_RESULTS_FOR_UT macro description in IRac.cpp + std::shared_ptr _utReceiver = nullptr; + std::unique_ptr _lastDecodeResults = nullptr; + /// @endcond +#else private: -#endif +#endif // UNIT_TEST uint16_t _pin; ///< The GPIO to use to transmit messages from. bool _inverted; ///< IR LED is lit when GPIO is LOW (true) or HIGH (false)? bool _modulation; ///< Is frequency modulation to be used? @@ -134,6 +146,16 @@ class IRac { const bool on, const stdAc::opmode_t mode, const float degrees, const stdAc::fanspeed_t fan, const stdAc::swingv_t swingv, const bool turbo, const int16_t sleep = -1); + void argoWrem3_ACCommand(IRArgoAC_WREM3 *ac, + const bool on, const stdAc::opmode_t mode, const float degrees, + const float roomTemp, const stdAc::fanspeed_t fan, + const stdAc::swingv_t swingv, const bool iFeel, const bool night, + const bool econo, const bool turbo, const bool filter, const bool light); + void argoWrem3_iFeelReport(IRArgoAC_WREM3 *ac, const float roomTemp); + void argoWrem3_ConfigSet(IRArgoAC_WREM3 *ac, const uint8_t param, + const uint8_t value, bool safe = true); + void argoWrem3_SetTimer(IRArgoAC_WREM3 *ac, bool on, + const uint16_t currentTime, const uint16_t delayMinutes); #endif // SEND_ARGO #if SEND_BOSCH144 void bosch144(IRBosch144AC *ac, diff --git a/src/IRrecv.cpp b/src/IRrecv.cpp index ef5d46396..d21c2a0a4 100644 --- a/src/IRrecv.cpp +++ b/src/IRrecv.cpp @@ -943,8 +943,20 @@ bool IRrecv::decode(decode_results *results, irparams_t *save, return true; #endif #if DECODE_ARGO - DPRINTLN("Attempting Argo decode"); - if (decodeArgo(results, offset) || + DPRINTLN("Attempting Argo WREM3 decode (AC Control)"); + if (decodeArgoWREM3(results, offset, kArgo3AcControlStateLength * 8, true)) + return true; + DPRINTLN("Attempting Argo WREM3 decode (iFeel report)"); + if (decodeArgoWREM3(results, offset, kArgo3iFeelReportStateLength * 8, true)) + return true; + DPRINTLN("Attempting Argo WREM3 decode (Config)"); + if (decodeArgoWREM3(results, offset, kArgo3ConfigStateLength * 8, true)) + return true; + DPRINTLN("Attempting Argo WREM3 decode (Timer)"); + if (decodeArgoWREM3(results, offset, kArgo3TimerStateLength * 8, true)) + return true; + DPRINTLN("Attempting Argo WREM2 decode"); + if (decodeArgo(results, offset, kArgoBits) || decodeArgo(results, offset, kArgoShortBits, false)) return true; #endif // DECODE_ARGO #if DECODE_SHARP_AC diff --git a/src/IRrecv.h b/src/IRrecv.h index 903fd2b07..2fc751dff 100644 --- a/src/IRrecv.h +++ b/src/IRrecv.h @@ -294,6 +294,9 @@ class IRrecv { #if DECODE_ARGO bool decodeArgo(decode_results *results, uint16_t offset = kStartOffset, const uint16_t nbits = kArgoBits, const bool strict = true); + bool decodeArgoWREM3(decode_results *results, uint16_t offset = kStartOffset, + const uint16_t nbits = kArgo3AcControlStateLength * 8, + const bool strict = true); #endif // DECODE_ARGO #if DECODE_ARRIS bool decodeArris(decode_results *results, uint16_t offset = kStartOffset, diff --git a/src/IRremoteESP8266.h b/src/IRremoteESP8266.h index 2484ed9c9..aee9b831a 100644 --- a/src/IRremoteESP8266.h +++ b/src/IRremoteESP8266.h @@ -1134,6 +1134,10 @@ const uint16_t kArgoStateLength = 12; const uint16_t kArgoShortStateLength = 4; const uint16_t kArgoBits = kArgoStateLength * 8; const uint16_t kArgoShortBits = kArgoShortStateLength * 8; +const uint16_t kArgo3AcControlStateLength = 6; // Bytes +const uint16_t kArgo3iFeelReportStateLength = 2; // Bytes +const uint16_t kArgo3TimerStateLength = 9; // Bytes +const uint16_t kArgo3ConfigStateLength = 4; // Bytes const uint16_t kArgoDefaultRepeat = kNoRepeat; const uint16_t kArrisBits = 32; const uint16_t kBosch144StateLength = 18; diff --git a/src/IRsend.h b/src/IRsend.h index 547253cf6..9b512cfb5 100644 --- a/src/IRsend.h +++ b/src/IRsend.h @@ -56,14 +56,15 @@ enum class opmode_t { /// Common A/C settings for Fan Speeds. enum class fanspeed_t { - kAuto = 0, - kMin = 1, - kLow = 2, - kMedium = 3, - kHigh = 4, - kMax = 5, + kAuto = 0, + kMin = 1, + kLow = 2, + kMedium = 3, + kHigh = 4, + kMax = 5, + kMediumHigh = 6, // Add new entries before this one, and update it to point to the last entry - kLastFanspeedEnum = kMax, + kLastFanspeedEnum = kMediumHigh, }; /// Common A/C settings for Vertical Swing. @@ -75,8 +76,21 @@ enum class swingv_t { kMiddle = 3, kLow = 4, kLowest = 5, + kUpperMiddle = 6, // Add new entries before this one, and update it to point to the last entry - kLastSwingvEnum = kLowest, + kLastSwingvEnum = kUpperMiddle, +}; + +/// @brief Tyoe of A/C command (if the remote uses different codes for each) +/// @note Most remotes support only a single command or aggregate multiple +/// into one (e.g. control+timer). Use @c kControlCommand in such case +enum class ac_command_t { + kControlCommand = 0, + kTemperatureReport = 1, + kTimerCommand = 2, + kConfigCommand = 3, + // Add new entries before this one, and update it to point to the last entry + kLastAcCommandEnum = kConfigCommand, }; /// Common A/C settings for Horizontal Swing. @@ -113,6 +127,9 @@ struct state_t { bool beep = false; int16_t sleep = -1; // `-1` means off. int16_t clock = -1; // `-1` means not set. + bool iFeel = false; + float roomTemperature = -1; // `-1` means not set. + stdAc::ac_command_t command = stdAc::ac_command_t::kControlCommand; }; }; // namespace stdAc @@ -202,6 +219,11 @@ enum lg_ac_remote_model_t { LG6711A20083V, // (5) Same as GE6711AR2853M, but only SwingV toggle. }; +/// Argo A/C model numbers +enum argo_ac_remote_model_t { + SAC_WREM2 = 1, // (1) ARGO WREM2 remote (default) + SAC_WREM3 // (2) ARGO WREM3 remote (touch buttons), bit-len vary by cmd +}; // Classes @@ -528,9 +550,13 @@ class IRsend { #endif #if SEND_ARGO void sendArgo(const unsigned char data[], + const uint16_t nbytes = kArgoStateLength, + const uint16_t repeat = kArgoDefaultRepeat, + bool sendFooter = false); + void sendArgoWREM3(const unsigned char data[], const uint16_t nbytes = kArgoStateLength, const uint16_t repeat = kArgoDefaultRepeat); -#endif +#endif // SEND_ARGO #if SEND_TROTEC void sendTrotec(const unsigned char data[], const uint16_t nbytes = kTrotecStateLength, diff --git a/src/IRtext.cpp b/src/IRtext.cpp index 14d3d1496..1f10e4b17 100644 --- a/src/IRtext.cpp +++ b/src/IRtext.cpp @@ -68,6 +68,8 @@ IRTEXT_CONST_STRING(kOffTimerStr, D_STR_OFFTIMER); ///< "Off Timer" IRTEXT_CONST_STRING(kTimerModeStr, D_STR_TIMERMODE); ///< "Timer Mode" IRTEXT_CONST_STRING(kClockStr, D_STR_CLOCK); ///< "Clock" IRTEXT_CONST_STRING(kCommandStr, D_STR_COMMAND); ///< "Command" +IRTEXT_CONST_STRING(kConfigCommandStr, D_STR_AC_COMMAND); ///< "A/C Config" +IRTEXT_CONST_STRING(kControlCommandStr, D_STR_AC_CONTROL); ///< "A/C Control" IRTEXT_CONST_STRING(kXFanStr, D_STR_XFAN); ///< "XFan" IRTEXT_CONST_STRING(kHealthStr, D_STR_HEALTH); ///< "Health" IRTEXT_CONST_STRING(kModelStr, D_STR_MODEL); ///< "Model" @@ -123,6 +125,7 @@ IRTEXT_CONST_STRING(kOutsideStr, D_STR_OUTSIDE); ///< "Outside" IRTEXT_CONST_STRING(kLoudStr, D_STR_LOUD); ///< "Loud" IRTEXT_CONST_STRING(kLowerStr, D_STR_LOWER); ///< "Lower" IRTEXT_CONST_STRING(kUpperStr, D_STR_UPPER); ///< "Upper" +IRTEXT_CONST_STRING(kUpperMiddleStr, D_STR_UPPER_MIDDLE); ///< "UpperMiddle" IRTEXT_CONST_STRING(kBreezeStr, D_STR_BREEZE); ///< "Breeze" IRTEXT_CONST_STRING(kCirculateStr, D_STR_CIRCULATE); ///< "Circulate" IRTEXT_CONST_STRING(kCeilingStr, D_STR_CEILING); ///< "Ceiling" @@ -158,6 +161,7 @@ IRTEXT_CONST_STRING(kRecycleStr, D_STR_RECYCLE); ///< "Recycle" IRTEXT_CONST_STRING(kMaxStr, D_STR_MAX); ///< "Max" IRTEXT_CONST_STRING(kMaximumStr, D_STR_MAXIMUM); ///< "Maximum" +IRTEXT_CONST_STRING(kMedHighStr, D_STR_MED_HIGH); ///< "Med-high" IRTEXT_CONST_STRING(kMinStr, D_STR_MIN); ///< "Min" IRTEXT_CONST_STRING(kMinimumStr, D_STR_MINIMUM); ///< "Minimum" IRTEXT_CONST_STRING(kMedStr, D_STR_MED); ///< "Med" @@ -205,6 +209,15 @@ IRTEXT_CONST_STRING(kSwingVModeStr, D_STR_SWINGVMODE); ///< "Swing(V) Mode" IRTEXT_CONST_STRING(kSwingVToggleStr, D_STR_SWINGVTOGGLE); ///< ///< "Swing(V) Toggle" IRTEXT_CONST_STRING(kTurboToggleStr, D_STR_TURBOTOGGLE); ///< "Turbo Toggle" +IRTEXT_CONST_STRING(kTemperatureReportStr, D_STR_AC_TEMP_REPORT); ///< +///< "A/C Temp Report" +IRTEXT_CONST_STRING(kTimerCommandStr, D_STR_AC_TIMER); ///< "A/C Set Timer" +IRTEXT_CONST_STRING(kScheduleStr, D_STR_SCHEDULE); ///< "Schedule" +IRTEXT_CONST_STRING(kChStr, D_STR_CH); ///< "CH#" +IRTEXT_CONST_STRING(kTimerActiveDaysStr, D_STR_TIMER_ACTIVE_DAYS); +///< "TimerActiveDays" +IRTEXT_CONST_STRING(kKeyStr, D_STR_KEY); ///< "Key" +IRTEXT_CONST_STRING(kValueStr, D_STR_VALUE); ///< "Value" // Separators & Punctuation const char kTimeSep = D_CHR_TIME_SEP; ///< ':' @@ -281,6 +294,8 @@ IRTEXT_CONST_STRING(k122lzfStr, D_STR_122LZF); ///< "122LZF" IRTEXT_CONST_STRING(kDg11j13aStr, D_STR_DG11J13A); ///< "DG11J13A" IRTEXT_CONST_STRING(kDg11j104Str, D_STR_DG11J104); ///< "DG11J104" IRTEXT_CONST_STRING(kDg11j191Str, D_STR_DG11J191); ///< "DG11J191" +IRTEXT_CONST_STRING(kArgoWrem2Str, D_STR_ARGO_WREM2); ///< "WREM3" +IRTEXT_CONST_STRING(kArgoWrem3Str, D_STR_ARGO_WREM3); ///< "WREM3" #define D_STR_UNSUPPORTED "?" // Unsupported protocols will be showing as // a question mark, check for length > 1 diff --git a/src/IRtext.h b/src/IRtext.h index 7bd4fbed3..73164f229 100644 --- a/src/IRtext.h +++ b/src/IRtext.h @@ -39,6 +39,8 @@ extern IRTEXT_CONST_PTR(kAkb73757604Str); extern IRTEXT_CONST_PTR(kAkb74955603Str); extern IRTEXT_CONST_PTR(kAkb75215403Str); extern IRTEXT_CONST_PTR(kArdb1Str); +extern IRTEXT_CONST_PTR(kArgoWrem2Str); +extern IRTEXT_CONST_PTR(kArgoWrem3Str); extern IRTEXT_CONST_PTR(kArjw2Str); extern IRTEXT_CONST_PTR(kArrah2eStr); extern IRTEXT_CONST_PTR(kArreb1eStr); @@ -57,6 +59,7 @@ extern IRTEXT_CONST_PTR(kCelsiusFahrenheitStr); extern IRTEXT_CONST_PTR(kCelsiusStr); extern IRTEXT_CONST_PTR(kCentreStr); extern IRTEXT_CONST_PTR(kChangeStr); +extern IRTEXT_CONST_PTR(kChStr); extern IRTEXT_CONST_PTR(kCirculateStr); extern IRTEXT_CONST_PTR(kCkpStr); extern IRTEXT_CONST_PTR(kCleanStr); @@ -66,6 +69,8 @@ extern IRTEXT_CONST_PTR(kColonSpaceStr); extern IRTEXT_CONST_PTR(kComfortStr); extern IRTEXT_CONST_PTR(kCommaSpaceStr); extern IRTEXT_CONST_PTR(kCommandStr); +extern IRTEXT_CONST_PTR(kConfigCommandStr); +extern IRTEXT_CONST_PTR(kControlCommandStr); extern IRTEXT_CONST_PTR(kCoolStr); extern IRTEXT_CONST_PTR(kCoolingStr); extern IRTEXT_CONST_PTR(kDashStr); @@ -116,6 +121,7 @@ extern IRTEXT_CONST_PTR(kIndirectStr); extern IRTEXT_CONST_PTR(kInsideStr); extern IRTEXT_CONST_PTR(kIonStr); extern IRTEXT_CONST_PTR(kJkeStr); +extern IRTEXT_CONST_PTR(kKeyStr); extern IRTEXT_CONST_PTR(kKkg29ac1Str); extern IRTEXT_CONST_PTR(kKkg9ac1Str); extern IRTEXT_CONST_PTR(kLastStr); @@ -139,6 +145,7 @@ extern IRTEXT_CONST_PTR(kMaxRightNoSpaceStr); extern IRTEXT_CONST_PTR(kMaxRightStr); extern IRTEXT_CONST_PTR(kMaxStr); extern IRTEXT_CONST_PTR(kMaximumStr); +extern IRTEXT_CONST_PTR(kMedHighStr); extern IRTEXT_CONST_PTR(kMedStr); extern IRTEXT_CONST_PTR(kMediumStr); extern IRTEXT_CONST_PTR(kMidStr); @@ -188,6 +195,7 @@ extern IRTEXT_CONST_PTR(kRlt0541htaaStr); extern IRTEXT_CONST_PTR(kRlt0541htabStr); extern IRTEXT_CONST_PTR(kRoomStr); extern IRTEXT_CONST_PTR(kSaveStr); +extern IRTEXT_CONST_PTR(kScheduleStr); extern IRTEXT_CONST_PTR(kSecondStr); extern IRTEXT_CONST_PTR(kSecondsStr); extern IRTEXT_CONST_PTR(kSensorStr); @@ -209,11 +217,14 @@ extern IRTEXT_CONST_PTR(kSwingVModeStr); extern IRTEXT_CONST_PTR(kSwingVStr); extern IRTEXT_CONST_PTR(kSwingVToggleStr); extern IRTEXT_CONST_PTR(kTac09chsdStr); +extern IRTEXT_CONST_PTR(kTemperatureReportStr); extern IRTEXT_CONST_PTR(kTempDownStr); extern IRTEXT_CONST_PTR(kTempStr); extern IRTEXT_CONST_PTR(kTempUpStr); extern IRTEXT_CONST_PTR(kThreeLetterDayOfWeekStr); +extern IRTEXT_CONST_PTR(kTimerActiveDaysStr); extern IRTEXT_CONST_PTR(kTimerModeStr); +extern IRTEXT_CONST_PTR(kTimerCommandStr); extern IRTEXT_CONST_PTR(kTimerStr); extern IRTEXT_CONST_PTR(kToggleStr); extern IRTEXT_CONST_PTR(kTopStr); @@ -224,6 +235,8 @@ extern IRTEXT_CONST_PTR(kTypeStr); extern IRTEXT_CONST_PTR(kUnknownStr); extern IRTEXT_CONST_PTR(kUpStr); extern IRTEXT_CONST_PTR(kUpperStr); +extern IRTEXT_CONST_PTR(kUpperMiddleStr); +extern IRTEXT_CONST_PTR(kValueStr); extern IRTEXT_CONST_PTR(kV9014557AStr); extern IRTEXT_CONST_PTR(kV9014557BStr); extern IRTEXT_CONST_PTR(kVaneStr); diff --git a/src/IRutils.cpp b/src/IRutils.cpp index 7c59228c0..49746ac4a 100644 --- a/src/IRutils.cpp +++ b/src/IRutils.cpp @@ -695,6 +695,13 @@ namespace irutils { default: return kUnknownStr; } break; + case decode_type_t::ARGO: + switch (model) { + case argo_ac_remote_model_t::SAC_WREM2: return kArgoWrem2Str; + case argo_ac_remote_model_t::SAC_WREM3: return kArgoWrem3Str; + default: return kUnknownStr; + } + break; default: return kUnknownStr; } } @@ -722,10 +729,12 @@ namespace irutils { /// @param[in] celsius Is the temp Celsius or Fahrenheit. /// true is C, false is F /// @param[in] precomma Should the output string start with ", " or not? + /// @param[in] isRoomTemp Is the value a room (ambient) temperature or target? /// @return The resulting String. String addTempToString(const uint16_t degrees, const bool celsius, - const bool precomma) { - String result = addIntToString(degrees, kTempStr, precomma); + const bool precomma, const bool isRoomTemp) { + String result = addIntToString(degrees, (isRoomTemp)? kRoomStr : kTempStr, + precomma); result += celsius ? 'C' : 'F'; return result; } @@ -736,12 +745,14 @@ namespace irutils { /// @param[in] celsius Is the temp Celsius or Fahrenheit. /// true is C, false is F /// @param[in] precomma Should the output string start with ", " or not? + /// @param[in] isRoomTemp Is the value a room (ambient) temperature or target? /// @return The resulting String. String addTempFloatToString(const float degrees, const bool celsius, - const bool precomma) { + const bool precomma, const bool isRoomTemp) { String result = ""; result.reserve(14); // Assuming ", Temp: XXX.5F" is the largest. - result += addIntToString(degrees, kTempStr, precomma); + result += addIntToString(degrees, isRoomTemp? kRoomStr : kTempStr, + precomma); // Is it a half degree? if (((uint16_t)(2 * degrees)) & 1) result += F(".5"); result += celsius ? 'C' : 'F'; @@ -788,43 +799,57 @@ namespace irutils { result.reserve(19); // ", Day: N (UNKNOWN)" result += addIntToString(day_of_week, kDayStr, precomma); result += kSpaceLBraceStr; + result += dayToString(day_of_week, offset); + return result + ')'; + } + + /// Create a String of the 3-letter day of the week from a numerical day of + /// the week. e.g. "Mon" + /// @param[in] day_of_week A numerical version of the sequential day of the + /// week. e.g. Sunday = 1, Monday = 2, ..., Saturday = 7 + /// @param[in] offset Days to offset by. + /// e.g. For different day starting the week. + /// @return The resulting String. + String dayToString(const uint8_t day_of_week, const int8_t offset) { if ((uint8_t)(day_of_week + offset) < 7) #if UNIT_TEST - result += String(kThreeLetterDayOfWeekStr).substr( - (day_of_week + offset) * 3, 3); + return String(kThreeLetterDayOfWeekStr).substr( + (day_of_week + offset) * 3, 3); #else // UNIT_TEST - result += String(kThreeLetterDayOfWeekStr).substring( - (day_of_week + offset) * 3, (day_of_week + offset) * 3 + 3); + return String(kThreeLetterDayOfWeekStr).substring( + (day_of_week + offset) * 3, (day_of_week + offset) * 3 + 3); #endif // UNIT_TEST else - result += kUnknownStr; - return result + ')'; + return kUnknownStr; } /// Create a String of human output for the given fan speed. /// e.g. "Fan: 0 (Auto)" /// @param[in] speed The numeric speed of the fan to display. - /// @param[in] high The numeric value for High speed. + /// @param[in] high The numeric value for High speed. (second highest) /// @param[in] low The numeric value for Low speed. /// @param[in] automatic The numeric value for Auto speed. /// @param[in] quiet The numeric value for Quiet speed. /// @param[in] medium The numeric value for Medium speed. /// @param[in] maximum The numeric value for Highest speed. (if > high) + /// @param[in] medium_high The numeric value for third-highest speed. + /// (if > medium) /// @return The resulting String. String addFanToString(const uint8_t speed, const uint8_t high, const uint8_t low, const uint8_t automatic, const uint8_t quiet, const uint8_t medium, - const uint8_t maximum) { + const uint8_t maximum, const uint8_t medium_high) { String result = ""; result.reserve(21); // ", Fan: NNN (UNKNOWN)" result += addIntToString(speed, kFanStr); result += kSpaceLBraceStr; - if (speed == high) result += kHighStr; - else if (speed == low) result += kLowStr; - else if (speed == automatic) result += kAutoStr; - else if (speed == quiet) result += kQuietStr; - else if (speed == medium) result += kMediumStr; - else if (speed == maximum) result += kMaximumStr; + if (speed == high) result += kHighStr; + else if (speed == low) result += kLowStr; + else if (speed == automatic) result += kAutoStr; + else if (speed == quiet) result += kQuietStr; + else if (speed == medium) result += kMediumStr; + else if (speed == maximum) result += kMaximumStr; + else if (speed == medium_high) result += kMedHighStr; else result += kUnknownStr; return result + ')'; diff --git a/src/IRutils.h b/src/IRutils.h index a5dcde043..5d5bb24da 100644 --- a/src/IRutils.h +++ b/src/IRutils.h @@ -60,16 +60,19 @@ namespace irutils { String addLabeledString(const String value, const String label, const bool precomma = true); String addTempToString(const uint16_t degrees, const bool celsius = true, - const bool precomma = true); + const bool precomma = true, + const bool isRoomTemp = false); String addTempFloatToString(const float degrees, const bool celsius = true, - const bool precomma = true); + const bool precomma = true, + const bool isRoomTemp = false); String addModeToString(const uint8_t mode, const uint8_t automatic, const uint8_t cool, const uint8_t heat, const uint8_t dry, const uint8_t fan); String addFanToString(const uint8_t speed, const uint8_t high, const uint8_t low, const uint8_t automatic, const uint8_t quiet, const uint8_t medium, - const uint8_t maximum = 0xFF); + const uint8_t maximum = 0xFF, + const uint8_t medium_high = 0xFE); String addSwingHToString(const uint8_t position, const uint8_t automatic, const uint8_t maxleft, const uint8_t left, const uint8_t middle, @@ -87,6 +90,7 @@ namespace irutils { const uint8_t breeze, const uint8_t circulate); String addDayToString(const uint8_t day_of_week, const int8_t offset = 0, const bool precomma = true); + String dayToString(const uint8_t day_of_week, const int8_t offset = 0); String htmlEscape(const String unescaped); String msToString(uint32_t const msecs); String minsToString(const uint16_t mins); diff --git a/src/ir_Argo.cpp b/src/ir_Argo.cpp index 885406faf..8e8eff208 100644 --- a/src/ir_Argo.cpp +++ b/src/ir_Argo.cpp @@ -1,11 +1,16 @@ // Copyright 2017 Schmolders // Copyright 2019 crankyoldgit +// Copyright 2022 Mateusz Bronk (mbronk) /// @file /// @brief Argo A/C protocol. -/// Controls an Argo Ulisse 13 DCI A/C + +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1859 +/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1912 + #include "ir_Argo.h" #include +#include #include #ifndef UNIT_TEST #include @@ -22,222 +27,858 @@ const uint16_t kArgoBitMark = 400; const uint16_t kArgoOneSpace = 2200; const uint16_t kArgoZeroSpace = 900; const uint32_t kArgoGap = kDefaultMessageGap; // Made up value. Complete guess. - const uint8_t kArgoSensorCheck = 52; // Part of the sensor message check calc. const uint8_t kArgoSensorFixed = 0b011; +const uint8_t kArgoWrem3Preamble = 0b1011; +const uint8_t kArgoWrem3Postfix_Timer = 0b1; +const uint8_t kArgoWrem3Postfix_ACControl = 0b110000; using irutils::addBoolToString; using irutils::addIntToString; using irutils::addLabeledString; using irutils::addModeToString; using irutils::addTempToString; +using irutils::addFanToString; +using irutils::addSwingVToString; +using irutils::minsToString; +using irutils::addDayToString; +using irutils::addModelToString; #if SEND_ARGO /// Send a Argo A/C formatted message. -/// Status: BETA / Probably works. +/// Status: [WREM-2] BETA / Probably works. +/// [WREM-3] Confirmed working w/ Argo 13 ECO (WREM-3) +/// @note The "no footer" part needs re-checking for validity but retained for +/// backwards compatibility. +/// Consider using @c sendFooter=true code for WREM-2 as well /// @param[in] data The message to be sent. /// @param[in] nbytes The number of bytes of message to be sent. /// @param[in] repeat The number of times the command is to be repeated. +/// @param[in] sendFooter Whether to send footer and add a final gap. +/// *REQUIRED* for WREM-3, UNKNOWN for WREM-2 (used to be +/// disabled in previous impl., hence retained) +/// @note Consider removing this param (default to true) if WREM-2 works w/ it void IRsend::sendArgo(const unsigned char data[], const uint16_t nbytes, - const uint16_t repeat) { - // TODO(kaschmo): validate + const uint16_t repeat, bool sendFooter /*= false*/) { + if (nbytes < std::min({kArgo3AcControlStateLength, + kArgo3ConfigStateLength, + kArgo3iFeelReportStateLength, + kArgo3TimerStateLength, + kArgoStateLength, + kArgoShortStateLength})) { + return; // Not enough bytes to send a proper message. + } + + const uint16_t _footermark = (sendFooter)? kArgoBitMark : 0; + const uint32_t _gap = (sendFooter)? kArgoGap : 0; + sendGeneric(kArgoHdrMark, kArgoHdrSpace, kArgoBitMark, kArgoOneSpace, - kArgoBitMark, kArgoZeroSpace, 0, 0, // No Footer. - data, nbytes, 38, false, repeat, kDutyDefault); + kArgoBitMark, kArgoZeroSpace, + _footermark, _gap, + data, nbytes, kArgoFrequency, false, repeat, kDutyDefault); +} + + +/// Send a Argo A/C formatted message. +/// Status: Confirmed working w/ Argo 13 ECO (WREM-3) +/// @param[in] data The message to be sent. +/// @param[in] nbytes The number of bytes of message to be sent. +/// @param[in] repeat The number of times the command is to be repeated. +void IRsend::sendArgoWREM3(const unsigned char data[], const uint16_t nbytes, + const uint16_t repeat) { + sendArgo(data, nbytes, repeat, true); } #endif // SEND_ARGO + /// 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? -IRArgoAC::IRArgoAC(const uint16_t pin, const bool inverted, +template +IRArgoACBase::IRArgoACBase(const uint16_t pin, const bool inverted, const bool use_modulation) : _irsend(pin, inverted, use_modulation) { stateReset(); } + +/// 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? +IRArgoAC::IRArgoAC(const uint16_t pin, const bool inverted, + const bool use_modulation) + : IRArgoACBase(pin, inverted, use_modulation) { } + + +/// 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? +IRArgoAC_WREM3::IRArgoAC_WREM3(const uint16_t pin, const bool inverted, + const bool use_modulation) + : IRArgoACBase(pin, inverted, use_modulation) {} + /// Set up hardware to be able to send a message. -void IRArgoAC::begin(void) { _irsend.begin(); } +template +void IRArgoACBase::begin(void) { _irsend.begin(); } -#if SEND_ARGO -/// Send the current internal state as an IR message. -/// @param[in] repeat Nr. of times the message will be repeated. -void IRArgoAC::send(const uint16_t repeat) { - _irsend.sendArgo(getRaw(), kArgoStateLength, repeat); + +/// @brief Get byte length of raw WREM-2 message based on IR cmd type +/// @note This is a full specialization for @c ArgoProtocol type and while +/// it semantically belongs to @c IrArgoAC class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @param type The type of IR command +/// @note Not all types are supported. AC_CONTROL and TIMER are the same cmd +/// @return Byte length of state command +template<> +uint16_t IRArgoACBase::getStateLengthForIrMsgType( + argoIrMessageType_t type) { + switch (type) { + case argoIrMessageType_t::AC_CONTROL: + case argoIrMessageType_t::TIMER_COMMAND: + return kArgoStateLength; + case argoIrMessageType_t::IFEEL_TEMP_REPORT: + return kArgoShortStateLength; + case argoIrMessageType_t::CONFIG_PARAM_SET: + default: + return 0; // Not supported by WREM-2 + } } -/// Send current room temperature for the iFeel feature as a silent IR -/// message (no acknowledgement from the device). -/// @param[in] degrees The temperature in degrees celsius. -/// @param[in] repeat Nr. of times the message will be repeated. -void IRArgoAC::sendSensorTemp(const uint8_t degrees, const uint16_t repeat) { - const uint8_t temp = std::max(std::min(degrees, kArgoMaxRoomTemp), - kArgoTempDelta) - kArgoTempDelta; - const uint8_t check = kArgoSensorCheck + temp; - ArgoProtocol data; - _stateReset(&data); - data.SensorT = temp; - data.CheckHi = check >> 5; - data.CheckLo = check; - data.Fixed = kArgoSensorFixed; - _checksum(&data); - _irsend.sendArgo(data.raw, kArgoStateLength, repeat); +/// @brief Get byte length of raw WREM-3 message based on IR cmd type +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @param type The type of IR command +/// @return Byte length of state command +template<> +uint16_t IRArgoACBase::getStateLengthForIrMsgType( + argoIrMessageType_t type) { + switch (type) { + case argoIrMessageType_t::AC_CONTROL: + return kArgo3AcControlStateLength; + case argoIrMessageType_t::IFEEL_TEMP_REPORT: + return kArgo3iFeelReportStateLength; + case argoIrMessageType_t::TIMER_COMMAND: + return kArgo3TimerStateLength; + case argoIrMessageType_t::CONFIG_PARAM_SET: + return kArgo3ConfigStateLength; + default: + return 0; + } } -#endif // SEND_ARGO -/// Verify the checksum is valid for a given state. -/// @param[in] state The array to verify the checksum of. + +/// @brief Get message type from raw WREM-2 data +/// @param _ 1st param ignored: WREM-2 does not caryy type in payload, allegedly +/// @param length Message length: used for *heuristic* detection of message type +/// @return IR message type +/// @note This is a full specialization for @c ArgoProtocol type and while +/// it semantically belongs to @c IrArgoAC class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +template<> +argoIrMessageType_t IRArgoACBase::getMessageType( + const uint8_t[], const uint16_t length) { + if (length == kArgoShortStateLength) { + return argoIrMessageType_t::IFEEL_TEMP_REPORT; + } + return argoIrMessageType_t::AC_CONTROL; +} + + +/// @brief Get message type from raw WREM-3 data +/// @param state The raw IR data +/// @param length Length of @c state (in byte) +/// @return IR message type +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +template<> +argoIrMessageType_t IRArgoACBase::getMessageType( + const uint8_t state[], const uint16_t length) { + if (length < 1) { + return static_cast(-1); + } + return static_cast(state[0] >> 6); +} + +/// @brief Get message type from raw WREM-3 data +/// @param raw Raw data +/// @return IR message type +argoIrMessageType_t IRArgoAC_WREM3::getMessageType( + const ArgoProtocolWREM3& raw) { + return static_cast(raw.IrCommandType); +} + + +/// @brief Get actual raw state byte length for the current state +/// @param _ 1st param ignored: WREM-2 does not caryy type in payload, allegedly +/// @param messageType Type of message the state is carrying +/// @return Actual length of state (in bytes) +/// @note This is a full specialization for @c ArgoProtocol type and while +/// it semantically belongs to @c IrArgoAC class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +template<> +uint16_t IRArgoACBase::getRawByteLength(const ArgoProtocol&, + argoIrMessageType_t messageType) { + if (messageType == argoIrMessageType_t::IFEEL_TEMP_REPORT) { + return kArgoShortStateLength; + } + return kArgoStateLength; +} + + +/// @brief Get actual raw state byte length for the current state +/// @param raw The raw state +/// @param _ 2nd param ignored (1st byte of @c raw is sufficient to get len) +/// @return Actual length of state (in bytes) +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +template<> +uint16_t IRArgoACBase::getRawByteLength( + const ArgoProtocolWREM3& raw, argoIrMessageType_t) { + return IRArgoAC_WREM3::getStateLengthForIrMsgType( + IRArgoAC_WREM3::getMessageType(raw)); +} + + +/// @brief Get actual raw state byte length for the current state +/// @return Actual length of state (in bytes) +template +uint16_t IRArgoACBase::getRawByteLength() const { + return getRawByteLength(_, _messageType); +} + + +/// Calculate the checksum for a given state (WREM-2). +/// @note This is a full specialization for @c ArgoProtocol type and while +/// it semantically belongs to @c IrArgoAC class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @warning This does NOT calculate 'short' (iFeel) message checksums +/// @param[in] state The array to calculate the checksum for. /// @param[in] length The size of the state. -/// @return A boolean indicating if it's checksum is valid. -uint8_t IRArgoAC::calcChecksum(const uint8_t state[], const uint16_t length) { +/// @return The 8-bit calculated result. +template<> +uint8_t IRArgoACBase::calcChecksum(const uint8_t state[], + const uint16_t length) { // Corresponds to byte 11 being constant 0b01 // Only add up bytes to 9. byte 10 is 0b01 constant anyway. // Assume that argo array is MSB first (left) return sumBytes(state, length - 2, 2); } -/// Verify the checksum is valid for a given state. -/// @param[in] state The array to verify the checksum of. + +/// Calculate the checksum for a given state (WREM-3). +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @param[in] state The array to calculate the checksum for. /// @param[in] length The size of the state. -/// @return A boolean indicating if it's checksum is valid. -bool IRArgoAC::validChecksum(const uint8_t state[], const uint16_t length) { - return ((state[length - 2] >> 2) + (state[length - 1] << 6)) == - IRArgoAC::calcChecksum(state, length); +/// @return The 8-bit calculated result. +template<> +uint8_t IRArgoACBase::calcChecksum(const uint8_t state[], + const uint16_t length) { + if (length < 1) { + return -1; // Nothing to compute on + } + + auto payloadSizeBits = (length - 1) * 8; // Last byte carries checksum + + auto msgType = getMessageType(state, length); + if (msgType == argoIrMessageType_t::IFEEL_TEMP_REPORT) { + payloadSizeBits += 5; // For WREM3::iFeel the checksum is 3-bit + } else if (msgType == argoIrMessageType_t::TIMER_COMMAND) { + payloadSizeBits += 3; // For WREM3::Timer the checksum is 5-bit + } // Otherwise: full 8-bit checksum + + uint8_t checksum = sumBytes(state, payloadSizeBits / 8, 0); + + // Add stray bits from last byte to the checksum (if any) + const uint8_t maskPayload = 0xFF >> (8 - (payloadSizeBits % 8)); + checksum += (state[length-1] & maskPayload); + + const uint8_t maskChecksum = 0xFF >> (payloadSizeBits % 8); + return checksum & maskChecksum; } -/// Update the checksum for a given state. + +/// Update the checksum for a given state (WREM2). +/// @note This is a full specialization for @c ArgoProtocol type and while +/// it semantically belongs to @c IrArgoAC class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @warning This impl does not support short message format (iFeel) /// @param[in,out] state Pointer to a binary representation of the A/C state. -void IRArgoAC::_checksum(ArgoProtocol *state) { - uint8_t sum = IRArgoAC::calcChecksum(state->raw, kArgoStateLength); +template<> +void IRArgoACBase::_checksum(ArgoProtocol *state) { + uint8_t sum = calcChecksum(state->raw, kArgoStateLength); // Append sum to end of array // Set const part of checksum bit 10 state->Post = kArgoPost; state->Sum = sum; } + +/// @brief Update the checksum for a given state (WREM3). +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @param[in,out] state Pointer to a binary representation of the A/C state. +template<> +void IRArgoACBase::_checksum(ArgoProtocolWREM3 *state) { + auto msgType = IRArgoAC_WREM3::getMessageType(*state); + + uint8_t sum = calcChecksum(state->raw, getRawByteLength(*state)); + switch (msgType) { + case argoIrMessageType_t::IFEEL_TEMP_REPORT: + state->CheckHi = sum; + break; + case argoIrMessageType_t::TIMER_COMMAND: + state->timer.Checksum = sum; + break; + case argoIrMessageType_t::CONFIG_PARAM_SET: + state->config.Checksum = sum; + break; + case argoIrMessageType_t::AC_CONTROL: + default: + state->Sum = sum; + break; + } +} + + /// Update the checksum for the internal state. -void IRArgoAC::checksum(void) { _checksum(&_); } +template +void IRArgoACBase::checksum(void) { _checksum(&_); } + /// Reset the given state to a known good state. +/// @note This is a full specialization for @c ArgoProtocol type and while +/// it semantically belongs to @c IrArgoAC class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function /// @param[in,out] state Pointer to a binary representation of the A/C state. -void IRArgoAC::_stateReset(ArgoProtocol *state) { +/// @param _ 2nd param unused (always resets to AC_CONTROL state) +template<> +void IRArgoACBase::_stateReset(ArgoProtocol *state, + argoIrMessageType_t) { for (uint8_t i = 2; i < kArgoStateLength; i++) state->raw[i] = 0x0; state->Pre1 = kArgoPreamble1; // LSB first (as sent) 0b00110101; - state->Pre2 = kArgoPreamble2; // LSB first: 0b10101111; //const preamble + state->Pre2 = kArgoPreamble2; // LSB first: 0b10101111; state->Post = kArgoPost; } -/// Reset the internals of the object to a known good state. -void IRArgoAC::stateReset(void) { - _stateReset(&_); - off(); - setTemp(20); - setRoomTemp(25); - setMode(kArgoAuto); - setFan(kArgoFanAuto); - _length = kArgoStateLength; + +/// Reset the given state to a known good state +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @param[in,out] state Pointer to a binary representation of the A/C state. +/// @param messageType Type of message to reset the state for +template<> +void IRArgoACBase::_stateReset(ArgoProtocolWREM3 *state, + argoIrMessageType_t messageType) { + for (uint8_t i = 1; i < sizeof(state->raw) / sizeof(state->raw[0]); i++) { + state->raw[i] = 0x0; + } + state->Pre1 = kArgoWrem3Preamble; // LSB first (as sent) 0b00110101; + state->IrChannel = 0; + state->IrCommandType = static_cast(messageType); + + if (messageType == argoIrMessageType_t::TIMER_COMMAND) { + state->timer.Post1 = kArgoWrem3Postfix_Timer; // 0b1 + } else if (messageType == argoIrMessageType_t::AC_CONTROL) { + state->Post1 = kArgoWrem3Postfix_ACControl; // 0b110000 + } +} + + +/// @brief Reset the internals of the object to a known good state. +/// @param messageType Type of message to reset the state for +template +void IRArgoACBase::stateReset(argoIrMessageType_t messageType) { + _stateReset(&_, messageType); + if (messageType == argoIrMessageType_t::AC_CONTROL) { + off(); + setTemp(20); + setRoomTemp(25); + setMode(argoMode_t::AUTO); + setFan(argoFan_t::FAN_AUTO); + } + _messageType = messageType; + _length = getStateLengthForIrMsgType(_messageType); } + +/// @brief Retrieve the checksum value from transmitted state +/// @note This is a full specialization for @c ArgoProtocol type and while +/// it semantically belongs to @c IrArgoAC class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @param[in] state Raw state +/// @param length Length of @c state in bytes +/// @return Checksum value (8-bit) +template<> +uint8_t IRArgoACBase::getChecksum(const uint8_t state[], + const uint16_t length) { + if (length < 1) { + return -1; + } + return (state[length - 2] >> 2) + (state[length - 1] << 6); +} + + +/// @brief Retrieve the checksum value from transmitted state +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @param[in] state Raw state +/// @param length Length of @c state in bytes +/// @return Checksum value (up to 8-bit) +template<> +uint8_t IRArgoACBase::getChecksum(const uint8_t state[], + const uint16_t length) { + if (length < 1) { + return -1; + } + auto msgType = getMessageType(state, length); + if (msgType == argoIrMessageType_t::IFEEL_TEMP_REPORT) { + return (state[length - 1] & 0b11100000) >> 5; + } + if (msgType == argoIrMessageType_t::TIMER_COMMAND) { + return state[length - 1] >> 3; + } + return (state[length - 1]); +} + + +/// Verify the checksum is valid for a given state. +/// @param[in] state The array to verify the checksum of. +/// @param[in] length The size of the state. +/// @return A boolean indicating if it's checksum is valid. +template +bool IRArgoACBase::validChecksum(const uint8_t state[], + const uint16_t length) { + return (getChecksum(state, length) == calcChecksum(state, length)); +} + + +#if SEND_ARGO +/// Send the current internal state as an IR message. +/// @param[in] repeat Nr. of times the message will be repeated. +template +void IRArgoACBase::send(const uint16_t repeat) { + _irsend.sendArgo(getRaw(), getRawByteLength(), repeat); +} + +/// Send the current internal state as an IR message. +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @param[in] repeat Nr. of times the message will be repeated. +template<> +void IRArgoACBase::send(const uint16_t repeat) { + _irsend.sendArgoWREM3(getRaw(), getRawByteLength(), repeat); +} + + +/// Send current room temperature for the iFeel feature as a silent IR +/// message (no acknowledgement from the device) (WREM2) +/// @param[in] degrees The temperature in degrees celsius. +/// @param[in] repeat Nr. of times the message will be repeated. +void IRArgoAC::sendSensorTemp(const uint8_t degrees, const uint16_t repeat) { + const uint8_t temp = std::max(std::min(degrees, kArgoMaxRoomTemp), + kArgoTempDelta) - kArgoTempDelta; + const uint8_t check = kArgoSensorCheck + temp; + + ArgoProtocol data; + _stateReset(&data, argoIrMessageType_t::IFEEL_TEMP_REPORT); + data.SensorT = temp; + data.CheckHi = check >> 5; + data.CheckLo = check; + data.Fixed = kArgoSensorFixed; + _checksum(&data); + auto msgLen = getRawByteLength(data, argoIrMessageType_t::IFEEL_TEMP_REPORT); + + _irsend.sendArgo(data.raw, msgLen, repeat); +} + +/// Send current room temperature for the iFeel feature as a silent IR +/// message (no acknowledgement from the device) (WREM3) +/// @param[in] degrees The temperature in degrees celsius. +/// @param[in] repeat Nr. of times the message will be repeated. +void IRArgoAC_WREM3::sendSensorTemp(const uint8_t degrees, + const uint16_t repeat) { + const uint8_t temp = std::max(std::min(degrees, kArgoMaxRoomTemp), + kArgoTempDelta) - kArgoTempDelta; + ArgoProtocolWREM3 data = {}; + _stateReset(&data, argoIrMessageType_t::IFEEL_TEMP_REPORT); + data.SensorT = temp; + _checksum(&data); + auto msgLen = getRawByteLength(data, argoIrMessageType_t::IFEEL_TEMP_REPORT); + _irsend.sendArgoWREM3(data.raw, msgLen, repeat); +} +#endif + + /// Get the raw state of the object, suitable to be sent with the appropriate /// IRsend object method. /// @return A PTR to the internal state. -uint8_t* IRArgoAC::getRaw(void) { +template +uint8_t* IRArgoACBase::getRaw(void) { checksum(); // Ensure correct bit array before returning return _.raw; } + /// Set the raw state of the object. /// @param[in] state The raw state from the native IR message. /// @param[in] length The length of raw state in bytes. -void IRArgoAC::setRaw(const uint8_t state[], const uint16_t length) { +template +void IRArgoACBase::setRaw(const uint8_t state[], const uint16_t length) { std::memcpy(_.raw, state, length); + _messageType = getMessageType(state, length); _length = length; } /// Set the internal state to have the power on. -void IRArgoAC::on(void) { setPower(true); } +template +void IRArgoACBase::on(void) { setPower(true); } /// Set the internal state to have the power off. -void IRArgoAC::off(void) { setPower(false); } +template +void IRArgoACBase::off(void) { setPower(false); } /// Set the internal state to have the desired power. /// @param[in] on The desired power state. -void IRArgoAC::setPower(const bool on) { +/// @note This is a full specialization for @c ArgoProtocol type and while +/// it semantically belongs to @c IrArgoAC class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +template<> +void IRArgoACBase::setPower(const bool on) { _.Power = on; } +/// @brief Set the internal state to have the desired power. +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @param[in] on The desired power state. +template<> +void IRArgoACBase::setPower(const bool on) { + if (_messageType == argoIrMessageType_t::TIMER_COMMAND) { + _.timer.IsOn = on; + } else { + _.Power = on; + } +} + +/// Get the power setting from the internal state. +/// @note This is a full specialization for @c ArgoProtocol type and while +/// it semantically belongs to @c IrArgoAC class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @return A boolean indicating the power setting. +template<> +bool IRArgoACBase::getPower(void) const { return _.Power; } + /// Get the power setting from the internal state. +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function /// @return A boolean indicating the power setting. -bool IRArgoAC::getPower(void) const { return _.Power; } +template<> +bool IRArgoACBase::getPower(void) const { + if (_messageType == argoIrMessageType_t::TIMER_COMMAND) { + return _.timer.IsOn; + } + return _.Power; +} /// Control the current Max setting. (i.e. Turbo) /// @param[in] on The desired setting. -void IRArgoAC::setMax(const bool on) { +template +void IRArgoACBase::setMax(const bool on) { _.Max = on; } /// Is the Max (i.e. Turbo) setting on? /// @return The current value. -bool IRArgoAC::getMax(void) const { return _.Max; } +template +bool IRArgoACBase::getMax(void) const { return _.Max; } /// Set the temperature. /// @param[in] degrees The temperature in degrees celsius. /// @note Sending 0 equals +4 -void IRArgoAC::setTemp(const uint8_t degrees) { +template +void IRArgoACBase::setTemp(const uint8_t degrees) { uint8_t temp = std::max(kArgoMinTemp, degrees); // delta 4 degrees. "If I want 12 degrees, I need to send 8" temp = std::min(kArgoMaxTemp, temp) - kArgoTempDelta; // mask out bits - // argo[13] & 0x00000100; // mask out ON/OFF Bit _.Temp = temp; } /// Get the current temperature setting. /// @return The current setting for temp. in degrees celsius. -uint8_t IRArgoAC::getTemp(void) const { +template +uint8_t IRArgoACBase::getTemp(void) const { return _.Temp + kArgoTempDelta; } -/// Get the current sensor temperature setting. +/// Get the current sensor temperature setting (iFeel report). +/// @note This is different from @c getRoomTemp() as it retrieves iFeel report +/// explicitly (won't take AC command reported temperature) +/// ? Does it have any use ? /// @return The current setting for the sensor. in degrees celsius. -uint8_t IRArgoAC::getSensorTemp(void) const { - return (_length == kArgoShortStateLength) ? _.SensorT + kArgoTempDelta : 0; +template +uint8_t IRArgoACBase::getSensorTemp(void) const { + return (_messageType == argoIrMessageType_t::IFEEL_TEMP_REPORT) ? + _.SensorT + kArgoTempDelta : 0; +} + + +/// @brief Get the current fan mode setting as a strongly typed value (WREM2). +/// @note This is a full specialization for @c ArgoProtocol type and while +/// it semantically belongs to @c IrArgoAC class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @return The current fan mode. +template<> +argoFan_t IRArgoACBase::getFanEx(void) const { + switch (_.Fan) { + case kArgoFan3: + return argoFan_t::FAN_HIGHEST; + case kArgoFan2: + return argoFan_t::FAN_MEDIUM; + case kArgoFan1: + return argoFan_t::FAN_LOWEST; + case kArgoFanAuto: + return argoFan_t::FAN_AUTO; + default: + return static_cast(_.Fan); + } +} + +/// @brief Get the current fan mode setting as a strongly typed value (WREM3). +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @return The current fan mode. +template<> +argoFan_t IRArgoACBase::getFanEx(void) const { + return static_cast(_.Fan); +} + +/// Set the desired fan mode (WREM2). +/// @note This is a full specialization for @c ArgoProtocol type and while +/// it semantically belongs to @c IrArgoAC class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @param[in] fan The desired fan speed. +/// @note Only a subset of fan speeds are supported (1|2|3|Auto) +template<> +void IRArgoACBase::setFan(argoFan_t fan) { + switch (fan) { + case argoFan_t::FAN_AUTO: + _.Fan = kArgoFanAuto; + break; + case argoFan_t::FAN_HIGHEST: + case argoFan_t::FAN_HIGH: + _.Fan = kArgoFan3; + break; + case argoFan_t::FAN_MEDIUM: + case argoFan_t::FAN_LOW: + _.Fan = kArgoFan2; + break; + case argoFan_t::FAN_LOWER: + case argoFan_t::FAN_LOWEST: + _.Fan = kArgoFan1; + break; + default: + auto raw_value = static_cast(fan); // 2-bit value, per def. + if ((raw_value & 0b11) == raw_value) { + // Outside of known value range, but matches field length + // Let's assume the caller knows what they're doing and pass it through + _.Fan = raw_value; + } else { + _.Fan = kArgoFanAuto; + } + break; + } +} + +/// Set the desired fan mode (WREM3). +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @param[in] mode The desired fan speed. +template<> +void IRArgoACBase::setFan(argoFan_t fan) { + switch (fan) { + case argoFan_t::FAN_AUTO: + case argoFan_t::FAN_HIGHEST: + case argoFan_t::FAN_HIGH: + case argoFan_t::FAN_MEDIUM: + case argoFan_t::FAN_LOW: + case argoFan_t::FAN_LOWER: + case argoFan_t::FAN_LOWEST: + _.Fan = static_cast(fan); + break; + default: + _.Fan = static_cast(argoFan_t::FAN_AUTO); + break; + } } /// Set the speed of the fan. +/// @deprecated /// @param[in] fan The desired setting. void IRArgoAC::setFan(const uint8_t fan) { _.Fan = std::min(fan, kArgoFan3); } /// Get the current fan speed setting. +/// @deprecated /// @return The current fan speed. uint8_t IRArgoAC::getFan(void) const { return _.Fan; } -/// Set the flap position. i.e. Swing. +/// @brief Get Flap (VSwing) value as a strongly-typed value +/// @note This @c getFlapEx() method has been introduced to be able to retain +/// old implementation of @c getFlap() for @c IRArgoAc which used uint8_t +/// @return Flap setting +template +argoFlap_t IRArgoACBase::getFlapEx(void) const { + return static_cast(_.Flap); +} + +/// Set the desired flap mode +/// @param[in] mode The desired flap mode. +template +void IRArgoACBase::setFlap(argoFlap_t flap) { + auto raw_value = static_cast(flap); + if ((raw_value & 0b111) == raw_value) { + // Outside of known value range, but matches field length + // Let's assume the caller knows what they're doing and pass it through + _.Flap = raw_value; + } else { + _.Flap = static_cast(argoFlap_t::FLAP_AUTO); + } +} + +/// Set the flap position. i.e. Swing. (WREM2) /// @warning Not yet working! +/// @deprecated /// @param[in] flap The desired setting. void IRArgoAC::setFlap(const uint8_t flap) { + setFlap(static_cast(flap)); flap_mode = flap; // TODO(kaschmo): set correct bits for flap mode } -/// Get the flap position. i.e. Swing. +/// Get the flap position. i.e. Swing. (WREM2) /// @warning Not yet working! +/// @deprecated /// @return The current flap setting. uint8_t IRArgoAC::getFlap(void) const { return flap_mode; } /// Get the current operation mode setting. +/// @note This is a full specialization for @c ArgoProtocol type and while +/// it semantically belongs to @c IrArgoAC class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @return The current operation mode. +/// @note This @c getModeEx() method has been introduced to be able to retain +/// old implementation of @c getMode() for @c IRArgoAc which used uint8_t +template<> +argoMode_t IRArgoACBase::getModeEx(void) const { + switch (_.Mode) { + case kArgoCool: + return argoMode_t::COOL; + case kArgoDry: + return argoMode_t::DRY; + case kArgoAuto: + return argoMode_t::AUTO; + case kArgoHeat: + return argoMode_t::HEAT; + case kArgoOff: // Modelling "FAN" as "OFF", for the lack of better constant + return argoMode_t::FAN; + case kArgoHeatAuto: + default: + return static_cast(_.Mode); + } +} + +/// Get the current operation mode setting. +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function. /// @return The current operation mode. -uint8_t IRArgoAC::getMode(void) const { - return _.Mode; +/// @note This @c getModeEx() method has been introduced to be able to retain +/// old implementation of @c getMode() for @c IRArgoAc which used uint8_t +template<> +argoMode_t IRArgoACBase::getModeEx(void) const { + return static_cast(_.Mode); } /// Set the desired operation mode. /// @param[in] mode The desired operation mode. -void IRArgoAC::setMode(const uint8_t mode) { +/// @note This is a full specialization for @c ArgoProtocol type and while +/// it semantically belongs to @c IrArgoAC class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +template<> +void IRArgoACBase::setMode(argoMode_t mode) { + switch (mode) { + case argoMode_t::COOL: + _.Mode = static_cast(kArgoCool); + break; + case argoMode_t::DRY: + _.Mode = static_cast(kArgoDry); + break; + case argoMode_t::HEAT: + _.Mode = static_cast(kArgoHeat); + break; + case argoMode_t::FAN: + _.Mode = static_cast(kArgoOff); + break; + case argoMode_t::AUTO: + _.Mode = static_cast(kArgoAuto); + break; + default: + auto raw_value = static_cast(mode); + if ((raw_value & 0b111) == raw_value) { + // Outside of known value range, but matches field length + // Let's assume the caller knows what they're doing and pass it through + _.Mode = raw_value; + } else { + _.Mode = static_cast(kArgoAuto);; + } + break; + } +} + +/// @brief Set the desired operation mode. +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @param[in] mode The desired operation mode. +template<> +void IRArgoACBase::setMode(argoMode_t mode) { + switch (mode) { + case argoMode_t::COOL: + case argoMode_t::DRY: + case argoMode_t::HEAT: + case argoMode_t::FAN: + case argoMode_t::AUTO: + _.Mode = static_cast(mode); + break; + default: + _.Mode = static_cast(argoMode_t::AUTO); + break; + } +} + +/// @brief Set the desired operation mode. +/// @deprecated +/// @param mode The desired operation mode. +void IRArgoAC::setMode(uint8_t mode) { switch (mode) { case kArgoCool: case kArgoDry: @@ -246,118 +887,306 @@ void IRArgoAC::setMode(const uint8_t mode) { case kArgoHeat: case kArgoHeatAuto: _.Mode = mode; - return; + break; default: _.Mode = kArgoAuto; + break; } } +/// @brief Get the current operation mode +/// @deprecated +/// @return The current operation mode +uint8_t IRArgoAC::getMode() const { return _.Mode;} + +argoFan_t IRArgoAC_WREM3::getFan(void) const { return getFanEx(); } +argoFlap_t IRArgoAC_WREM3::getFlap(void) const { return getFlapEx(); } +argoMode_t IRArgoAC_WREM3::getMode(void) const { return getModeEx(); } + /// Turn on/off the Night mode. i.e. Sleep. /// @param[in] on The desired setting. -void IRArgoAC::setNight(const bool on) { - _.Night = on; -} +template +void IRArgoACBase::setNight(const bool on) { _.Night = on; } /// Get the status of Night mode. i.e. Sleep. /// @return true if on, false if off. -bool IRArgoAC::getNight(void) const { return _.Night; } +template +bool IRArgoACBase::getNight(void) const { return _.Night; } -/// Turn on/off the iFeel mode. +/// @brief Turn on/off the Economy mode (lowered power mode) /// @param[in] on The desired setting. -void IRArgoAC::setiFeel(const bool on) { - _.iFeel = on; +void IRArgoAC_WREM3::setEco(const bool on) { _.Eco = on; } + +/// @brief Get the status of Economy function +/// @return true if on, false if off. +bool IRArgoAC_WREM3::getEco(void) const { return _.Eco; } + +/// @brief Turn on/off the Filter mode (not supported by Argo Ulisse) +/// @param[in] on The desired setting. +void IRArgoAC_WREM3::setFilter(const bool on) { _.Filter = on; } + +/// @brief Get status of the filter function +/// @return true if on, false if off. +bool IRArgoAC_WREM3::getFilter(void) const { return _.Filter; } + +/// @brief Turn on/off the device Lights (LED) +/// @param[in] on The desired setting. +void IRArgoAC_WREM3::setLight(const bool on) { _.Light = on; } + +/// @brief Get status of device lights +/// @return true if on, false if off. +bool IRArgoAC_WREM3::getLight(void) const { return _.Light; } + +/// @brief Set the IR channel on which to communicate +/// @param[in] channel The desired IR channel. +void IRArgoAC_WREM3::setChannel(const uint8_t channel) { + _.IrChannel = std::min(channel, kArgoMaxChannel); } +/// @brief Get the currently set transmission channel +/// @return Channel number +uint8_t IRArgoAC_WREM3::getChannel(void) const { return _.IrChannel;} + +/// @brief Set the config data to send +/// Valid only for @c argoIrMessageType_t::CONFIG_PARAM_SET message +/// @param paramId The param ID +/// @param value The value of the parameter +void IRArgoAC_WREM3::setConfigEntry(const uint8_t paramId, + const uint8_t value) { + _.config.Key = paramId; + _.config.Value = value; +} + +/// @brief Get the config entry previously set +/// @return Key->value pair (paramID: value) +std::pair IRArgoAC_WREM3::getConfigEntry(void) const { + return std::make_pair(_.config.Key, _.config.Value); +} + +/// Turn on/off the iFeel mode. +/// @param[in] on The desired setting. +template +void IRArgoACBase::setiFeel(const bool on) { _.iFeel = on; } + /// Get the status of iFeel mode. /// @return true if on, false if off. -bool IRArgoAC::getiFeel(void) const { return _.iFeel; } +template +bool IRArgoACBase::getiFeel(void) const { return _.iFeel; } -/// Set the time for the A/C -/// @warning Not yet working! -void IRArgoAC::setTime(void) { - // TODO(kaschmo): use function call from checksum to set time first +/// @brief Set the message type of the next command (setting this resets state) +/// @param msgType The message type to set +template +void IRArgoACBase::setMessageType(const argoIrMessageType_t msgType) { + stateReset(msgType); +} + +/// @brief Get the message type +/// @return Message type currently set +template +argoIrMessageType_t IRArgoACBase::getMessageType(void) const { + return _messageType; } /// Set the value for the current room temperature. +/// @note Depending on message type - this will set `sensor` or `roomTemp` value /// @param[in] degrees The temperature in degrees celsius. -void IRArgoAC::setRoomTemp(const uint8_t degrees) { +template +void IRArgoACBase::setRoomTemp(const uint8_t degrees) { uint8_t temp = std::min(degrees, kArgoMaxRoomTemp); temp = std::max(temp, kArgoTempDelta) - kArgoTempDelta; - _.RoomTemp = temp; + if (getMessageType() == argoIrMessageType_t::IFEEL_TEMP_REPORT) { + _.SensorT = temp; + } else { + _.RoomTemp = temp; + } } /// Get the currently stored value for the room temperature setting. +/// @note Depending on message type - this will get `sensor` or `roomTemp` value /// @return The current setting for the room temp. in degrees celsius. -uint8_t IRArgoAC::getRoomTemp(void) const { +template +uint8_t IRArgoACBase::getRoomTemp(void) const { + if (getMessageType() == argoIrMessageType_t::IFEEL_TEMP_REPORT) { + return _.SensorT + kArgoTempDelta; + } return _.RoomTemp + kArgoTempDelta; } +/// @brief Convert a stdAc::ac_command_t enum into its native message type. +/// @param command The enum to be converted. +/// @return The native equivalent of the enum. +template +argoIrMessageType_t IRArgoACBase::convertCommand( + const stdAc::ac_command_t command) { + switch (command) { + case stdAc::ac_command_t::kTemperatureReport: + return argoIrMessageType_t::IFEEL_TEMP_REPORT; + case stdAc::ac_command_t::kTimerCommand: + return argoIrMessageType_t::TIMER_COMMAND; + case stdAc::ac_command_t::kConfigCommand: + return argoIrMessageType_t::CONFIG_PARAM_SET; + case stdAc::ac_command_t::kControlCommand: + default: + return argoIrMessageType_t::AC_CONTROL; + } +} + /// 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 IRArgoAC::convertMode(const stdAc::opmode_t mode) { +template +argoMode_t IRArgoACBase::convertMode(const stdAc::opmode_t mode) { switch (mode) { case stdAc::opmode_t::kCool: - return kArgoCool; + return argoMode_t::COOL; case stdAc::opmode_t::kHeat: - return kArgoHeat; + return argoMode_t::HEAT; case stdAc::opmode_t::kDry: - return kArgoDry; - case stdAc::opmode_t::kOff: - return kArgoOff; - // No fan mode. - default: - return kArgoAuto; + return argoMode_t::DRY; + case stdAc::opmode_t::kFan: + return argoMode_t::FAN; + case stdAc::opmode_t::kAuto: + default: // No off mode. + return argoMode_t::AUTO; } } /// 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 IRArgoAC::convertFan(const stdAc::fanspeed_t speed) { +template +argoFan_t IRArgoACBase::convertFan(const stdAc::fanspeed_t speed) { switch (speed) { case stdAc::fanspeed_t::kMin: + return argoFan_t::FAN_LOWEST; case stdAc::fanspeed_t::kLow: - return kArgoFan1; + return argoFan_t::FAN_LOWER; case stdAc::fanspeed_t::kMedium: - return kArgoFan2; + return argoFan_t::FAN_LOW; + case stdAc::fanspeed_t::kMediumHigh: + return argoFan_t::FAN_MEDIUM; case stdAc::fanspeed_t::kHigh: + return argoFan_t::FAN_HIGH; case stdAc::fanspeed_t::kMax: - return kArgoFan3; + return argoFan_t::FAN_HIGHEST; default: - return kArgoFanAuto; + return argoFan_t::FAN_AUTO; } } /// Convert a stdAc::swingv_t enum into it's native setting. /// @param[in] position The enum to be converted. /// @return The native equivalent of the enum. -uint8_t IRArgoAC::convertSwingV(const stdAc::swingv_t position) { +template +argoFlap_t IRArgoACBase::convertSwingV(const stdAc::swingv_t position) { switch (position) { case stdAc::swingv_t::kHighest: - return kArgoFlapFull; + return argoFlap_t::FLAP_1; case stdAc::swingv_t::kHigh: - return kArgoFlap5; + return argoFlap_t::FLAP_2; + case stdAc::swingv_t::kUpperMiddle: + return argoFlap_t::FLAP_3; case stdAc::swingv_t::kMiddle: - return kArgoFlap4; + return argoFlap_t::FLAP_4; case stdAc::swingv_t::kLow: - return kArgoFlap3; + return argoFlap_t::FLAP_5; case stdAc::swingv_t::kLowest: - return kArgoFlap1; + return argoFlap_t::FLAP_6; + case stdAc::swingv_t::kOff: // This is abusing the semantics quite a bit + return argoFlap_t::FLAP_FULL; + case stdAc::swingv_t::kAuto: default: - return kArgoFlapAuto; + return argoFlap_t::FLAP_AUTO; + } +} + + +/// Convert a native flap mode into its stdAc equivalent (WREM2). +/// @note This is a full specialization for @c ArgoProtocol type and while +/// it semantically belongs to @c IrArgoAC class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @param[in] position The native setting to be converted. +/// @return The stdAc equivalent of the native setting. +template<> +stdAc::swingv_t IRArgoACBase::toCommonSwingV( + const uint8_t position) { + switch (position) { + case kArgoFlapFull: + return stdAc::swingv_t::kHighest; + case kArgoFlap5: + return stdAc::swingv_t::kHigh; + case kArgoFlap4: + return stdAc::swingv_t::kMiddle; + case kArgoFlap3: + return stdAc::swingv_t::kLow; + case kArgoFlap1: + return stdAc::swingv_t::kLowest; + default: + return stdAc::swingv_t::kAuto; + } +} + +/// Convert a native flap mode into its stdAc equivalent (WREM3). +/// @note This is a full specialization for @c ArgoProtocolWREM3 type and while +/// it semantically belongs to @c IrArgoAC_WREM3 class impl., it has *not* +/// been pushed there, to avoid having to use a virtual function +/// @param[in] position The native setting to be converted. +/// @return The stdAc equivalent of the native setting. +template<> +stdAc::swingv_t IRArgoACBase::toCommonSwingV( + const uint8_t position) { + switch (static_cast(position)) { + case argoFlap_t::FLAP_FULL: + return stdAc::swingv_t::kOff; + case argoFlap_t::FLAP_6: + return stdAc::swingv_t::kHighest; + case argoFlap_t::FLAP_5: + return stdAc::swingv_t::kHigh; + case argoFlap_t::FLAP_4: + return stdAc::swingv_t::kUpperMiddle; + case argoFlap_t::FLAP_3: + return stdAc::swingv_t::kMiddle; + case argoFlap_t::FLAP_2: + return stdAc::swingv_t::kLow; + case argoFlap_t::FLAP_1: + return stdAc::swingv_t::kLowest; + case argoFlap_t::FLAP_AUTO: + default: + return stdAc::swingv_t::kAuto; + } +} + +/// Convert a native message type into its stdAc equivalent. +/// @param[in] mode The native setting to be converted. +/// @return The stdAc equivalent of the native setting. +template +stdAc::ac_command_t IRArgoACBase::toCommonCommand( + const argoIrMessageType_t command) { + switch (command) { + case argoIrMessageType_t::AC_CONTROL: + return stdAc::ac_command_t::kControlCommand; + case argoIrMessageType_t::IFEEL_TEMP_REPORT: + return stdAc::ac_command_t::kTemperatureReport; + case argoIrMessageType_t::TIMER_COMMAND: + return stdAc::ac_command_t::kTimerCommand; + case argoIrMessageType_t::CONFIG_PARAM_SET: + return stdAc::ac_command_t::kConfigCommand; + default: + return stdAc::ac_command_t::kControlCommand; } } /// 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 IRArgoAC::toCommonMode(const uint8_t mode) { +template +stdAc::opmode_t IRArgoACBase::toCommonMode(const argoMode_t mode) { switch (mode) { - case kArgoCool: return stdAc::opmode_t::kCool; - case kArgoHeat: return stdAc::opmode_t::kHeat; - case kArgoDry: return stdAc::opmode_t::kDry; - // No fan mode. + case argoMode_t::COOL: return stdAc::opmode_t::kCool; + case argoMode_t::DRY : return stdAc::opmode_t::kDry; + case argoMode_t::FAN : return stdAc::opmode_t::kFan; + case argoMode_t::HEAT : return stdAc::opmode_t::kHeat; + case argoMode_t::AUTO : return stdAc::opmode_t::kAuto; default: return stdAc::opmode_t::kAuto; } } @@ -365,11 +1194,16 @@ stdAc::opmode_t IRArgoAC::toCommonMode(const uint8_t mode) { /// 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 IRArgoAC::toCommonFanSpeed(const uint8_t speed) { +template +stdAc::fanspeed_t IRArgoACBase::toCommonFanSpeed(const argoFan_t speed) { switch (speed) { - case kArgoFan3: return stdAc::fanspeed_t::kMax; - case kArgoFan2: return stdAc::fanspeed_t::kMedium; - case kArgoFan1: return stdAc::fanspeed_t::kMin; + case argoFan_t::FAN_AUTO: return stdAc::fanspeed_t::kAuto; + case argoFan_t::FAN_HIGHEST: return stdAc::fanspeed_t::kMax; + case argoFan_t::FAN_HIGH: return stdAc::fanspeed_t::kHigh; + case argoFan_t::FAN_MEDIUM: return stdAc::fanspeed_t::kMediumHigh; + case argoFan_t::FAN_LOW: return stdAc::fanspeed_t::kMedium; + case argoFan_t::FAN_LOWER: return stdAc::fanspeed_t::kLow; + case argoFan_t::FAN_LOWEST: return stdAc::fanspeed_t::kMin; default: return stdAc::fanspeed_t::kAuto; } } @@ -379,15 +1213,18 @@ stdAc::fanspeed_t IRArgoAC::toCommonFanSpeed(const uint8_t speed) { stdAc::state_t IRArgoAC::toCommon(void) const { stdAc::state_t result{}; result.protocol = decode_type_t::ARGO; + result.model = argo_ac_remote_model_t::SAC_WREM2; + result.command = toCommonCommand(_messageType); result.power = _.Power; - result.mode = toCommonMode(_.Mode); + result.mode = toCommonMode(getModeEx()); result.celsius = true; result.degrees = getTemp(); - result.fanspeed = toCommonFanSpeed(_.Fan); + result.fanspeed = toCommonFanSpeed(getFanEx()); result.turbo = _.Max; result.sleep = _.Night ? 0 : -1; + result.iFeel = getiFeel(); + result.roomTemperature = getRoomTemp(); // Not supported. - result.model = -1; // Not supported. result.swingv = stdAc::swingv_t::kOff; result.swingh = stdAc::swingh_t::kOff; result.light = false; @@ -400,16 +1237,68 @@ stdAc::state_t IRArgoAC::toCommon(void) const { return result; } -/// Convert the current internal state into a human readable string. +/// Convert the current internal state into its stdAc::state_t equivalent. +/// @return The stdAc equivalent of the native settings. +stdAc::state_t IRArgoAC_WREM3::toCommon(void) const { + stdAc::state_t result{}; + result.protocol = decode_type_t::ARGO; + result.model = argo_ac_remote_model_t::SAC_WREM3; + result.command = toCommonCommand(_messageType); + result.power = getPower(); + result.mode = toCommonMode(getModeEx()); + result.celsius = true; + result.degrees = getTemp(); + result.fanspeed = toCommonFanSpeed(getFanEx()); + result.turbo = _.Max; + result.swingv = toCommonSwingV(_.Flap); + result.light = getLight(); + result.filter = getFilter(); + result.econo = getEco(); + result.quiet = getNight(); + result.beep = (_messageType != argoIrMessageType_t::IFEEL_TEMP_REPORT); + + result.clock = -1; + result.sleep = _.Night ? 0 : -1; + if (_messageType == argoIrMessageType_t::TIMER_COMMAND) { + result.clock = getCurrentTimeMinutes(); + result.sleep = getDelayTimerMinutes(); + } + result.iFeel = getiFeel(); + result.roomTemperature = getRoomTemp(); + + // Not supported. + result.swingh = stdAc::swingh_t::kOff; + result.clean = false; + + return result; +} + + +namespace { + /// @brief Short-hand for casting enum to its underlying storage type + /// @tparam E The type of enum + /// @param e Enum value + /// @return Type of underlying value + template + constexpr typename std::underlying_type::type to_underlying(E e) noexcept { + return static_cast::type>(e); + } +} + +/// Convert the current internal state into a human readable string (WREM2). /// @return A human readable string. String IRArgoAC::toString(void) const { String result = ""; - result.reserve(100); // Reserve some heap for the string to reduce fragging. - if (_length == kArgoShortStateLength) { - result += addIntToString(getSensorTemp(), kSensorTempStr, false); + result.reserve(118); // Reserve some heap for the string to reduce fragging. + // E.g.: Model: 1 (WREM2), Power: On, Mode: 0 (Cool), Fan: 0 (Auto), + // Temp: 20C, Room Temp: 21C, Max: On, IFeel: On, Night: On + result += addModelToString(decode_type_t::ARGO, + argo_ac_remote_model_t::SAC_WREM2, false); + if (_messageType == argoIrMessageType_t::IFEEL_TEMP_REPORT) { + result += addIntToString(getSensorTemp(), kSensorTempStr); result += 'C'; } else { - result += addBoolToString(_.Power, kPowerStr, false); + result += addBoolToString(_.Power, kPowerStr); result += addIntToString(_.Mode, kModeStr); result += kSpaceLBraceStr; switch (_.Mode) { @@ -468,8 +1357,364 @@ String IRArgoAC::toString(void) const { return result; } +/// @brief Set current clock (as minutes, counted from 0:00) +/// E.g. 13:38 becomes 818 (13*60+38) +/// @param currentTimeMinutes Current time (in minutes) +void IRArgoAC_WREM3::setCurrentTimeMinutes(uint16_t currentTimeMinutes) { + uint16_t time = std::min(currentTimeMinutes, static_cast(23*60+59)); + _.timer.CurrentTimeHi = (time >> 4); + _.timer.CurrentTimeLo = (time & 0b1111); +} + +/// @brief Retrieve current time +/// @return Current time as minutes from 0:00 +uint16_t IRArgoAC_WREM3::getCurrentTimeMinutes(void) const { + return (_.timer.CurrentTimeHi << 4) + _.timer.CurrentTimeLo; +} + +/// @brief Set current day of week +/// @param dayOfWeek Current day of week +void IRArgoAC_WREM3::setCurrentDayOfWeek(argoWeekday dayOfWeek) { + auto day = std::min(to_underlying(dayOfWeek), + to_underlying(argoWeekday::SATURDAY)); + _.timer.CurrentWeekdayHi = (day >> 1); + _.timer.CurrentWeekdayLo = (day & 0b1); +} + +/// @brief Get current day of week +/// @return Current day of week +argoWeekday IRArgoAC_WREM3::getCurrentDayOfWeek(void) const { + return static_cast((_.timer.CurrentWeekdayHi << 1) + + _.timer.CurrentWeekdayLo); +} + +/// @brief Set timer type +/// @param timerType Timer type to use OFF | DELAY | SCHEDULE<1|2|3> +/// @note 2 timer types supported: delay | schedule timer +/// - @c DELAY_TIMER requires setting @c setDelayTimerMinutes +/// and @c setCurrentTimeMinutes and (optionally) @c setCurrentDayOfWeek +/// - @c SCHEDULE_TIMER requires setting: +/// @c setScheduleTimerStartMinutes +/// @c setScheduleTimerStopMinutes +/// @c setScheduleTimerActiveDays +/// as well as current time *and* day +/// @c setCurrentTimeMinutes and @c setCurrentDayOfWeek +void IRArgoAC_WREM3::setTimerType(const argoTimerType_t timerType) { + if (timerType > argoTimerType_t::SCHEDULE_TIMER_3) { + _.timer.TimerType = to_underlying(argoTimerType_t::NO_TIMER); + } else { + _.timer.TimerType = to_underlying(timerType); + } +} + +/// @brief Get currently set timer type +/// @return Timer type +argoTimerType_t IRArgoAC_WREM3::getTimerType(void) const { + return static_cast(_.timer.TimerType); +} + +/// @brief Set delay timer delay in minutes (10-minute increments only) +/// Max is 1190 (19h50m) +/// @note The delay timer also accepts current device state: set by @c setPower +/// @param delayMinutes Delay minutes +void IRArgoAC_WREM3::setDelayTimerMinutes(const uint16_t delayMinutes) { + const uint16_t DELAY_TIMER_MAX = 19*60+50; + uint16_t time = std::min(delayMinutes, DELAY_TIMER_MAX); + + // only full 10 minute increments are allowed + time = static_cast((time / 10.0) + 0.5) * 10; + + _.timer.DelayTimeHi = (time >> 6); + _.timer.DelayTimeLo = (time & 0b111111); +} + +/// @brief Get current delay timer value +/// @return Delay timer value (in minutes) +uint16_t IRArgoAC_WREM3::getDelayTimerMinutes(void) const { + return (_.timer.DelayTimeHi << 6) + _.timer.DelayTimeLo; +} + +/// @brief Set schedule timer on time (time when the device should turn on) +/// (10-minute increments only) +/// @param startTimeMinutes Time when the device should turn itself on +/// expressed as # of minutes counted from 0:00 +/// The value is in 10-minute increments (rounded) +/// E.g. 13:38 becomes 820 (13:40 in minutes) +void IRArgoAC_WREM3::setScheduleTimerStartMinutes( + const uint16_t startTimeMinutes) { + const uint16_t SCHEDULE_TIMER_MAX = 23*60+50; + uint16_t time = std::min(startTimeMinutes, SCHEDULE_TIMER_MAX); + + // only full 10 minute increments are allowed + time = static_cast((time / 10.0) + 0.5) * 10; + + _.timer.TimerStartHi = (time >> 3); + _.timer.TimerStartLo = (time & 0b111); +} + +/// @brief Get schedule timer ON time +/// @return Schedule on time (as # of minutes from 0:00) +uint16_t IRArgoAC_WREM3::getScheduleTimerStartMinutes(void) const { + return (_.timer.TimerStartHi << 3) + _.timer.TimerStartLo; +} + +/// @brief Set schedule timer off time (time when the device should turn off) +/// (10-minute increments only) +/// @param stopTimeMinutes Time when the device should turn itself off +/// expressed as # of minutes counted from 0:00 +/// The value is in 10-minute increments (rounded) +/// E.g. 13:38 becomes 820 (13:40 in minutes) +void IRArgoAC_WREM3::setScheduleTimerStopMinutes( + const uint16_t stopTimeMinutes) { + const uint16_t SCHEDULE_TIMER_MAX = 23*60+50; + uint16_t time = std::min(stopTimeMinutes, SCHEDULE_TIMER_MAX); + + // only full 10 minute increments are allowed + time = static_cast((time / 10.0) + 0.5) * 10; + + _.timer.TimerEndHi = (time >> 8); + _.timer.TimerEndLo = (time & 0b11111111); +} + +/// @brief Get schedule timer OFF time +/// @return Schedule off time (as # of minutes from 0:00) +uint16_t IRArgoAC_WREM3::getScheduleTimerStopMinutes(void) const { + return (_.timer.TimerEndHi << 8) + _.timer.TimerEndLo; +} + +/// @brief Get the days when shedule timer shall be active (as bitmap) +/// @return Days when schedule timer is active, as raw bitmap type +/// where bit[0] is Sunday, bit[1] -> Monday, ... +uint8_t IRArgoAC_WREM3::getTimerActiveDaysBitmap(void) const { + return (_.timer.TimerActiveDaysHi << 5) + _.timer.TimerActiveDaysLo; +} + +/// @brief Set the days when the schedule timer shall be active +/// @param days A set of days when the timer shall run +void IRArgoAC_WREM3::setScheduleTimerActiveDays( + const std::set& days) { + uint8_t daysBitmap = 0; + for (const auto& day : days) { + daysBitmap |= (0b1 << to_underlying(day)); + } + _.timer.TimerActiveDaysHi = (daysBitmap >> 5); + _.timer.TimerActiveDaysLo = (daysBitmap & 0b11111); +} + +/// @brief Get the days when shedule timer shall be active (as set) +/// @return Days when the schedule timer runs +std::set IRArgoAC_WREM3::getScheduleTimerActiveDays(void) const { + std::set result = {}; + uint8_t daysBitmap = getTimerActiveDaysBitmap(); + for (uint8_t i = to_underlying(argoWeekday::SUNDAY); + i <= to_underlying(argoWeekday::SATURDAY); + ++i) { + if (((daysBitmap >> i) & 0b1) == 0b1) { + result.insert(static_cast(i)); + } + } + return result; +} + +/// @brief Get device model +/// @return Device model +argo_ac_remote_model_t IRArgoAC_WREM3::getModel() const { + return argo_ac_remote_model_t::SAC_WREM3; +} + +namespace { + /// @brief Helper function to convert timer active days to a string + /// @param days The active days bitmap + /// @return `|`-delimited representation of active days (3-letter day repr.) + String dayBitmaskToString(std::set days) { + String result = ""; + result.reserve(29); // [Sun|Mon|Tue|Wed|Thu|Fri|Sat] + + for (const auto& day : days) { + if (result.length() > 0) { + result += "|"; + } + result += irutils::dayToString(to_underlying(day)); + } + return result; + } + + /// @brief Get string representation of a timer type + /// @param timerType Timer type to represent + /// @return String representation + String timerTypeToString(argoTimerType_t timerType) { + String result = ""; + result.reserve(13); // "2 (Schedule1)" + result += uint64ToString(to_underlying(timerType)); + result += kSpaceLBraceStr; + + switch (timerType) { + case argoTimerType_t::NO_TIMER: + result += kOffStr; + break; + case argoTimerType_t::DELAY_TIMER: + result += kSleepTimerStr; + break; + case argoTimerType_t::SCHEDULE_TIMER_1: + result += kScheduleStr; + result += '1'; + break; + case argoTimerType_t::SCHEDULE_TIMER_2: + result += kScheduleStr; + result += '2'; + break; + case argoTimerType_t::SCHEDULE_TIMER_3: + result += kScheduleStr; + result += '3'; + break; + default: + result += kUnknownStr; + break; + } + return result + ')'; + } + + String channelToString(uint8_t channel) { + String result = ""; + result.reserve(6); // "[CH#4]" + result += "["; + result += kChStr; + result += uint64ToString(channel); + result += "]"; + return result; + } + + String commandTypeToString(argoIrMessageType_t type, uint8_t channel) { + String result = ""; + result.reserve(19); // "Sensor Temp[CH#0]: " + switch (type) { + case argoIrMessageType_t::AC_CONTROL: + result += kCommandStr; + break; + case argoIrMessageType_t::IFEEL_TEMP_REPORT: + result += kSensorTempStr; + break; + case argoIrMessageType_t::TIMER_COMMAND: + result += kTimerStr; + break; + case argoIrMessageType_t::CONFIG_PARAM_SET: + result += kConfigCommandStr; + break; + default: + result += kUnknownStr; + } + result += channelToString(channel); + result += kColonSpaceStr; + return result; + } +} // namespace + +/// Convert the current internal state into a human readable string (WREM3). +/// @return A human readable string. +String IRArgoAC_WREM3::toString(void) const { + String result = ""; + result.reserve(190); // Reserve some heap for the string to reduce fragging. + // E.g.: Command[CH#0]: Model: 2 (WREM3), Power: On, Mode: 1 (Cool), + // Temp: 22C, Room: 26C, Fan: 0 (Auto), Swing(V): 7 (Breeze), + // IFeel: Off, Night: Off, Econo: Off, Max: Off, Filter: Off, Light: On + // Temp: 20C, Room Temp: 21C, Max: On, IFeel: On, Night: On + + auto commandType = this->getMessageType(); + argo_ac_remote_model_t model = getModel(); + + result += commandTypeToString(commandType, getChannel()); + result += addModelToString(decode_type_t::ARGO, model, false); + + switch (commandType) { + case argoIrMessageType_t::IFEEL_TEMP_REPORT: + result += addTempToString(getSensorTemp(), true, true, true); + break; + + case argoIrMessageType_t::AC_CONTROL: + result += addBoolToString(getPower(), kPowerStr); + result += addModeToString(to_underlying(getModeEx()), + to_underlying(argoMode_t::AUTO), + to_underlying(argoMode_t::COOL), + to_underlying(argoMode_t::HEAT), + to_underlying(argoMode_t::DRY), + to_underlying(argoMode_t::FAN)); + result += addTempToString(getTemp()); + result += addTempToString(getRoomTemp(), true, true, true); + result += addFanToString(to_underlying(getFanEx()), + to_underlying(argoFan_t::FAN_HIGH), + to_underlying(argoFan_t::FAN_LOWER), + to_underlying(argoFan_t::FAN_AUTO), + to_underlying(argoFan_t::FAN_LOWEST), + to_underlying(argoFan_t::FAN_LOW), + to_underlying(argoFan_t::FAN_HIGHEST), + to_underlying(argoFan_t::FAN_MEDIUM)); + result += addSwingVToString(to_underlying(getFlapEx()), + to_underlying(argoFlap_t::FLAP_AUTO), + to_underlying(argoFlap_t::FLAP_1), + to_underlying(argoFlap_t::FLAP_2), + to_underlying(argoFlap_t::FLAP_3), + to_underlying(argoFlap_t::FLAP_4), -1, + to_underlying(argoFlap_t::FLAP_5), + to_underlying(argoFlap_t::FLAP_6), -1, -1, + to_underlying(argoFlap_t::FLAP_FULL), -1); + result += addBoolToString(getiFeel(), kIFeelStr); + result += addBoolToString(getNight(), kNightStr); + result += addBoolToString(getEco(), kEconoStr); + result += addBoolToString(getMax(), kMaxStr); // Turbo + result += addBoolToString(getFilter(), kFilterStr); + result += addBoolToString(getLight(), kLightStr); + break; + + case argoIrMessageType_t::TIMER_COMMAND: + result += addBoolToString(_.timer.IsOn, kPowerStr); + result += addLabeledString(timerTypeToString(getTimerType()), + kTimerModeStr); + result += addLabeledString(minsToString(getCurrentTimeMinutes()), + kClockStr); + result += addDayToString(to_underlying(getCurrentDayOfWeek())); + switch (getTimerType()) { + case argoTimerType_t::NO_TIMER: + result += addLabeledString(kOffStr, kTimerStr); + break; + case argoTimerType_t::DELAY_TIMER: + result += addLabeledString(minsToString(getDelayTimerMinutes()), + kTimerStr); + break; + default: + result += addLabeledString(minsToString(getScheduleTimerStartMinutes()), + kOnTimerStr); + result += addLabeledString(minsToString(getScheduleTimerStopMinutes()), + kOffTimerStr); + result += addLabeledString(dayBitmaskToString( + getScheduleTimerActiveDays()), kTimerActiveDaysStr); + break; + } + break; + + case argoIrMessageType_t::CONFIG_PARAM_SET: + result += addIntToString(_.config.Key, kKeyStr); + result += addIntToString(_.config.Value, kValueStr); + break; + } + + return result; +} + +/// @brief Check if raw ARGO state starts with valid WREM3 preamble +/// @param state The state bytes +/// @param length Length of state in bytes +/// @return True if state starts wiht valid WREM3 preamble, False otherwise +bool IRArgoAC_WREM3::hasValidPreamble(const uint8_t state[], + const uint16_t length) { + if (length < 1) { + return false; + } + auto preamble = state[0] & 0x0F; + return preamble == kArgoWrem3Preamble; +} + #if DECODE_ARGO -/// Decode the supplied Argo message. +/// Decode the supplied Argo message (WREM2). /// Status: BETA / Probably works. /// @param[in,out] results Ptr to the data to decode & where to store the decode /// result. @@ -496,7 +1741,9 @@ bool IRrecv::decodeArgo(decode_results *results, uint16_t offset, // Compliance // Verify we got a valid checksum. - if (strict && !IRArgoAC::validChecksum(results->state)) return false; + if (strict && !IRArgoAC::validChecksum(results->state, kArgoStateLength)) { + return false; + } // Success results->decode_type = decode_type_t::ARGO; results->bits = nbits; @@ -505,4 +1752,98 @@ bool IRrecv::decodeArgo(decode_results *results, uint16_t offset, // is a union data type. return true; } + +/// Decode the supplied Argo message (WREM3). +/// Status: Confirmed working w/ Argo 13 ECO (WREM-3) +/// @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. +/// @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. +/// @note This decoder is separate from @c decodeArgo to maintain backwards +/// compatibility. Contrary to WREM2, this expects a footer and gap! +bool IRrecv::decodeArgoWREM3(decode_results *results, uint16_t offset, + const uint16_t nbits, + const bool strict) { + if (strict + && nbits != kArgo3AcControlStateLength * 8 + && nbits != kArgo3ConfigStateLength * 8 + && nbits != kArgo3iFeelReportStateLength * 8 + && nbits != kArgo3TimerStateLength * 8) { + return false; + } + + const uint16_t kArgoOverhead = 3; + uint16_t bytesRead = matchGeneric(results->rawbuf + offset, results->state, + results->rawlen - offset, nbits, + kArgoHdrMark, kArgoHdrSpace, + kArgoBitMark, kArgoOneSpace, + kArgoBitMark, kArgoZeroSpace, + kArgoBitMark, kArgoGap, // difference vs decodeArgo + true, _tolerance, kArgoOverhead, + false); + if (!bytesRead || + !IRArgoAC_WREM3::isValidWrem3Message(results->state, nbits, strict)) { + return false; + } + + // Success: Matched ARGO protocol and WREM3-model + // Note that unfortunately decode_type does not allow to persist model... + // so we will be re-detecting it later :) + results->decode_type = decode_type_t::ARGO; + results->bits = nbits; + // No need to record the state as we stored it as we decoded it. + // As we use result->state, we don't record value, address, or command as it + // is a union data type. + return true; +} + +/// @brief Detects if an ARGO protocol message is a WREM-3 sub-type (model) +/// @param state The raw IR decore state +/// @param nbits The length of @c state **IN BITS** +/// @param strict Whether to perform strict matching (incl. checksum) +/// @return True if the message is a WREM-3 one +bool IRArgoAC_WREM3::isValidWrem3Message(const uint8_t state[], + const uint16_t nbits, bool strict) { + auto stateLength = std::min(static_cast(std::ceil(nbits / 8.0)), + kStateSizeMax); + + if (strict && !IRArgoAC_WREM3::hasValidPreamble(state, stateLength)) { + return false; + } + + auto messageType = IRArgoACBase::getMessageType(state, + stateLength); + + switch (messageType) { + case argoIrMessageType_t::AC_CONTROL : + if (nbits != kArgo3AcControlStateLength * 8) { return false; } + break; + case argoIrMessageType_t::CONFIG_PARAM_SET: + if (nbits != kArgo3ConfigStateLength * 8) { return false; } + break; + case argoIrMessageType_t::TIMER_COMMAND: + if (nbits != kArgo3TimerStateLength * 8) { return false; } + break; + case argoIrMessageType_t::IFEEL_TEMP_REPORT: + if (nbits != kArgo3iFeelReportStateLength * 8) { return false; } + break; + default: + return false; + } + + // Compliance: Verify we got a valid checksum. + if (strict && !IRArgoAC_WREM3::validChecksum(state, stateLength)) { + return false; + } + return true; +} + #endif // DECODE_ARGO + + +// force template instantiation +template class IRArgoACBase; +template class IRArgoACBase; diff --git a/src/ir_Argo.h b/src/ir_Argo.h index e0ead8e26..e0d1decce 100644 --- a/src/ir_Argo.h +++ b/src/ir_Argo.h @@ -1,14 +1,18 @@ // Copyright 2017 Schmolders // Copyright 2022 crankyoldgit +// Copyright 2022 Mateusz Bronk (mbronk) /// @file /// @brief Support for Argo Ulisse 13 DCI Mobile Split ACs. // Supports: -// Brand: Argo, Model: Ulisse 13 DCI Mobile Split A/C +// Brand: Argo, Model: Ulisse 13 DCI Mobile Split A/C [WREM2 remote] +// Brand: Argo, Model: Ulisse Eco Mobile Split A/C (Wifi) [WREM3 remote] #ifndef IR_ARGO_H_ #define IR_ARGO_H_ +#include +#include #ifndef UNIT_TEST #include #endif @@ -21,7 +25,7 @@ // ARGO Ulisse DCI -/// Native representation of a Argo A/C message. +/// Native representation of a Argo A/C message for WREM-2 remote. union ArgoProtocol { uint8_t raw[kArgoStateLength]; ///< The state in native IR code form struct { @@ -74,15 +78,127 @@ union ArgoProtocol { }; }; -// Constants. Store MSB left. +/// Native representation of A/C IR message for WREM-3 remote +/// @note The remote sends 4 different IR command types, varying in length +/// and methods of checksum calculation +/// - [0b00] Regular A/C command (change operation mode) - 6-byte +/// - [0b01] iFeel Temperature report - 2-byte +/// - [0b10] Timer command - 9-byte +/// - [0b11] Config command - 4-byte +/// @note The 1st 2 structures are unnamed for compat. with @c ArgoProtocol +/// 1st byte definition is a header common across all commands though +union ArgoProtocolWREM3 { + uint8_t raw[kArgoStateLength]; ///< The state in native IR code form + struct { + // Byte 0 (same definition across the union) + uint8_t Pre1 :4; /// Preamble: 0b1011 @ref kArgoWrem3Preamble + uint8_t IrChannel :2; /// 0..3 range + uint8_t IrCommandType :2; /// @ref argoIrMessageType_t + // Byte 1 + uint8_t RoomTemp :5; // in Celsius, range: 4..35 (offset by -4[*C]) + uint8_t Mode :3; /// @ref argoMode_t + // Byte 2 + uint8_t Temp :5; // in Celsius, range: 10..32 (offset by -4[*C]) + uint8_t Fan :3; /// @ref argoFan_t + // Byte3 + uint8_t Flap :3; /// SwingV @ref argoFlap_t + uint8_t Power :1; + uint8_t iFeel :1; + uint8_t Night :1; + uint8_t Eco :1; + uint8_t Max :1; ///< a.k.a. Turbo + // Byte4 + uint8_t Filter :1; + uint8_t Light :1; + uint8_t Post1 :6; /// Unknown, always 0b110000 (TempScale?) + // Byte5 + uint8_t Sum :8; /// Checksum + }; + struct { + // Byte 0 (same definition across the union) + uint8_t :8; // {Pre1 | IrChannel | IrCommandType} + // Byte 1 + uint8_t SensorT :5; // in Celsius, range: 4..35 (offset by -4[*C]) + uint8_t CheckHi :3; // Checksum (short) + }; + struct Timer { + // Byte 0 (same definition across the union) + uint8_t : 8; // {Pre1 | IrChannel | IrCommandType} + // Byte 1 + uint8_t IsOn : 1; + uint8_t TimerType : 3; + uint8_t CurrentTimeLo : 4; + // Byte 2 + uint8_t CurrentTimeHi : 7; + uint8_t CurrentWeekdayLo : 1; + // Byte 3 + uint8_t CurrentWeekdayHi : 2; + uint8_t DelayTimeLo : 6; + // Byte 4 + uint8_t DelayTimeHi : 5; + uint8_t TimerStartLo : 3; + // Byte 5 + uint8_t TimerStartHi : 8; + // Byte 6 + uint8_t TimerEndLo : 8; + // Byte 7 + uint8_t TimerEndHi : 3; + uint8_t TimerActiveDaysLo : 5; // Bitmap (LSBit is Sunday) + // Byte 8 + uint8_t TimerActiveDaysHi : 2; // Bitmap (LSBit is Sunday) + uint8_t Post1 : 1; // Unknown, always 1 + uint8_t Checksum : 5; + } timer; + struct Config { + uint8_t :8; // Byte 0 {Pre1 | IrChannel | IrCommandType} + uint8_t Key :8; // Byte 1 + uint8_t Value :8; // Byte 2 + uint8_t Checksum :8; // Byte 3 + } config; +}; + +// Constants (WREM-2). Store MSB left. +const uint8_t kArgoHeatBit = 0b00100000; +const uint8_t kArgoPreamble1 = 0b10101100; +const uint8_t kArgoPreamble2 = 0b11110101; +const uint8_t kArgoPost = 0b00000010; + +// Constants (generic) +const uint16_t kArgoFrequency = 38000; // Hz +// Temp +const uint8_t kArgoTempDelta = 4; +const uint8_t kArgoMaxRoomTemp = 35; // Celsius +const uint8_t kArgoMinTemp = 10; // Celsius delta +4 +const uint8_t kArgoMaxTemp = 32; // Celsius +const uint8_t kArgoMaxChannel = 3; + -const uint8_t kArgoHeatBit = 0b00100000; +/// @brief IR message type (determines the payload part of IR command) +/// @note Raw values match WREM-3 protocol, but the enum is used in generic +/// context +/// @note WREM-3 remote supports all commands separately, whereas +/// WREM-2 (allegedly) only has the @c AC_CONTROL and @c IFEEL_TEMP_REPORT +/// (timers are part of @c AC_CONTROL command), and there's no config. +enum class argoIrMessageType_t : uint8_t { + AC_CONTROL = 0b00, + IFEEL_TEMP_REPORT = 0b01, + TIMER_COMMAND = 0b10, // WREM-3 only (WREM-2 has it under AC_CONTROL) + CONFIG_PARAM_SET = 0b11 // WREM-3 only +}; -const uint8_t kArgoPreamble1 = 0b10101100; -const uint8_t kArgoPreamble2 = 0b11110101; -const uint8_t kArgoPost = 0b00000010; +/// @brief A/C operation mode +/// @note Raw values match WREM-3 protocol, but the enum is used in generic +/// context +enum class argoMode_t : uint8_t { + COOL = 0b001, + DRY = 0b010, + HEAT = 0b011, + FAN = 0b100, + AUTO = 0b101 +}; -// Mode 0b00111000 +// Raw mode definitions for WREM-2 remote +// (not wraped into a ns nor enum for backwards-compat.) const uint8_t kArgoCool = 0b000; const uint8_t kArgoDry = 0b001; const uint8_t kArgoAuto = 0b010; @@ -92,19 +208,42 @@ const uint8_t kArgoHeatAuto = 0b101; // ?no idea what mode that is const uint8_t kArgoHeatBlink = 0b110; -// Fan 0b00011000 +/// @brief Fan speed +/// @note Raw values match WREM-3 protocol, but the enum is used in generic +/// context +enum class argoFan_t : uint8_t { + FAN_AUTO = 0b000, + FAN_LOWEST = 0b001, + FAN_LOWER = 0b010, + FAN_LOW = 0b011, + FAN_MEDIUM = 0b100, + FAN_HIGH = 0b101, + FAN_HIGHEST = 0b110 +}; + +// Raw fan speed definitions for WREM-2 remote +// (not wraped into a ns nor enum for backwards-compat.) const uint8_t kArgoFanAuto = 0; // 0b00 const uint8_t kArgoFan1 = 1; // 0b01 const uint8_t kArgoFan2 = 2; // 0b10 const uint8_t kArgoFan3 = 3; // 0b11 -// Temp -const uint8_t kArgoTempDelta = 4; -const uint8_t kArgoMaxRoomTemp = 35; // Celsius -const uint8_t kArgoMinTemp = 10; // Celsius delta +4 -const uint8_t kArgoMaxTemp = 32; // Celsius +/// @brief Flap position (swing-V) +/// @note Raw values match WREM-3 protocol, but the enum is used in generic +/// context +enum class argoFlap_t : uint8_t { + FLAP_AUTO = 0, + FLAP_1 = 1, // Highest + FLAP_2 = 2, + FLAP_3 = 3, + FLAP_4 = 4, + FLAP_5 = 5, + FLAP_6 = 6, // Lowest + FLAP_FULL = 7 +}; -// Flap/SwingV +// Raw Flap/SwingV definitions for WREM-2 remote +// (not wraped into a ns nor enum for backwards-compat.) const uint8_t kArgoFlapAuto = 0; const uint8_t kArgoFlap1 = 1; const uint8_t kArgoFlap2 = 2; @@ -138,22 +277,61 @@ const uint8_t kArgoFlapFull = 7; #define ARGO_FLAP_FULL kArgoFlapFull -/// Class for handling detailed Argo A/C messages. -class IRArgoAC { +/// @brief Timer type to set (for @c argoIrMessageType_t::TIMER_COMMAND) +/// @note Raw values match WREM-3 protocol +enum class argoTimerType_t : uint8_t { + NO_TIMER = 0b000, + DELAY_TIMER = 0b001, + SCHEDULE_TIMER_1 = 0b010, + SCHEDULE_TIMER_2 = 0b011, + SCHEDULE_TIMER_3 = 0b100 +}; + +/// @brief Day type to set (for @c argoIrMessageType_t::TIMER_COMMAND) +/// @note Raw values match WREM-3 protocol +enum class argoWeekday : uint8_t { + SUNDAY = 0b000, + MONDAY = 0b001, + TUESDAY = 0b010, + WEDNESDAY = 0b011, + THURSDAY = 0b100, + FRIDAY = 0b101, + SATURDAY = 0b110 +}; + + + +/// @brief Base class for handling *common* support for Argo remote protocols +/// (functionality is shared across WREM-2 and WREM-3 IR protocols) +/// @note This class uses static polymorphism and full template specializations +/// when required, to avoid a performance penalty of doing v-table lookup. +/// 2 instantiations are forced in impl. file: for @c ArgoProtocol and +/// @c ArgoProtocolWREM3 +/// @note This class is abstract (though does not declare a pure-virtual fn. +/// for abovementioned reasons), and instead declares protected c-tor +/// @tparam ARGO_PROTOCOL_T The Raw device protocol/message used +template +class IRArgoACBase { +#ifndef UNIT_TEST // A less cloggy way of expressing FRIEND_TEST(...) + + protected: +#else + public: - explicit IRArgoAC(const uint16_t pin, const bool inverted = false, +#endif + explicit IRArgoACBase(const uint16_t pin, const bool inverted = false, const bool use_modulation = true); + public: #if SEND_ARGO void send(const uint16_t repeat = kArgoDefaultRepeat); - void sendSensorTemp(const uint8_t degrees, - const uint16_t repeat = kArgoDefaultRepeat); /// 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_ARGO + void begin(void); void on(void); void off(void); @@ -164,16 +342,21 @@ class IRArgoAC { void setTemp(const uint8_t degrees); uint8_t getTemp(void) const; + void setRoomTemp(const uint8_t degrees); + uint8_t getRoomTemp(void) const; uint8_t getSensorTemp(void) const; - void setFan(const uint8_t fan); - uint8_t getFan(void) const; + void setFan(const argoFan_t fan); + void setFanEx(const argoFan_t fan) { setFan(fan); } + argoFan_t getFanEx(void) const; ///< `-Ex` for backw. compat w/ @c IRArgoAC - void setFlap(const uint8_t flap); - uint8_t getFlap(void) const; + void setFlap(const argoFlap_t flap); + void setFlapEx(const argoFlap_t flap) { setFlap(flap); } + argoFlap_t getFlapEx(void) const; ///< `-Ex` for backw. compat w/ @c IRArgoAC - void setMode(const uint8_t mode); - uint8_t getMode(void) const; + void setMode(const argoMode_t mode); + void setModeEx(const argoMode_t mode) { setMode(mode); } + argoMode_t getModeEx(void) const; ///< `-Ex` for backw. compat w/ @c IRArgoAC void setMax(const bool on); bool getMax(void) const; @@ -184,44 +367,160 @@ class IRArgoAC { void setiFeel(const bool on); bool getiFeel(void) const; - void setTime(void); - void setRoomTemp(const uint8_t degrees); - uint8_t getRoomTemp(void) const; + void setMessageType(const argoIrMessageType_t msgType); + argoIrMessageType_t getMessageType(void) const; + static argoIrMessageType_t getMessageType(const uint8_t state[], + const uint16_t length); uint8_t* getRaw(void); - void setRaw(const uint8_t state[], const uint16_t length = kArgoStateLength); - static uint8_t calcChecksum(const uint8_t state[], - const uint16_t length = kArgoStateLength); - static bool validChecksum(const uint8_t state[], - const uint16_t length = kArgoStateLength); - static uint8_t convertMode(const stdAc::opmode_t mode); - static uint8_t convertFan(const stdAc::fanspeed_t speed); - static uint8_t convertSwingV(const stdAc::swingv_t position); - 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; + uint16_t getRawByteLength() const; + static uint16_t getStateLengthForIrMsgType(argoIrMessageType_t type); + void setRaw(const uint8_t state[], const uint16_t length); + + static bool validChecksum(const uint8_t state[], const uint16_t length); + + static argoMode_t convertMode(const stdAc::opmode_t mode); + static argoFan_t convertFan(const stdAc::fanspeed_t speed); + static argoFlap_t convertSwingV(const stdAc::swingv_t position); + static argoIrMessageType_t convertCommand(const stdAc::ac_command_t command); + + protected: + void _stateReset(ARGO_PROTOCOL_T *state, argoIrMessageType_t messageType + = argoIrMessageType_t::AC_CONTROL); + void stateReset(argoIrMessageType_t messageType + = argoIrMessageType_t::AC_CONTROL); + void _checksum(ARGO_PROTOCOL_T *state); + void checksum(void); + static uint16_t getRawByteLength(const ARGO_PROTOCOL_T& raw, + argoIrMessageType_t messageTypeHint = argoIrMessageType_t::AC_CONTROL); + static uint8_t calcChecksum(const uint8_t state[], const uint16_t length); + static uint8_t getChecksum(const uint8_t state[], const uint16_t length); + + static stdAc::opmode_t toCommonMode(const argoMode_t mode); + static stdAc::fanspeed_t toCommonFanSpeed(const argoFan_t speed); + static stdAc::swingv_t toCommonSwingV(const uint8_t position); + static stdAc::ac_command_t toCommonCommand(const argoIrMessageType_t command); + + // Attributes + ARGO_PROTOCOL_T _; ///< The raw protocol data + uint16_t _length = kArgoStateLength; + argoIrMessageType_t _messageType = argoIrMessageType_t::AC_CONTROL; + #ifndef UNIT_TEST - private: + protected: IRsend _irsend; ///< instance of the IR send class #else + + public: /// @cond IGNORE IRsendTest _irsend; ///< instance of the testing IR send class /// @endcond #endif - // # of bytes per command - ArgoProtocol _; - void _stateReset(ArgoProtocol *state); - void stateReset(void); - void _checksum(ArgoProtocol *state); - void checksum(void); +}; - // Attributes - uint8_t flap_mode; - uint8_t heat_mode; - uint8_t cool_mode; - uint16_t _length = kArgoStateLength; +/// @brief Supports Argo A/C SAC-WREM2 IR remote protocol +class IRArgoAC : public IRArgoACBase { + public: + explicit IRArgoAC(const uint16_t pin, const bool inverted = false, + const bool use_modulation = true); + + #if SEND_ARGO + void sendSensorTemp(const uint8_t degrees, + const uint16_t repeat = kArgoDefaultRepeat); + #endif // SEND_ARGO + + String toString(void) const; + stdAc::state_t toCommon(void) const; + + using IRArgoACBase::setMode; + void setMode(const uint8_t mode); /// @deprecated, for backwards-compat. + uint8_t getMode(void) const; /// @deprecated, for backwards-compat. + + using IRArgoACBase::setFan; + void setFan(const uint8_t fan); /// @deprecated, for backwards-compat. + uint8_t getFan(void) const; /// @deprecated, for backwards-compat. + + using IRArgoACBase::setFlap; + void setFlap(const uint8_t flap); /// @deprecated, for backwards-compat. + uint8_t getFlap(void) const; /// @deprecated, for backwards-compat. + + private: + // Attributes + uint8_t flap_mode; ///< Unused, remove(?) +}; + +/// @brief Supports Argo A/C SAC-WREM3 IR remote protocol +class IRArgoAC_WREM3 : public IRArgoACBase { + public: + explicit IRArgoAC_WREM3(const uint16_t pin, const bool inverted = false, + const bool use_modulation = true); + + #if SEND_ARGO + void sendSensorTemp(const uint8_t degrees, + const uint16_t repeat = kArgoDefaultRepeat); + #endif // SEND_ARGO + + argo_ac_remote_model_t getModel(void) const; + + + argoFan_t getFan(void) const; + argoFlap_t getFlap(void) const; + argoMode_t getMode(void) const; + + void setEco(const bool on); + bool getEco(void) const; + + void setFilter(const bool on); + bool getFilter(void) const; + + void setLight(const bool on); + bool getLight(void) const; + + void setChannel(const uint8_t channel); + uint8_t getChannel(void) const; + + void setConfigEntry(const uint8_t paramId, const uint8_t value); + std::pair getConfigEntry(void) const; + + void setCurrentTimeMinutes(uint16_t currentTimeMinutes); + uint16_t getCurrentTimeMinutes(void) const; + + void setCurrentDayOfWeek(argoWeekday dayOfWeek); + argoWeekday getCurrentDayOfWeek(void) const; + + void setTimerType(const argoTimerType_t timerType); + argoTimerType_t getTimerType(void) const; + + void setDelayTimerMinutes(const uint16_t delayMinutes); + uint16_t getDelayTimerMinutes(void) const; + + void setScheduleTimerStartMinutes(const uint16_t startTimeMinutes); + uint16_t getScheduleTimerStartMinutes(void) const; + // uint16_t getTimerXStartMinutes(void) const + + void setScheduleTimerStopMinutes(const uint16_t stopTimeMinutes); + uint16_t getScheduleTimerStopMinutes(void) const; + // uint16_t getTimerXStopMinutes(void) const; + + + void setScheduleTimerActiveDays(const std::set& days); + std::set getScheduleTimerActiveDays(void) const; + uint8_t getTimerActiveDaysBitmap(void) const; + + using IRArgoACBase::getMessageType; + static argoIrMessageType_t getMessageType(const ArgoProtocolWREM3& raw); + + String toString(void) const; + stdAc::state_t toCommon(void) const; + + static bool hasValidPreamble(const uint8_t state[], const uint16_t length); + + public: +#if DECODE_ARGO + static bool isValidWrem3Message(const uint8_t state[], const uint16_t nbits, + bool strict); +#endif }; #endif // IR_ARGO_H_ diff --git a/src/locale/defaults.h b/src/locale/defaults.h index d60b9b24c..2e141f4fc 100644 --- a/src/locale/defaults.h +++ b/src/locale/defaults.h @@ -378,6 +378,9 @@ D_STR_INDIRECT " " D_STR_MODE #ifndef D_STR_MEDIUM #define D_STR_MEDIUM "Medium" #endif // D_STR_MEDIUM +#ifndef D_STR_MED_HIGH +#define D_STR_MED_HIGH "Med-high" +#endif // D_STR_MEDIUM #ifndef D_STR_HIGHEST #define D_STR_HIGHEST "Highest" @@ -445,6 +448,39 @@ D_STR_INDIRECT " " D_STR_MODE #ifndef D_STR_BOTTOM #define D_STR_BOTTOM "Bottom" #endif // D_STR_BOTTOM +#ifndef D_STR_UPPER_MIDDLE +#define D_STR_UPPER_MIDDLE "UpperMiddle" +#endif // D_STR_UPPER_MIDDLE +#ifndef D_STR_AC_COMMAND +#define D_STR_AC_COMMAND "A/C Config" +#endif // D_STR_AC_COMMAND +#ifndef D_STR_AC_CONTROL +#define D_STR_AC_CONTROL "A/C Control" +#endif // D_STR_AC_CONTROL +#ifndef D_STR_AC_TEMP_REPORT +#define D_STR_AC_TEMP_REPORT "A/C Temp Report" +#endif // D_STR_AC_TEMP_REPORT +#ifndef D_STR_AC_TIMER +#define D_STR_AC_TIMER "A/C Set Timer" +#endif // D_STR_AC_TIMER +#ifndef D_STR_COMMAND_TYPE +#define D_STR_COMMAND_TYPE "A/C Command" +#endif // D_STR_COMMAND_TYPE +#ifndef D_STR_SCHEDULE +#define D_STR_SCHEDULE "Schedule" +#endif // D_STR_SCHEDULE +#ifndef D_STR_CH +#define D_STR_CH "CH#" +#endif // D_STR_CH +#ifndef D_STR_TIMER_ACTIVE_DAYS +#define D_STR_TIMER_ACTIVE_DAYS "TimerActiveDays" +#endif // D_STR_TIMER_ACTIVE_DAYS +#ifndef D_STR_KEY +#define D_STR_KEY "Key" +#endif // D_STR_KEY +#ifndef D_STR_VALUE +#define D_STR_VALUE "Value" +#endif // D_STR_VALUE // Compound words/phrases/descriptions from pre-defined words. // Note: Obviously these need to be defined *after* their component words. @@ -689,6 +725,12 @@ D_STR_INDIRECT " " D_STR_MODE #ifndef D_STR_DG11J191 #define D_STR_DG11J191 "DG11J191" #endif // D_STR_DG11J191 +#ifndef D_STR_ARGO_WREM2 +#define D_STR_ARGO_WREM2 "WREM2" +#endif // D_STR_ARGO_WREM2 +#ifndef D_STR_ARGO_WREM3 +#define D_STR_ARGO_WREM3 "WREM3" +#endif // D_STR_ARGO_WREM3 // Protocols Names #ifndef D_STR_AIRTON diff --git a/test/ir_Argo_test.cpp b/test/ir_Argo_test.cpp index cb4c24682..52fcac2a3 100644 --- a/test/ir_Argo_test.cpp +++ b/test/ir_Argo_test.cpp @@ -1,13 +1,19 @@ // Copyright 2019 David Conran +// Copyright 2022 Mateusz Bronk (mbronk) #include #include +#include #include "ir_Argo.h" #include "IRac.h" #include "IRrecv.h" #include "IRrecv_test.h" #include "IRsend.h" #include "IRsend_test.h" +#include "./ut_utils.h" +/******************************************************************************/ +/* Tests for toCommon() */ +/******************************************************************************/ TEST(TestArgoACClass, toCommon) { IRArgoAC ac(kGpioUnused); @@ -19,6 +25,7 @@ TEST(TestArgoACClass, toCommon) { ac.setNight(true); // Now test it. ASSERT_EQ(decode_type_t::ARGO, ac.toCommon().protocol); + ASSERT_EQ(stdAc::ac_command_t::kControlCommand, ac.toCommon().command); ASSERT_TRUE(ac.toCommon().power); ASSERT_TRUE(ac.toCommon().celsius); ASSERT_EQ(20, ac.toCommon().degrees); @@ -26,8 +33,8 @@ TEST(TestArgoACClass, toCommon) { ASSERT_EQ(stdAc::fanspeed_t::kMax, ac.toCommon().fanspeed); ASSERT_EQ(0, ac.toCommon().sleep); ASSERT_TRUE(ac.toCommon().turbo); + ASSERT_EQ(argo_ac_remote_model_t::SAC_WREM2, ac.toCommon().model); // Unsupported. - ASSERT_EQ(-1, ac.toCommon().model); ASSERT_EQ(stdAc::swingv_t::kOff, ac.toCommon().swingv); ASSERT_EQ(stdAc::swingh_t::kOff, ac.toCommon().swingh); ASSERT_FALSE(ac.toCommon().econo); @@ -37,8 +44,51 @@ TEST(TestArgoACClass, toCommon) { ASSERT_FALSE(ac.toCommon().beep); ASSERT_FALSE(ac.toCommon().quiet); ASSERT_EQ(-1, ac.toCommon().clock); + ASSERT_FALSE(ac.toCommon().iFeel); + ASSERT_EQ(25, ac.toCommon().roomTemperature); } +TEST(TestArgoAC_WREM3Class, toCommon) { + IRArgoAC_WREM3 ac(kGpioUnused); + ac.setPower(true); + ac.setMode(argoMode_t::COOL); + ac.setTemp(21); + ac.setFan(argoFan_t::FAN_HIGHEST); + ac.setMax(true); + ac.setNight(true); + ac.setFlap(argoFlap_t::FLAP_4); + ac.setEco(true); + ac.setFilter(true); + ac.setLight(true); + ac.setiFeel(true); + // Now test it. + ASSERT_EQ(decode_type_t::ARGO, ac.toCommon().protocol); + ASSERT_EQ(argo_ac_remote_model_t::SAC_WREM3, ac.toCommon().model); + ASSERT_EQ(stdAc::ac_command_t::kControlCommand, ac.toCommon().command); + ASSERT_TRUE(ac.toCommon().celsius); + ASSERT_TRUE(ac.toCommon().beep); // Always on (except for iFeel) + ASSERT_FALSE(ac.toCommon().clean); + ASSERT_EQ(-1, ac.toCommon().clock); + ASSERT_EQ(0, ac.toCommon().sleep); + ASSERT_EQ(stdAc::swingh_t::kOff, ac.toCommon().swingh); + ASSERT_TRUE(ac.toCommon().power); + ASSERT_EQ(stdAc::opmode_t::kCool, ac.toCommon().mode); + ASSERT_EQ(21, ac.toCommon().degrees); + ASSERT_EQ(stdAc::fanspeed_t::kMax, ac.toCommon().fanspeed); + ASSERT_TRUE(ac.toCommon().turbo); + ASSERT_TRUE(ac.toCommon().quiet); // Night + ASSERT_EQ(stdAc::swingv_t::kUpperMiddle, ac.toCommon().swingv); + ASSERT_TRUE(ac.toCommon().econo); + ASSERT_TRUE(ac.toCommon().light); + ASSERT_TRUE(ac.toCommon().filter); + ASSERT_TRUE(ac.toCommon().iFeel); + ASSERT_EQ(25, ac.toCommon().roomTemperature); +} + +/******************************************************************************/ +/* Tests of message construction */ +/******************************************************************************/ + TEST(TestArgoACClass, MessageConstructon) { IRArgoAC ac(kGpioUnused); ac.setPower(true); @@ -57,12 +107,166 @@ TEST(TestArgoACClass, MessageConstructon) { EXPECT_THAT(std::vector(actual, actual + kArgoBits / 8), ::testing::ElementsAreArray(expected)); EXPECT_EQ( - "Power: On, Mode: 0 (Cool), Fan: 0 (Auto), Temp: 20C, Room Temp: 21C, " - "Max: On, IFeel: On, Night: On", + "Model: 1 (WREM2), Power: On, Mode: 0 (Cool), Fan: 0 (Auto), Temp: 20C, " + "Room Temp: 21C, Max: On, IFeel: On, Night: On", + ac.toString()); +} + +TEST(TestArgoAC_WREM3Class, MessageConstructon_ACControl) { + IRArgoAC_WREM3 ac(kGpioUnused); + ac.setChannel(0); + ac.setPower(true); + ac.setMode(argoMode_t::COOL); + ac.setTemp(22); + ac.setRoomTemp(26); + ac.setFan(argoFan_t::FAN_AUTO); + ac.setFlap(argoFlap_t::FLAP_FULL); + ac.setiFeel(false); + ac.setNight(false); + ac.setEco(false); + ac.setMax(false); + ac.setFilter(false); + ac.setLight(true); + auto expected = std::vector({ + 0x0B, 0x36, 0x12, 0x0F, 0xC2, 0x24}); + auto actual = ac.getRaw(); + ASSERT_EQ(ac.getRawByteLength(), kArgo3AcControlStateLength); + EXPECT_THAT(std::vector(actual, actual + ac.getRawByteLength()), + ::testing::ElementsAreArray(expected)); + EXPECT_EQ( + "Command[CH#0]: Model: 2 (WREM3), Power: On, Mode: 1 (Cool), Temp: 22C, " + "Room: 26C, Fan: 0 (Auto), Swing(V): 7 (Breeze), IFeel: Off, Night: Off, " + "Econo: Off, Max: Off, Filter: Off, Light: On", + ac.toString()); +} + +TEST(TestArgoAC_WREM3Class, MessageConstructon_ACControl_2) { + IRArgoAC_WREM3 ac(kGpioUnused); + ac.setChannel(2); + ac.setPower(true); + ac.setMode(argoMode_t::AUTO); + ac.setTemp(23); + ac.setRoomTemp(28); + ac.setFan(argoFan_t::FAN_LOWER); + ac.setMax(true); + ac.setNight(true); + ac.setFlap(argoFlap_t::FLAP_4); + ac.setEco(true); + ac.setFilter(true); + ac.setLight(true); + ac.setiFeel(true); + auto expected = std::vector({ + 0x2B, 0xB8, 0x53, 0xFC, 0xC3, 0xF5}); + auto actual = ac.getRaw(); + ASSERT_EQ(ac.getRawByteLength(), kArgo3AcControlStateLength); + EXPECT_THAT(std::vector(actual, actual + ac.getRawByteLength()), + ::testing::ElementsAreArray(expected)); + EXPECT_EQ( + "Command[CH#2]: Model: 2 (WREM3), Power: On, Mode: 5 (Auto), Temp: 23C, " + "Room: 28C, Fan: 2 (Low), Swing(V): 4 (Middle), IFeel: On, Night: On," + " Econo: On, Max: On, Filter: On, Light: On", + ac.toString()); +} + +TEST(TestArgoAC_WREM3Class, MessageConstructon_iFeelReport) { + IRArgoAC_WREM3 ac(kGpioUnused); + ac.setMessageType(argoIrMessageType_t::IFEEL_TEMP_REPORT); + ac.setRoomTemp(31); + + auto expected = std::vector({0x4B, 0xDB}); + auto actual = ac.getRaw(); + ASSERT_EQ(ac.getRawByteLength(), kArgo3iFeelReportStateLength); + EXPECT_THAT(std::vector(actual, actual + ac.getRawByteLength()), + ::testing::ElementsAreArray(expected)); + EXPECT_EQ( + "Sensor Temp[CH#0]: Model: 2 (WREM3), Room: 31C", + ac.toString()); +} + +TEST(TestArgoAC_WREM3Class, MessageConstructon_Config) { + IRArgoAC_WREM3 ac(kGpioUnused); + ac.setMessageType(argoIrMessageType_t::CONFIG_PARAM_SET); + ac.setConfigEntry(6, 30); + + auto expected = std::vector({0xCB, 0x06, 0x1E, 0xEF}); + auto actual = ac.getRaw(); + ASSERT_EQ(ac.getRawByteLength(), kArgo3ConfigStateLength); + EXPECT_THAT(std::vector(actual, actual + ac.getRawByteLength()), + ::testing::ElementsAreArray(expected)); + EXPECT_EQ( + "A/C Config[CH#0]: Model: 2 (WREM3), Key: 6, Value: 30", + ac.toString()); +} + +TEST(TestArgoAC_WREM3Class, MessageConstructon_NoTimer) { + IRArgoAC_WREM3 ac(kGpioUnused); + ac.setMessageType(argoIrMessageType_t::TIMER_COMMAND); + ac.off(); + ac.setCurrentTimeMinutes(1*60+59); + ac.setCurrentDayOfWeek(argoWeekday::MONDAY); + ac.setTimerType(argoTimerType_t::NO_TIMER); + + auto expected = std::vector({0x8B, 0x70, 0x87, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x34}); + auto actual = ac.getRaw(); + ASSERT_EQ(ac.getRawByteLength(), kArgo3TimerStateLength); + EXPECT_THAT(std::vector(actual, actual + ac.getRawByteLength()), + ::testing::ElementsAreArray(expected)); + EXPECT_EQ( + "Timer[CH#0]: Model: 2 (WREM3), Power: Off, Timer Mode: 0 (Off), " + "Clock: 01:59, Day: 1 (Mon), Timer: Off", + ac.toString()); +} + +TEST(TestArgoAC_WREM3Class, MessageConstructon_DelayTimer) { + IRArgoAC_WREM3 ac(kGpioUnused); + ac.setMessageType(argoIrMessageType_t::TIMER_COMMAND); + ac.off(); + ac.setCurrentTimeMinutes(12*60+00); + ac.setCurrentDayOfWeek(argoWeekday::SATURDAY); + ac.setTimerType(argoTimerType_t::DELAY_TIMER); + ac.setDelayTimerMinutes(9*60+40); + + auto expected = std::vector({0x8B, 0x02, 0x2D, 0x13, 0x09, 0x00, + 0x00, 0x00, 0xD4}); + auto actual = ac.getRaw(); + ASSERT_EQ(ac.getRawByteLength(), kArgo3TimerStateLength); + EXPECT_THAT(std::vector(actual, actual + ac.getRawByteLength()), + ::testing::ElementsAreArray(expected)); + EXPECT_EQ( + "Timer[CH#0]: Model: 2 (WREM3), Power: Off, Timer Mode: 1 (Sleep Timer)," + " Clock: 12:00, Day: 6 (Sat), Timer: 09:40", + ac.toString()); +} + +TEST(TestArgoAC_WREM3Class, MessageConstructon_ScheduleTimer) { + IRArgoAC_WREM3 ac(kGpioUnused); + ac.setMessageType(argoIrMessageType_t::TIMER_COMMAND); + ac.on(); + ac.setCurrentTimeMinutes(18*60+16); + ac.setCurrentDayOfWeek(argoWeekday::SATURDAY); + ac.setTimerType(argoTimerType_t::SCHEDULE_TIMER_3); + ac.setScheduleTimerStartMinutes(8*60+40); + ac.setScheduleTimerStopMinutes(19*60+50); + ac.setScheduleTimerActiveDays({argoWeekday::MONDAY, argoWeekday::SATURDAY, + argoWeekday::SUNDAY}); + + auto expected = std::vector({0x8B, 0x89, 0x44, 0x03, 0x00, 0x41, + 0xA6, 0x1C, 0x26}); + auto actual = ac.getRaw(); + ASSERT_EQ(ac.getRawByteLength(), kArgo3TimerStateLength); + EXPECT_THAT(std::vector(actual, actual + ac.getRawByteLength()), + ::testing::ElementsAreArray(expected)); + EXPECT_EQ( + "Timer[CH#0]: Model: 2 (WREM3), Power: On, Timer Mode: 4 (Schedule3)," + " Clock: 18:16, Day: 6 (Sat), On Timer: 08:40, Off Timer: 19:50, " + "TimerActiveDays: Sun|Mon|Sat", ac.toString()); } -// Tests for sendArgo(). +/******************************************************************************/ +/* Tests for sendArgo(). */ +/******************************************************************************/ // Test sending typical data only. TEST(TestSendArgo, SendDataOnly) { @@ -91,9 +295,36 @@ TEST(TestSendArgo, SendDataOnly) { irsend.outputStr()); } -// Tests for decodeArgo(). -// Decode normal Argo messages. +TEST(TestSendArgoWrem3, SendDataOnly) { + IRsendTest irsend(0); + irsend.begin(); + uint8_t data[kArgoStateLength] = { + 0x0B, 0x31, 0x35, 0xFE, 0xC0, 0x2F}; + irsend.sendArgoWREM3(data); + EXPECT_EQ( + "f38000d50" + "m6400s3300" + "m400s2200m400s2200m400s900m400s2200m400s900m400s900m400s900m400s900" + "m400s2200m400s900m400s900m400s900m400s2200m400s2200m400s900m400s900" + "m400s2200m400s900m400s2200m400s900m400s2200m400s2200m400s900m400s900" + "m400s900m400s2200m400s2200m400s2200m400s2200m400s2200m400s2200m400s2200" + "m400s900m400s900m400s900m400s900m400s900m400s900m400s2200m400s2200" + "m400s2200m400s2200m400s2200m400s2200m400s900m400s2200m400s900m400s900" + "m400s900m400s900m400s900m400s900m400s900m400s900m400s900m400s900m400s900" + "m400s900m400s900m400s900m400s900m400s900m400s900m400s900m400s900m400s900" + "m400s900m400s900m400s900m400s900m400s900m400s900m400s900m400s900m400s900" + "m400s900m400s900m400s900m400s900m400s900m400s900m400s900m400s900m400s900" + "m400s900m400s900m400s900m400s900m400s900m400s900m400s900m400s900m400s900" + "m400s900m400s900m400s900m400s100000", + irsend.outputStr()); +} + +/******************************************************************************/ +/* Tests for decodeArgo(). */ +/******************************************************************************/ + +// Decode normal Argo messages. TEST(TestDecodeArgo, SyntheticDecode) { IRsendTest irsend(kGpioUnused); IRrecv irrecv(kGpioUnused); @@ -110,15 +341,141 @@ TEST(TestDecodeArgo, SyntheticDecode) { EXPECT_EQ(kArgoBits, irsend.capture.bits); EXPECT_STATE_EQ(expectedState, irsend.capture.state, irsend.capture.bits); EXPECT_EQ( - "Power: On, Mode: 0 (Cool), Fan: 0 (Auto), Temp: 20C, Room Temp: 21C, " - "Max: On, IFeel: On, Night: On", + "Model: 1 (WREM2), Power: On, Mode: 0 (Cool), Fan: 0 (Auto), Temp: 20C, " + "Room Temp: 21C, Max: On, IFeel: On, Night: On", IRAcUtils::resultAcToString(&irsend.capture)); stdAc::state_t r, p; ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &r, &p)); } -TEST(TestArgoACClass, SetAndGetTemp) { - IRArgoAC ac(kGpioUnused); +// Synthetic send and decode ***via common*** interface +TEST(TestIrAc, ArgoWrem2_SyntheticSendAndDecode_ACCommand) { + IRac irac(kGpioUnused); + auto capture = std::make_shared(kGpioUnused); + irac._utReceiver = capture; + + stdAc::state_t state = {}; + state.protocol = ARGO; + state.model = 1; + state.power = true; + state.mode = stdAc::opmode_t::kCool; + + irac.sendAc(state, nullptr); + ASSERT_NE(nullptr, irac._lastDecodeResults); + EXPECT_EQ(ARGO, irac._lastDecodeResults->decode_type); + EXPECT_EQ("Model: 1 (WREM2), Power: On, Mode: 0 (Cool), Fan: 0 (Auto), " + "Temp: 25C, Room Temp: 25C, Max: Off, IFeel: Off, Night: Off", + IRAcUtils::resultAcToString(irac._lastDecodeResults.get())); + + stdAc::state_t r = {}; + ASSERT_TRUE(IRAcUtils::decodeToState(irac._lastDecodeResults.get(), &r, + nullptr)); + EXPECT_EQ(ARGO, r.protocol); + EXPECT_EQ(1, r.model); + EXPECT_TRUE(r.power); + EXPECT_EQ(state.mode, r.mode); +} + +TEST(TestIrAc, ArgoWrem3_SyntheticSendAndDecode_ACCommand) { + IRac irac(kGpioUnused); + auto capture = std::make_shared(kGpioUnused); + irac._utReceiver = capture; + + stdAc::state_t state = {}; + state.protocol = ARGO; + state.model = argo_ac_remote_model_t::SAC_WREM3; + state.power = true; + state.mode = stdAc::opmode_t::kCool; + + irac.sendAc(state, nullptr); + ASSERT_NE(nullptr, irac._lastDecodeResults); + EXPECT_EQ(ARGO, irac._lastDecodeResults->decode_type); + EXPECT_EQ("Command[CH#0]: Model: 2 (WREM3), Power: On, Mode: 1 (Cool)," + " Temp: 25C, Room: 25C, Fan: 0 (Auto), Swing(V): 7 (Breeze), " + "IFeel: Off, Night: Off, Econo: Off, Max: Off, Filter: Off, " + "Light: Off", + IRAcUtils::resultAcToString(irac._lastDecodeResults.get())); + + stdAc::state_t r = {}; + ASSERT_TRUE(IRAcUtils::decodeToState(irac._lastDecodeResults.get(), &r, + nullptr)); + EXPECT_EQ(ARGO, r.protocol); + EXPECT_EQ(state.model, r.model); + EXPECT_EQ(state.power, r.power); +} + +TEST(TestIrAc, ArgoWrem3_SyntheticSendAndDecode_iFeelReport) { + IRac irac(kGpioUnused); + auto capture = std::make_shared(kGpioUnused); + irac._utReceiver = capture; + + stdAc::state_t state = {}; + state.protocol = ARGO; + state.model = argo_ac_remote_model_t::SAC_WREM3; + state.command = stdAc::ac_command_t::kTemperatureReport; + state.roomTemperature = 19; + + irac.sendAc(state, nullptr); + + ASSERT_NE(nullptr, irac._lastDecodeResults); + EXPECT_EQ(ARGO, irac._lastDecodeResults->decode_type); + EXPECT_EQ("Sensor Temp[CH#0]: Model: 2 (WREM3), Room: 19C", + IRAcUtils::resultAcToString(irac._lastDecodeResults.get())); + + stdAc::state_t r = {}; + ASSERT_TRUE(IRAcUtils::decodeToState(irac._lastDecodeResults.get(), &r, + nullptr)); + EXPECT_EQ(ARGO, r.protocol); + EXPECT_EQ(state.model, r.model); + EXPECT_EQ(state.command, r.command); + EXPECT_EQ(state.roomTemperature, r.roomTemperature); +} + +TEST(TestIrAc, ArgoWrem3_SyntheticSendAndDecode_Timer) { + IRac irac(kGpioUnused); + auto capture = std::make_shared(kGpioUnused); + irac._utReceiver = capture; + + stdAc::state_t state = {}; + state.protocol = ARGO; + state.model = argo_ac_remote_model_t::SAC_WREM3; + state.command = stdAc::ac_command_t::kTimerCommand; + state.power = true; + state.mode = stdAc::opmode_t::kAuto; // Needs to be set for `state.power` + // not to be ignored! + state.clock = 13*60+21; + state.sleep = 2*60+10; + + irac.sendAc(state, nullptr); + + ASSERT_NE(nullptr, irac._lastDecodeResults); + EXPECT_EQ(ARGO, irac._lastDecodeResults->decode_type); + EXPECT_EQ("Timer[CH#0]: Model: 2 (WREM3), Power: On, Timer Mode: 1 " + "(Sleep Timer), Clock: 13:21, Day: 0 (Sun), Timer: 02:10", + IRAcUtils::resultAcToString(irac._lastDecodeResults.get())); + + stdAc::state_t r = {}; + ASSERT_TRUE(IRAcUtils::decodeToState(irac._lastDecodeResults.get(), &r, + nullptr)); + EXPECT_EQ(ARGO, r.protocol); + EXPECT_EQ(state.model, r.model); + EXPECT_EQ(state.command, r.command); + EXPECT_EQ(state.power, r.power); + EXPECT_EQ(state.clock, r.clock); + EXPECT_EQ(state.sleep, r.sleep); +} + +/******************************************************************************/ +/* Tests for IRArgoACBase (comon functionality across WREM2 and WREM3) */ +/******************************************************************************/ + +using IRArgoACBase_typelist = ::testing::Types; +template struct TestArgoACBaseClass : public testing::Test {}; +TYPED_TEST_CASE(TestArgoACBaseClass, IRArgoACBase_typelist); + + +TYPED_TEST(TestArgoACBaseClass, SetAndGetTemp) { + IRArgoACBase ac(kGpioUnused); ac.setTemp(25); EXPECT_EQ(25, ac.getTemp()); @@ -132,59 +489,117 @@ TEST(TestArgoACClass, SetAndGetTemp) { EXPECT_EQ(kArgoMaxTemp, ac.getTemp()); } -TEST(TestArgoACClass, SetAndGetRoomTemp) { - IRArgoAC ac(kGpioUnused); +TYPED_TEST(TestArgoACBaseClass, SetAndGetRoomTemp) { + IRArgoACBase ac(kGpioUnused); + // Room Temp from AC command ac.setRoomTemp(25); EXPECT_EQ(25, ac.getRoomTemp()); + EXPECT_EQ(0, ac.getSensorTemp()); + ac.setRoomTemp(kArgoTempDelta); + EXPECT_EQ(kArgoTempDelta, ac.getRoomTemp()); + EXPECT_EQ(0, ac.getSensorTemp()); + ac.setRoomTemp(kArgoMaxRoomTemp); + EXPECT_EQ(kArgoMaxRoomTemp, ac.getRoomTemp()); + EXPECT_EQ(0, ac.getSensorTemp()); + ac.setRoomTemp(kArgoTempDelta - 1); + EXPECT_EQ(kArgoTempDelta, ac.getRoomTemp()); + EXPECT_EQ(0, ac.getSensorTemp()); + ac.setRoomTemp(kArgoMaxRoomTemp + 1); + EXPECT_EQ(kArgoMaxRoomTemp, ac.getRoomTemp()); + EXPECT_EQ(0, ac.getSensorTemp()); + + // Room temp from iFeel coommand + ac.setMessageType(argoIrMessageType_t::IFEEL_TEMP_REPORT); // reset + EXPECT_EQ(kArgoTempDelta, ac.getRoomTemp()); + ac.setRoomTemp(19); + EXPECT_EQ(19, ac.getRoomTemp()); + EXPECT_EQ(19, ac.getSensorTemp()); ac.setRoomTemp(kArgoTempDelta); EXPECT_EQ(kArgoTempDelta, ac.getRoomTemp()); + EXPECT_EQ(kArgoTempDelta, ac.getSensorTemp()); ac.setRoomTemp(kArgoMaxRoomTemp); EXPECT_EQ(kArgoMaxRoomTemp, ac.getRoomTemp()); + EXPECT_EQ(kArgoMaxRoomTemp, ac.getSensorTemp()); ac.setRoomTemp(kArgoTempDelta - 1); EXPECT_EQ(kArgoTempDelta, ac.getRoomTemp()); + EXPECT_EQ(kArgoTempDelta, ac.getSensorTemp()); ac.setRoomTemp(kArgoMaxRoomTemp + 1); EXPECT_EQ(kArgoMaxRoomTemp, ac.getRoomTemp()); + EXPECT_EQ(kArgoMaxRoomTemp, ac.getSensorTemp()); } -TEST(TestArgoACClass, SetAndGetMode) { - IRArgoAC ac(kGpioUnused); +TYPED_TEST(TestArgoACBaseClass, SetAndGetModeEx) { + IRArgoACBase ac(kGpioUnused); - ac.setMode(kArgoHeat); - EXPECT_EQ(kArgoHeat, ac.getMode()); - ac.setMode(kArgoCool); - EXPECT_EQ(kArgoCool, ac.getMode()); - ac.setMode(kArgoDry); - EXPECT_EQ(kArgoDry, ac.getMode()); - ac.setMode(kArgoAuto); - EXPECT_EQ(kArgoAuto, ac.getMode()); - ac.setMode(kArgoHeatAuto); - EXPECT_EQ(kArgoHeatAuto, ac.getMode()); - ac.setMode(kArgoOff); - EXPECT_EQ(kArgoOff, ac.getMode()); - ac.setMode(255); - EXPECT_EQ(kArgoAuto, ac.getMode()); + ac.setMode(argoMode_t::HEAT); + EXPECT_EQ(argoMode_t::HEAT, ac.getModeEx()); + ac.setMode(argoMode_t::AUTO); + EXPECT_EQ(argoMode_t::AUTO, ac.getModeEx()); + ac.setMode(argoMode_t::COOL); + EXPECT_EQ(argoMode_t::COOL, ac.getModeEx()); + ac.setMode(argoMode_t::DRY); + EXPECT_EQ(argoMode_t::DRY, ac.getModeEx()); + ac.setMode(argoMode_t::FAN); + EXPECT_EQ(argoMode_t::FAN, ac.getModeEx()); + ac.setMode(static_cast(255)); + EXPECT_EQ(argoMode_t::AUTO, ac.getModeEx()); } -TEST(TestArgoACClass, SetAndGetFan) { - IRArgoAC ac(kGpioUnused); +TYPED_TEST(TestArgoACBaseClass, SetAndGetFanEx) { + IRArgoACBase ac(kGpioUnused); - ac.setFan(kArgoFan3); - EXPECT_EQ(kArgoFan3, ac.getFan()); - ac.setFan(kArgoFan1); - EXPECT_EQ(kArgoFan1, ac.getFan()); - ac.setFan(kArgoFanAuto); - EXPECT_EQ(kArgoFanAuto, ac.getFan()); - ac.setFan(kArgoFan3); - EXPECT_EQ(kArgoFan3, ac.getFan()); - ASSERT_NE(7, kArgoFan3); - // Now try some unexpected value. - ac.setFan(7); - EXPECT_EQ(kArgoFan3, ac.getFan()); + ac.setFan(argoFan_t::FAN_AUTO); + EXPECT_EQ(argoFan_t::FAN_AUTO, ac.getFanEx()); + ac.setFan(argoFan_t::FAN_HIGHEST); + EXPECT_EQ(argoFan_t::FAN_HIGHEST, ac.getFanEx()); + if (std::is_same()) { + // Only supported on WREM3 + ac.setFan(argoFan_t::FAN_HIGH); + EXPECT_EQ(argoFan_t::FAN_HIGH, ac.getFanEx()); + } + ac.setFan(argoFan_t::FAN_MEDIUM); + EXPECT_EQ(argoFan_t::FAN_MEDIUM, ac.getFanEx()); + if (std::is_same()) { + // Only supported on WREM3 + ac.setFan(argoFan_t::FAN_LOW); + EXPECT_EQ(argoFan_t::FAN_LOW, ac.getFanEx()); + ac.setFan(argoFan_t::FAN_LOWER); + EXPECT_EQ(argoFan_t::FAN_LOWER, ac.getFanEx()); + } + ac.setFan(argoFan_t::FAN_LOWEST); + EXPECT_EQ(argoFan_t::FAN_LOWEST, ac.getFanEx()); + ac.setFan(static_cast(255)); + EXPECT_EQ(argoFan_t::FAN_AUTO, ac.getFanEx()); } -TEST(TestArgoACClass, Night) { - IRArgoAC ac(kGpioUnused); +TYPED_TEST(TestArgoACBaseClass, SetAndGetFlapEx) { + IRArgoACBase ac(kGpioUnused); + + ac.setFlap(argoFlap_t::FLAP_FULL); + EXPECT_EQ(argoFlap_t::FLAP_FULL, ac.getFlapEx()); + ac.setFlap(argoFlap_t::FLAP_AUTO); + EXPECT_EQ(argoFlap_t::FLAP_AUTO, ac.getFlapEx()); + ac.setFlap(argoFlap_t::FLAP_6); + EXPECT_EQ(argoFlap_t::FLAP_6, ac.getFlapEx()); + ac.setFlap(argoFlap_t::FLAP_5); + EXPECT_EQ(argoFlap_t::FLAP_5, ac.getFlapEx()); + ac.setFlap(argoFlap_t::FLAP_4); + EXPECT_EQ(argoFlap_t::FLAP_4, ac.getFlapEx()); + ac.setFlap(argoFlap_t::FLAP_3); + EXPECT_EQ(argoFlap_t::FLAP_3, ac.getFlapEx()); + ac.setFlap(argoFlap_t::FLAP_2); + EXPECT_EQ(argoFlap_t::FLAP_2, ac.getFlapEx()); + ac.setFlap(argoFlap_t::FLAP_1); + EXPECT_EQ(argoFlap_t::FLAP_1, ac.getFlapEx()); + ac.setFlap(argoFlap_t::FLAP_FULL); + ac.setFlap(static_cast(255)); + EXPECT_EQ(argoFlap_t::FLAP_AUTO, ac.getFlapEx()); +} + +TYPED_TEST(TestArgoACBaseClass, Night) { + IRArgoACBase ac(kGpioUnused); + ac.setNight(false); ASSERT_FALSE(ac.getNight()); ac.setNight(true); @@ -193,8 +608,9 @@ TEST(TestArgoACClass, Night) { ASSERT_FALSE(ac.getNight()); } -TEST(TestArgoACClass, iFeel) { - IRArgoAC ac(kGpioUnused); +TYPED_TEST(TestArgoACBaseClass, iFeel) { + IRArgoACBase ac(kGpioUnused); + ac.setiFeel(false); ASSERT_FALSE(ac.getiFeel()); ac.setiFeel(true); @@ -203,8 +619,9 @@ TEST(TestArgoACClass, iFeel) { ASSERT_FALSE(ac.getiFeel()); } -TEST(TestArgoACClass, Power) { - IRArgoAC ac(kGpioUnused); +TYPED_TEST(TestArgoACBaseClass, Power) { + IRArgoACBase ac(kGpioUnused); + ac.setPower(false); ASSERT_FALSE(ac.getPower()); ac.setPower(true); @@ -213,8 +630,18 @@ TEST(TestArgoACClass, Power) { ASSERT_FALSE(ac.getPower()); } -TEST(TestArgoACClass, Max) { - IRArgoAC ac(kGpioUnused); +TYPED_TEST(TestArgoACBaseClass, OnOff) { + IRArgoACBase ac(kGpioUnused); + + ASSERT_FALSE(ac.getPower()); + ac.on(); + ASSERT_TRUE(ac.getPower()); + ac.off(); + ASSERT_FALSE(ac.getPower()); +} + +TYPED_TEST(TestArgoACBaseClass, Max) { + IRArgoACBase ac(kGpioUnused); ac.setMax(false); ASSERT_FALSE(ac.getMax()); ac.setMax(true); @@ -223,12 +650,697 @@ TEST(TestArgoACClass, Max) { ASSERT_FALSE(ac.getMax()); } +TYPED_TEST(TestArgoACBaseClass, SetAndGetMessageType) { + IRArgoACBase ac(kGpioUnused); + + ac.setMessageType(argoIrMessageType_t::AC_CONTROL); + EXPECT_EQ(argoIrMessageType_t::AC_CONTROL, ac.getMessageType()); + ac.setMessageType(argoIrMessageType_t::CONFIG_PARAM_SET); + EXPECT_EQ(argoIrMessageType_t::CONFIG_PARAM_SET, ac.getMessageType()); + ac.setMessageType(argoIrMessageType_t::IFEEL_TEMP_REPORT); + EXPECT_EQ(argoIrMessageType_t::IFEEL_TEMP_REPORT, ac.getMessageType()); + ac.setMessageType(argoIrMessageType_t::TIMER_COMMAND); + EXPECT_EQ(argoIrMessageType_t::TIMER_COMMAND, ac.getMessageType()); +} + +TYPED_TEST(TestArgoACBaseClass, SetMessageTypeResetsState) { + IRArgoACBase ac(kGpioUnused); + + ac.on(); + ac.setTemp(30); + ac.setRoomTemp(33); + ac.setMode(argoMode_t::COOL); + ac.setFan(argoFan_t::FAN_HIGHEST); + + ac.setMessageType(argoIrMessageType_t::AC_CONTROL); + EXPECT_EQ(argoIrMessageType_t::AC_CONTROL, ac.getMessageType()); + EXPECT_FALSE(ac.getPower()); + EXPECT_EQ(20, ac.getTemp()); + EXPECT_EQ(25, ac.getRoomTemp()); + EXPECT_EQ(argoMode_t::AUTO, ac.getModeEx()); + EXPECT_EQ(argoFan_t::FAN_AUTO, ac.getFanEx()); + + ac.setMessageType(argoIrMessageType_t::IFEEL_TEMP_REPORT); + EXPECT_EQ(argoIrMessageType_t::IFEEL_TEMP_REPORT, ac.getMessageType()); + EXPECT_EQ(kArgoTempDelta, ac.getRoomTemp()); +} + +TYPED_TEST(TestArgoACBaseClass, staticGetMessageType) { + IRArgoACBase ac(kGpioUnused); + + ac.setMessageType(argoIrMessageType_t::AC_CONTROL); + EXPECT_EQ(argoIrMessageType_t::AC_CONTROL, + IRArgoACBase::getMessageType(ac.getRaw(), + ac.getRawByteLength())); + + ac.setMessageType(argoIrMessageType_t::IFEEL_TEMP_REPORT); + EXPECT_EQ(argoIrMessageType_t::IFEEL_TEMP_REPORT, + IRArgoACBase::getMessageType(ac.getRaw(), + ac.getRawByteLength())); + + if (std::is_same()) { + // Only supported for WREM3 + ac.setMessageType(argoIrMessageType_t::CONFIG_PARAM_SET); + EXPECT_EQ(argoIrMessageType_t::CONFIG_PARAM_SET, + IRArgoACBase::getMessageType(ac.getRaw(), + ac.getRawByteLength())); + + ac.setMessageType(argoIrMessageType_t::TIMER_COMMAND); + EXPECT_EQ(argoIrMessageType_t::TIMER_COMMAND, + IRArgoACBase::getMessageType(ac.getRaw(), + ac.getRawByteLength())); + } +} + +TYPED_TEST(TestArgoACBaseClass, staticgetStateLengthForIrMsgType) { + if (std::is_same()) { + EXPECT_EQ(kArgoStateLength, + IRArgoACBase::getStateLengthForIrMsgType( + argoIrMessageType_t::AC_CONTROL)); + EXPECT_EQ(kArgoStateLength, + IRArgoACBase::getStateLengthForIrMsgType( + argoIrMessageType_t::TIMER_COMMAND)); + EXPECT_EQ(0, + IRArgoACBase::getStateLengthForIrMsgType( + argoIrMessageType_t::CONFIG_PARAM_SET)); + EXPECT_EQ(kArgoShortStateLength, + IRArgoACBase::getStateLengthForIrMsgType( + argoIrMessageType_t::IFEEL_TEMP_REPORT)); + } else { + EXPECT_EQ(kArgo3AcControlStateLength, + IRArgoACBase::getStateLengthForIrMsgType( + argoIrMessageType_t::AC_CONTROL)); + EXPECT_EQ(kArgo3TimerStateLength, + IRArgoACBase::getStateLengthForIrMsgType( + argoIrMessageType_t::TIMER_COMMAND)); + EXPECT_EQ(kArgo3ConfigStateLength, + IRArgoACBase::getStateLengthForIrMsgType( + argoIrMessageType_t::CONFIG_PARAM_SET)); + EXPECT_EQ(kArgo3iFeelReportStateLength, + IRArgoACBase::getStateLengthForIrMsgType( + argoIrMessageType_t::IFEEL_TEMP_REPORT)); + } +} + +TYPED_TEST(TestArgoACBaseClass, setRaw) { + TypeParam rawStateAC = {}; + rawStateAC.RoomTemp = 30; + + TypeParam rawStateIFeel = {}; + rawStateIFeel.SensorT = 25; + + IRArgoACBase ac(kGpioUnused); + + if (std::is_same()) { + ac.setRaw(reinterpret_cast(&rawStateAC), std::min( + static_cast(kArgoStateLength), sizeof(TypeParam))); + EXPECT_EQ(30 + kArgoTempDelta, ac.getRoomTemp()); + EXPECT_EQ(argoIrMessageType_t::AC_CONTROL, ac.getMessageType()); + + ac.setRaw(reinterpret_cast(&rawStateIFeel), std::min( + static_cast(kArgoShortStateLength), sizeof(TypeParam))); + EXPECT_EQ(25 + kArgoTempDelta, ac.getRoomTemp()); + EXPECT_EQ(argoIrMessageType_t::IFEEL_TEMP_REPORT, ac.getMessageType()); + } else { + ac.setRaw(reinterpret_cast(&rawStateAC), std::min( + static_cast(kArgo3AcControlStateLength), sizeof(TypeParam))); + EXPECT_EQ(30 + kArgoTempDelta, ac.getRoomTemp()); + EXPECT_EQ(argoIrMessageType_t::AC_CONTROL, ac.getMessageType()); + + auto raw = reinterpret_cast(&rawStateIFeel); + raw[0] = 0x4B; // sets Byte0::IrCommandType to IFeel (0b01) + ac.setRaw(raw, std::min(static_cast(kArgo3iFeelReportStateLength), + sizeof(TypeParam))); + EXPECT_EQ(25 + kArgoTempDelta, ac.getRoomTemp()); + EXPECT_EQ(argoIrMessageType_t::IFEEL_TEMP_REPORT, ac.getMessageType()); + } +} + +/******************************************************************************/ +/* Backward-compatibility tests of legacy IRArgoAc raw methods vs. base class */ +/******************************************************************************/ + +/// @brief Tests interactions of raw setFan() method +/// with a base-class getFanEx() +TEST(TestArgoACClass, SetAndGetFanEx) { + IRArgoAC ac(kGpioUnused); + + ac.setFan(kArgoFan3); + EXPECT_EQ(kArgoFan3, ac.getFan()); + EXPECT_EQ(argoFan_t::FAN_HIGHEST, ac.getFanEx()); + ac.setFan(kArgoFan1); + EXPECT_EQ(kArgoFan1, ac.getFan()); + EXPECT_EQ(argoFan_t::FAN_LOWEST, ac.getFanEx()); + ac.setFan(kArgoFanAuto); + EXPECT_EQ(kArgoFanAuto, ac.getFan()); + EXPECT_EQ(argoFan_t::FAN_AUTO, ac.getFanEx()); + ac.setFan(kArgoFan2); + EXPECT_EQ(kArgoFan2, ac.getFan()); + EXPECT_EQ(argoFan_t::FAN_MEDIUM, ac.getFanEx()); + + ASSERT_NE(7, kArgoFan3); + // Now try some unexpected value. + ac.setFan(7); + EXPECT_EQ(kArgoFan3, ac.getFan()); +} + +/// @brief Tests interactions of base-class setFan() method +/// with a raw getFan() +TEST(TestArgoACClass, SetFanExAndGetFan) { + IRArgoAC ac(kGpioUnused); + + ac.setFan(argoFan_t::FAN_AUTO); + EXPECT_EQ(kArgoFanAuto, ac.getFan()); + + ac.setFan(argoFan_t::FAN_HIGHEST); + EXPECT_EQ(kArgoFan3, ac.getFan()); + ac.setFan(argoFan_t::FAN_HIGH); + EXPECT_EQ(kArgoFan3, ac.getFan()); + + ac.setFan(argoFan_t::FAN_MEDIUM); + EXPECT_EQ(kArgoFan2, ac.getFan()); + ac.setFan(argoFan_t::FAN_LOW); + EXPECT_EQ(kArgoFan2, ac.getFan()); + + ac.setFan(argoFan_t::FAN_LOWER); + EXPECT_EQ(kArgoFan1, ac.getFan()); + ac.setFan(argoFan_t::FAN_LOWEST); + EXPECT_EQ(kArgoFan1, ac.getFan()); + + ac.setFan(static_cast(255)); + EXPECT_EQ(kArgoFanAuto, ac.getFan()); +} + +TEST(TestArgoACClass, SetFlapGetFlap) { + IRArgoAC ac(kGpioUnused); + + ac.setFlap(kArgoFlapFull); + EXPECT_EQ(kArgoFlapFull, ac.getFlap()); + ac.setFlap(kArgoFlapAuto); + EXPECT_EQ(kArgoFlapAuto, ac.getFlap()); + ac.setFlap(kArgoFlap1); + EXPECT_EQ(kArgoFlap1, ac.getFlap()); + ac.setFlap(kArgoFlap2); + EXPECT_EQ(kArgoFlap2, ac.getFlap()); + ac.setFlap(kArgoFlap3); + EXPECT_EQ(kArgoFlap3, ac.getFlap()); + ac.setFlap(kArgoFlap4); + EXPECT_EQ(kArgoFlap4, ac.getFlap()); + ac.setFlap(kArgoFlap5); + EXPECT_EQ(kArgoFlap5, ac.getFlap()); + ac.setFlap(kArgoFlap6); + EXPECT_EQ(kArgoFlap6, ac.getFlap()); +} + +/// @brief Tests interactions of raw setMode() method +/// with a base-class getModeEx() +TEST(TestArgoACClass, SetModeAndGetModeEx) { + IRArgoAC ac(kGpioUnused); + + ac.setMode(kArgoHeat); + EXPECT_EQ(kArgoHeat, ac.getMode()); + EXPECT_EQ(argoMode_t::HEAT, ac.getModeEx()); + ac.setMode(kArgoCool); + EXPECT_EQ(kArgoCool, ac.getMode()); + EXPECT_EQ(argoMode_t::COOL, ac.getModeEx()); + ac.setMode(kArgoDry); + EXPECT_EQ(kArgoDry, ac.getMode()); + EXPECT_EQ(argoMode_t::DRY, ac.getModeEx()); + ac.setMode(kArgoAuto); + EXPECT_EQ(kArgoAuto, ac.getMode()); + EXPECT_EQ(argoMode_t::AUTO, ac.getModeEx()); + ac.setMode(kArgoHeatAuto); + EXPECT_EQ(kArgoHeatAuto, ac.getMode()); + EXPECT_EQ(argoMode_t::AUTO, ac.getModeEx()); + ac.setMode(kArgoOff); + EXPECT_EQ(kArgoOff, ac.getMode()); + EXPECT_EQ(argoMode_t::FAN, ac.getModeEx()); + ac.setMode(255); + EXPECT_EQ(kArgoAuto, ac.getMode()); + EXPECT_EQ(argoMode_t::AUTO, ac.getModeEx()); +} + +/// @brief Tests interactions of base-class setMode() method +/// with a raw getMode() +TEST(TestArgoACClass, SetModeExAndGetMode) { + IRArgoAC ac(kGpioUnused); + + ac.setMode(argoMode_t::HEAT); + EXPECT_EQ(kArgoHeat, ac.getMode()); + EXPECT_EQ(argoMode_t::HEAT, ac.getModeEx()); + ac.setMode(argoMode_t::AUTO); + EXPECT_EQ(kArgoAuto, ac.getMode()); + EXPECT_EQ(argoMode_t::AUTO, ac.getModeEx()); + ac.setMode(argoMode_t::COOL); + EXPECT_EQ(kArgoCool, ac.getMode()); + EXPECT_EQ(argoMode_t::COOL, ac.getModeEx()); + ac.setMode(argoMode_t::DRY); + EXPECT_EQ(kArgoDry, ac.getMode()); + EXPECT_EQ(argoMode_t::DRY, ac.getModeEx()); + ac.setMode(argoMode_t::FAN); + EXPECT_EQ(kArgoOff, ac.getMode()); // Fan is N/A (?) -> defaults to off + EXPECT_EQ(argoMode_t::FAN, ac.getModeEx()); + ac.setMode(static_cast(kArgoHeatBlink)); + EXPECT_EQ(kArgoHeatBlink, ac.getMode()); + EXPECT_EQ(static_cast(kArgoHeatBlink), ac.getModeEx()); + ac.setMode(static_cast(255)); + EXPECT_EQ(kArgoAuto, ac.getMode()); + EXPECT_EQ(argoMode_t::AUTO, ac.getModeEx()); +} + +TEST(TestArgoACClass, SendSensorTemp) { + IRrecv irrecv(kGpioUnused); + + // Method 1 (via sendSensorTemp()) + IRArgoAC ac(kGpioUnused); + ac.sendSensorTemp(10); + ac._irsend.makeDecodeResult(); + EXPECT_TRUE(irrecv.decode(&ac._irsend.capture)); + EXPECT_EQ(decode_type_t::ARGO, ac._irsend.capture.decode_type); + EXPECT_EQ("Model: 1 (WREM2), Sensor Temp: 10C", + IRAcUtils::resultAcToString(&ac._irsend.capture)); + + // Method 2 (via send()) + IRArgoAC ac2(kGpioUnused); + ac2.setMessageType(argoIrMessageType_t::IFEEL_TEMP_REPORT); + ac2.setRoomTemp(19); + ac2.send(); + ac2._irsend.makeDecodeResult(); + EXPECT_TRUE(irrecv.decode(&ac2._irsend.capture)); + EXPECT_EQ(decode_type_t::ARGO, ac2._irsend.capture.decode_type); + EXPECT_EQ("Model: 1 (WREM2), Sensor Temp: 19C", + IRAcUtils::resultAcToString(&ac2._irsend.capture)); +} + +/******************************************************************************/ +/* IRArgoAC_WREM3-specific tests */ +/******************************************************************************/ + +TEST(TestArgoAC_WREM3Class, Eco) { + IRArgoAC_WREM3 ac(kGpioUnused); + + ac.setEco(false); + ASSERT_FALSE(ac.getEco()); + ac.setEco(true); + ASSERT_TRUE(ac.getEco()); + ac.setEco(false); + ASSERT_FALSE(ac.getEco()); +} + +TEST(TestArgoAC_WREM3Class, Filter) { + IRArgoAC_WREM3 ac(kGpioUnused); + + ac.setFilter(false); + ASSERT_FALSE(ac.getFilter()); + ac.setFilter(true); + ASSERT_TRUE(ac.getFilter()); + ac.setFilter(false); + ASSERT_FALSE(ac.getFilter()); +} + +TEST(TestArgoAC_WREM3Class, Light) { + IRArgoAC_WREM3 ac(kGpioUnused); + + ac.setLight(false); + ASSERT_FALSE(ac.getLight()); + ac.setLight(true); + ASSERT_TRUE(ac.getLight()); + ac.setLight(false); + ASSERT_FALSE(ac.getLight()); +} + +TEST(TestArgoAC_WREM3Class, Channel) { + IRArgoAC_WREM3 ac(kGpioUnused); + + ac.setChannel(0); + ASSERT_EQ(0, ac.getChannel()); + ac.setChannel(1); + ASSERT_EQ(1, ac.getChannel()); + ac.setChannel(2); + ASSERT_EQ(2, ac.getChannel()); + ac.setChannel(3); + ASSERT_EQ(3, ac.getChannel()); + ac.setChannel(4); + ASSERT_EQ(3, ac.getChannel()); +} + +TEST(TestArgoAC_WREM3Class, ConfigEntry) { + IRArgoAC_WREM3 ac(kGpioUnused); + + ac.setConfigEntry(0, 0); + ASSERT_EQ(std::make_pair(static_cast(0), static_cast(0)), + ac.getConfigEntry()); + ac.setConfigEntry(80, 86); + ASSERT_EQ(std::make_pair(static_cast(80), static_cast(86)), + ac.getConfigEntry()); + ac.setConfigEntry(255, 255); + ASSERT_EQ(std::make_pair(static_cast(255), + static_cast(255)), + ac.getConfigEntry()); +} + +TEST(TestArgoAC_WREM3Class, CurrentTimeMinutes) { + IRArgoAC_WREM3 ac(kGpioUnused); + + ac.setCurrentTimeMinutes(0); + ASSERT_EQ(0, ac.getCurrentTimeMinutes()); + ac.setCurrentTimeMinutes(16*60+50); + ASSERT_EQ(16*60+50, ac.getCurrentTimeMinutes()); + ac.setCurrentTimeMinutes(23*60+59); + ASSERT_EQ(23*60+59, ac.getCurrentTimeMinutes()); + ac.setCurrentTimeMinutes(23*60+59+1); + ASSERT_EQ(23*60+59, ac.getCurrentTimeMinutes()); +} + +TEST(TestArgoAC_WREM3Class, CurrentDayOfWeek) { + IRArgoAC_WREM3 ac(kGpioUnused); + + ac.setCurrentDayOfWeek(argoWeekday::SUNDAY); + ASSERT_EQ(argoWeekday::SUNDAY, ac.getCurrentDayOfWeek()); + ac.setCurrentDayOfWeek(argoWeekday::MONDAY); + ASSERT_EQ(argoWeekday::MONDAY, ac.getCurrentDayOfWeek()); + ac.setCurrentDayOfWeek(argoWeekday::TUESDAY); + ASSERT_EQ(argoWeekday::TUESDAY, ac.getCurrentDayOfWeek()); + ac.setCurrentDayOfWeek(argoWeekday::WEDNESDAY); + ASSERT_EQ(argoWeekday::WEDNESDAY, ac.getCurrentDayOfWeek()); + ac.setCurrentDayOfWeek(argoWeekday::THURSDAY); + ASSERT_EQ(argoWeekday::THURSDAY, ac.getCurrentDayOfWeek()); + ac.setCurrentDayOfWeek(argoWeekday::FRIDAY); + ASSERT_EQ(argoWeekday::FRIDAY, ac.getCurrentDayOfWeek()); + ac.setCurrentDayOfWeek(argoWeekday::SATURDAY); + ASSERT_EQ(argoWeekday::SATURDAY, ac.getCurrentDayOfWeek()); + ac.setCurrentDayOfWeek(static_cast(200)); + ASSERT_EQ(argoWeekday::SATURDAY, ac.getCurrentDayOfWeek()); +} + +TEST(TestArgoAC_WREM3Class, TimerType) { + IRArgoAC_WREM3 ac(kGpioUnused); + + ac.setTimerType(argoTimerType_t::NO_TIMER); + ASSERT_EQ(argoTimerType_t::NO_TIMER, ac.getTimerType()); + ac.setTimerType(argoTimerType_t::DELAY_TIMER); + ASSERT_EQ(argoTimerType_t::DELAY_TIMER, ac.getTimerType()); + ac.setTimerType(argoTimerType_t::SCHEDULE_TIMER_1); + ASSERT_EQ(argoTimerType_t::SCHEDULE_TIMER_1, ac.getTimerType()); + ac.setTimerType(argoTimerType_t::SCHEDULE_TIMER_2); + ASSERT_EQ(argoTimerType_t::SCHEDULE_TIMER_2, ac.getTimerType()); + ac.setTimerType(argoTimerType_t::SCHEDULE_TIMER_3); + ASSERT_EQ(argoTimerType_t::SCHEDULE_TIMER_3, ac.getTimerType()); + ac.setTimerType(static_cast(201)); + ASSERT_EQ(argoTimerType_t::NO_TIMER, ac.getTimerType()); +} + +TEST(TestArgoAC_WREM3Class, DelayTimerMinutes) { + IRArgoAC_WREM3 ac(kGpioUnused); + + ac.setDelayTimerMinutes(0); + ASSERT_EQ(0, ac.getDelayTimerMinutes()); + ac.setDelayTimerMinutes(16*60+50); + ASSERT_EQ(16*60+50, ac.getDelayTimerMinutes()); + ac.setDelayTimerMinutes(16*60+54); + ASSERT_EQ(16*60+50, ac.getDelayTimerMinutes()); + ac.setDelayTimerMinutes(16*60+55); + ASSERT_EQ(16*60+60, ac.getDelayTimerMinutes()); + ac.setDelayTimerMinutes(19*60+44); + ASSERT_EQ(19*60+40, ac.getDelayTimerMinutes()); + ac.setDelayTimerMinutes(19*60+50); + ASSERT_EQ(19*60+50, ac.getDelayTimerMinutes()); + ac.setDelayTimerMinutes(19*60+60); + ASSERT_EQ(19*60+50, ac.getDelayTimerMinutes()); + ac.setDelayTimerMinutes(23*60+59); // Above max (19h50m) + ASSERT_EQ(19*60+50, ac.getDelayTimerMinutes()); + ac.setDelayTimerMinutes(23*60+59+1); + ASSERT_EQ(19*60+50, ac.getDelayTimerMinutes()); +} + +TEST(TestArgoAC_WREM3Class, ScheduleTimerStartMinutes) { + IRArgoAC_WREM3 ac(kGpioUnused); + + ac.setScheduleTimerStartMinutes(0); + ASSERT_EQ(0, ac.getScheduleTimerStartMinutes()); + ac.setScheduleTimerStartMinutes(16*60+50); + ASSERT_EQ(16*60+50, ac.getScheduleTimerStartMinutes()); + ac.setScheduleTimerStartMinutes(16*60+54); + ASSERT_EQ(16*60+50, ac.getScheduleTimerStartMinutes()); + ac.setScheduleTimerStartMinutes(16*60+55); + ASSERT_EQ(16*60+60, ac.getScheduleTimerStartMinutes()); + ac.setScheduleTimerStartMinutes(23*60+50); + ASSERT_EQ(23*60+50, ac.getScheduleTimerStartMinutes()); + ac.setScheduleTimerStartMinutes(23*60+59); // Above max (23h50m) + ASSERT_EQ(23*60+50, ac.getScheduleTimerStartMinutes()); + ac.setScheduleTimerStartMinutes(23*60+59+1); + ASSERT_EQ(23*60+50, ac.getScheduleTimerStartMinutes()); +} + +TEST(TestArgoAC_WREM3Class, ScheduleTimerStopMinutes) { + IRArgoAC_WREM3 ac(kGpioUnused); + + ac.setScheduleTimerStopMinutes(0); + ASSERT_EQ(0, ac.getScheduleTimerStopMinutes()); + ac.setScheduleTimerStopMinutes(16*60+50); + ASSERT_EQ(16*60+50, ac.getScheduleTimerStopMinutes()); + ac.setScheduleTimerStopMinutes(16*60+54); + ASSERT_EQ(16*60+50, ac.getScheduleTimerStopMinutes()); + ac.setScheduleTimerStopMinutes(16*60+55); + ASSERT_EQ(16*60+60, ac.getScheduleTimerStopMinutes()); + ac.setScheduleTimerStopMinutes(23*60+50); + ASSERT_EQ(23*60+50, ac.getScheduleTimerStopMinutes()); + ac.setScheduleTimerStopMinutes(23*60+59); // Above max (23h50m) + ASSERT_EQ(23*60+50, ac.getScheduleTimerStopMinutes()); + ac.setScheduleTimerStopMinutes(23*60+59+1); + ASSERT_EQ(23*60+50, ac.getScheduleTimerStopMinutes()); +} + +TEST(TestArgoAC_WREM3Class, ScheduleTimerActiveDays) { + IRArgoAC_WREM3 ac(kGpioUnused); + + ac.setScheduleTimerActiveDays(std::set({})); + EXPECT_THAT(ac.getScheduleTimerActiveDays(), + ::testing::IsEmpty()); + EXPECT_EQ(0b0000000, ac.getTimerActiveDaysBitmap()); + + ac.setScheduleTimerActiveDays(std::set({argoWeekday::SUNDAY})); + EXPECT_THAT(ac.getScheduleTimerActiveDays(), + ::testing::ElementsAre(argoWeekday::SUNDAY)); + EXPECT_EQ(0b0000001, ac.getTimerActiveDaysBitmap()); + + ac.setScheduleTimerActiveDays(std::set({argoWeekday::MONDAY})); + EXPECT_THAT(ac.getScheduleTimerActiveDays(), + ::testing::ElementsAre(argoWeekday::MONDAY)); + EXPECT_EQ(0b0000010, ac.getTimerActiveDaysBitmap()); + + ac.setScheduleTimerActiveDays(std::set({argoWeekday::TUESDAY})); + EXPECT_THAT(ac.getScheduleTimerActiveDays(), + ::testing::ElementsAre(argoWeekday::TUESDAY)); + EXPECT_EQ(0b0000100, ac.getTimerActiveDaysBitmap()); + + ac.setScheduleTimerActiveDays(std::set({ + argoWeekday::WEDNESDAY})); + EXPECT_THAT(ac.getScheduleTimerActiveDays(), + ::testing::ElementsAre(argoWeekday::WEDNESDAY)); + EXPECT_EQ(0b0001000, ac.getTimerActiveDaysBitmap()); + + ac.setScheduleTimerActiveDays(std::set({argoWeekday::THURSDAY})); + EXPECT_THAT(ac.getScheduleTimerActiveDays(), + ::testing::ElementsAre(argoWeekday::THURSDAY)); + EXPECT_EQ(0b0010000, ac.getTimerActiveDaysBitmap()); + + ac.setScheduleTimerActiveDays(std::set({argoWeekday::FRIDAY})); + EXPECT_THAT(ac.getScheduleTimerActiveDays(), + ::testing::ElementsAre(argoWeekday::FRIDAY)); + EXPECT_EQ(0b0100000, ac.getTimerActiveDaysBitmap()); + + ac.setScheduleTimerActiveDays(std::set({argoWeekday::SATURDAY})); + EXPECT_THAT(ac.getScheduleTimerActiveDays(), + ::testing::ElementsAre(argoWeekday::SATURDAY)); + EXPECT_EQ(0b1000000, ac.getTimerActiveDaysBitmap()); + + ac.setScheduleTimerActiveDays(std::set({ + argoWeekday::MONDAY, argoWeekday::TUESDAY, argoWeekday::WEDNESDAY, + argoWeekday::THURSDAY, argoWeekday::FRIDAY, argoWeekday::SATURDAY, + argoWeekday::SUNDAY})); + EXPECT_THAT(ac.getScheduleTimerActiveDays(), ::testing::ElementsAre( + argoWeekday::SUNDAY, argoWeekday::MONDAY, argoWeekday::TUESDAY, + argoWeekday::WEDNESDAY, argoWeekday::THURSDAY, argoWeekday::FRIDAY, + argoWeekday::SATURDAY)); + EXPECT_EQ(0b1111111, ac.getTimerActiveDaysBitmap()); + + ac.setScheduleTimerActiveDays(std::set({ + argoWeekday::MONDAY, argoWeekday::TUESDAY, argoWeekday::WEDNESDAY, + argoWeekday::THURSDAY, argoWeekday::FRIDAY})); + EXPECT_THAT(ac.getScheduleTimerActiveDays(), ::testing::ElementsAre( + argoWeekday::MONDAY, argoWeekday::TUESDAY, argoWeekday::WEDNESDAY, + argoWeekday::THURSDAY, argoWeekday::FRIDAY)); + EXPECT_EQ(0b0111110, ac.getTimerActiveDaysBitmap()); + + ac.setScheduleTimerActiveDays(std::set({ argoWeekday::TUESDAY, + argoWeekday::THURSDAY, argoWeekday::SATURDAY, argoWeekday::SUNDAY})); + EXPECT_THAT(ac.getScheduleTimerActiveDays(), ::testing::ElementsAre( + argoWeekday::SUNDAY, argoWeekday::TUESDAY, argoWeekday::THURSDAY, + argoWeekday::SATURDAY)); + EXPECT_EQ(0b1010101, ac.getTimerActiveDaysBitmap()); +} + +TEST(TestArgoAC_WREM3Class, staticGetMessageTypeFromRaw) { + ArgoProtocolWREM3 raw = {}; + + raw.IrCommandType = static_cast( + argoIrMessageType_t::AC_CONTROL); + EXPECT_EQ(argoIrMessageType_t::AC_CONTROL, + IRArgoAC_WREM3::getMessageType(raw)); + + raw.IrCommandType = static_cast( + argoIrMessageType_t::CONFIG_PARAM_SET); + EXPECT_EQ(argoIrMessageType_t::CONFIG_PARAM_SET, + IRArgoAC_WREM3::getMessageType(raw)); + + raw.IrCommandType = static_cast( + argoIrMessageType_t::IFEEL_TEMP_REPORT); + EXPECT_EQ(argoIrMessageType_t::IFEEL_TEMP_REPORT, + IRArgoAC_WREM3::getMessageType(raw)); + + raw.IrCommandType = static_cast( + argoIrMessageType_t::TIMER_COMMAND); + EXPECT_EQ(argoIrMessageType_t::TIMER_COMMAND, + IRArgoAC_WREM3::getMessageType(raw)); +} + +TEST(TestArgoAC_WREM3Class, HasValidPreamble) { + uint8_t preamble[] = { 0x4B, 0x57 }; + ASSERT_TRUE(IRArgoAC_WREM3::hasValidPreamble(preamble, + sizeof(preamble) / sizeof(preamble[0]))); + ASSERT_FALSE(IRArgoAC_WREM3::hasValidPreamble(preamble, 0)); + ASSERT_TRUE(IRArgoAC_WREM3::hasValidPreamble(preamble, 1)); + + preamble[0] = 0b00001011; + ASSERT_TRUE(IRArgoAC_WREM3::hasValidPreamble(preamble, + sizeof(preamble) / sizeof(preamble[0]))); + preamble[0] = 0b11111011; + ASSERT_TRUE(IRArgoAC_WREM3::hasValidPreamble(preamble, + sizeof(preamble) / sizeof(preamble[0]))); + preamble[0] = 0b00001010; + ASSERT_FALSE(IRArgoAC_WREM3::hasValidPreamble(preamble, + sizeof(preamble) / sizeof(preamble[0]))); + preamble[0] = 0b00000011; + ASSERT_FALSE(IRArgoAC_WREM3::hasValidPreamble(preamble, + sizeof(preamble) / sizeof(preamble[0]))); +} + +TEST(TestArgoAC_WREM3Class, IsValidWrem3Message) { + uint8_t wrem3AC[] = { 0x0B, 0x36, 0x12, 0x0F, 0xC2, 0x24 }; + uint8_t wrem3IFeel[] = { 0x4B, 0x78 }; + uint8_t wrem3Config[] = { 0xCB, 0x0C, 0x4A, 0x21 }; + uint8_t wrem3Tmr[] = { 0x8B, 0x05, 0x4D, 0x98, 0xD2, 0x44, 0x2E, 0x34, 0xA7 }; + uint8_t wrem2IFeel[] = { 0xAC, 0xF5, 0xC2, 0x63 }; + + ASSERT_TRUE(IRArgoAC_WREM3::isValidWrem3Message(wrem3AC, + sizeof(wrem3AC) / sizeof(wrem3AC[0]) * 8, true)); + ASSERT_TRUE(IRArgoAC_WREM3::isValidWrem3Message(wrem3IFeel, + sizeof(wrem3IFeel) / sizeof(wrem3IFeel[0]) * 8, true)); + ASSERT_TRUE(IRArgoAC_WREM3::isValidWrem3Message(wrem3Config, + sizeof(wrem3Config) / sizeof(wrem3Config[0]) * 8, true)); + ASSERT_TRUE(IRArgoAC_WREM3::isValidWrem3Message(wrem3Tmr, + sizeof(wrem3Tmr) / sizeof(wrem3Tmr[0]) * 8, true)); + ASSERT_FALSE(IRArgoAC_WREM3::isValidWrem3Message(wrem2IFeel, + sizeof(wrem2IFeel) / sizeof(wrem2IFeel[0]) * 8, true)); + + // 1 bit too short + ASSERT_FALSE(IRArgoAC_WREM3::isValidWrem3Message(wrem3AC, + sizeof(wrem3AC) / sizeof(wrem3AC[0]) * 8 - 1, true)); + + // wrong checksum + wrem3AC[5] ^= wrem3AC[5]; + ASSERT_FALSE(IRArgoAC_WREM3::isValidWrem3Message(wrem3AC, + sizeof(wrem3AC) / sizeof(wrem3AC[0]) * 8, true)); // strict + ASSERT_TRUE(IRArgoAC_WREM3::isValidWrem3Message(wrem3AC, + sizeof(wrem3AC) / sizeof(wrem3AC[0]) * 8, false)); // lax + wrem3AC[5] ^= wrem3AC[5]; // restore + + wrem3IFeel[1] ^= wrem3IFeel[1]; + ASSERT_FALSE(IRArgoAC_WREM3::isValidWrem3Message(wrem3IFeel, + sizeof(wrem3IFeel) / sizeof(wrem3IFeel[0]) * 8, true)); + ASSERT_TRUE(IRArgoAC_WREM3::isValidWrem3Message(wrem3IFeel, + sizeof(wrem3IFeel) / sizeof(wrem3IFeel[0]) * 8, false)); + wrem3IFeel[1] ^= wrem3IFeel[1]; // restore + + wrem3Config[3] ^= wrem3Config[3]; + ASSERT_FALSE(IRArgoAC_WREM3::isValidWrem3Message(wrem3Config, + sizeof(wrem3Config) / sizeof(wrem3Config[0]) * 8, true)); + ASSERT_TRUE(IRArgoAC_WREM3::isValidWrem3Message(wrem3Config, + sizeof(wrem3Config) / sizeof(wrem3Config[0]) * 8, false)); + wrem3Config[3] ^= wrem3Config[3]; // restore + + wrem3Tmr[8] ^= wrem3Tmr[8]; + ASSERT_FALSE(IRArgoAC_WREM3::isValidWrem3Message(wrem3Tmr, + sizeof(wrem3Tmr) / sizeof(wrem3Tmr[0]) * 8, true)); + ASSERT_TRUE(IRArgoAC_WREM3::isValidWrem3Message(wrem3Tmr, + sizeof(wrem3Tmr) / sizeof(wrem3Tmr[0]) * 8, false)); + wrem3Tmr[8] ^= wrem3Tmr[8]; // restore + + // wrong preamble + wrem3IFeel[0] += 1; + ASSERT_FALSE(IRArgoAC_WREM3::isValidWrem3Message(wrem3IFeel, + sizeof(wrem3IFeel) / sizeof(wrem3IFeel[0]) * 8, true)); + ASSERT_TRUE(IRArgoAC_WREM3::isValidWrem3Message(wrem3IFeel, + sizeof(wrem3IFeel) / sizeof(wrem3IFeel[0]) * 8, false)); + wrem3IFeel[0] -= 1; // restore +} + +TEST(TestArgoAC_WREM3Class, SendSensorTemp) { + IRrecv irrecv(kGpioUnused); + + // Method 1 (via sendSensorTemp()) + IRArgoAC_WREM3 ac(kGpioUnused); + ac.sendSensorTemp(10); + ac._irsend.makeDecodeResult(); + EXPECT_TRUE(irrecv.decode(&ac._irsend.capture)); + EXPECT_EQ(decode_type_t::ARGO, ac._irsend.capture.decode_type); + EXPECT_EQ("Sensor Temp[CH#0]: Model: 2 (WREM3), Room: 10C", + IRAcUtils::resultAcToString(&ac._irsend.capture)); + + // Method 2 (via send()) + IRArgoAC_WREM3 ac2(kGpioUnused); + ac2.setMessageType(argoIrMessageType_t::IFEEL_TEMP_REPORT); + ac2.setRoomTemp(19); + ac2.send(); + ac2._irsend.makeDecodeResult(); + EXPECT_TRUE(irrecv.decode(&ac2._irsend.capture)); + EXPECT_EQ(decode_type_t::ARGO, ac2._irsend.capture.decode_type); + EXPECT_EQ("Sensor Temp[CH#0]: Model: 2 (WREM3), Room: 19C", + IRAcUtils::resultAcToString(&ac2._irsend.capture)); +} + +TEST(TestArgoAC_WREM3Class, NonExModeFlapFan) { + IRArgoAC_WREM3 ac(kGpioUnused); + + ac.setFan(argoFan_t::FAN_HIGH); + EXPECT_EQ(argoFan_t::FAN_HIGH, ac.getFan()); + EXPECT_EQ(argoFan_t::FAN_HIGH, ac.getFanEx()); + + ac.setMode(argoMode_t::FAN); + EXPECT_EQ(argoMode_t::FAN, ac.getMode()); + EXPECT_EQ(argoMode_t::FAN, ac.getModeEx()); + + ac.setFlap(argoFlap_t::FLAP_4); + EXPECT_EQ(argoFlap_t::FLAP_4, ac.getFlap()); + EXPECT_EQ(argoFlap_t::FLAP_4, ac.getFlapEx()); +} + + +/******************************************************************************/ +/* Housekeeping */ +/******************************************************************************/ + TEST(TestUtils, Housekeeping) { ASSERT_EQ("ARGO", typeToString(decode_type_t::ARGO)); ASSERT_EQ(decode_type_t::ARGO, strToDecodeType("ARGO")); ASSERT_TRUE(hasACState(decode_type_t::ARGO)); } + +/******************************************************************************/ +/* Decode tests based on real (recorded) IR messages */ +/******************************************************************************/ TEST(TestDecodeArgo, RealShortDecode) { IRsendTest irsend(kGpioUnused); IRrecv irrecv(kGpioUnused); @@ -252,9 +1364,157 @@ TEST(TestDecodeArgo, RealShortDecode) { EXPECT_EQ(kArgoShortBits, irsend.capture.bits); EXPECT_STATE_EQ(expectedState, irsend.capture.state, irsend.capture.bits); EXPECT_EQ( - "Sensor Temp: 28C", + "Model: 1 (WREM2), Sensor Temp: 28C", IRAcUtils::resultAcToString(&irsend.capture)); stdAc::state_t r, p; - // These short messages don't result in a valid state. - ASSERT_FALSE(IRAcUtils::decodeToState(&irsend.capture, &r, &p)); + // These short messages do result in a valid state (w/ room temperature only) + EXPECT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &r, &p)); + EXPECT_EQ(stdAc::ac_command_t::kTemperatureReport, r.command); + EXPECT_EQ(28, r.roomTemperature); } + +/// +/// @brief Test Fixture for recorded tests +/// +struct ArgoE2ETestParam { + const std::vector rawDataInput; + const uint8_t expectedEncodedSizeBytes; + const std::vector expectedEncodedValue; + const std::string expectedString; + + ArgoE2ETestParam(std::vector _raw, uint8_t _encSize, + std::vector _encValue, std::string _str) + : rawDataInput(_raw), expectedEncodedSizeBytes(_encSize), + expectedEncodedValue(_encValue), expectedString(_str) {} + + friend std::ostream& operator<<(std::ostream& os, const ArgoE2ETestParam& v) { + return os << "rawDataInput: " << ::testing::PrintToString(v.rawDataInput) + << "\n\texpectedEncodedSize: " + << static_cast(v.expectedEncodedSizeBytes) + << "[B]" << "\n\texpectedEncodedValue: 0x" + << bytesToHexString(v.expectedEncodedValue) << "\n\texpectedString: " + << v.expectedString; + } +}; + +/// +/// @brief Test fixture for real-world recorded messages for WREM3 +/// +class TestArgoE2E : public ::testing::TestWithParam {}; + +// Test code +TEST_P(TestArgoE2E, RealExampleCommands) { + IRsendTest irsend(kGpioUnused); + IRrecv irrecv(kGpioUnused); + irsend.begin(); + + irsend.reset(); + irsend.sendRaw(&GetParam().rawDataInput[0], + GetParam().rawDataInput.size(), kArgoFrequency); + irsend.makeDecodeResult(); + + ASSERT_TRUE(irrecv.decode(&irsend.capture)); + EXPECT_EQ(decode_type_t::ARGO, irsend.capture.decode_type); + ASSERT_TRUE(IRArgoAC_WREM3::isValidWrem3Message(irsend.capture.state, + irsend.capture.bits, true)); + + EXPECT_EQ(GetParam().expectedEncodedSizeBytes * 8, irsend.capture.bits); + + std::vector stateActual(irsend.capture.state, irsend.capture.state + + GetParam().expectedEncodedSizeBytes); + EXPECT_THAT(stateActual, ::testing::ElementsAreArray( + GetParam().expectedEncodedValue)); + + EXPECT_FALSE(irsend.capture.repeat); + + EXPECT_EQ(GetParam().expectedString, + IRAcUtils::resultAcToString(&irsend.capture)); +} + +// Test cases +INSTANTIATE_TEST_CASE_P( + TestDecodeArgo, + TestArgoE2E, + ::testing::Values( + ArgoE2ETestParam( + std::vector { + 6468, 3150, 456, 2154, 428, 2152, 462, 874, 422, 2158, 424, 882, + 424, 880, 428, 876, 430, 874, 454, 850, 424, 2154, 460, 2150, + 430, 874, 422, 2156, 458, 2152, 430, 874, 420, 884, 454, 850, + 424, 2152, 462, 874, 422, 882, 424, 2154, 428, 876, 430, 874, + 476, 828, 478, 2098, 462, 2148, 424, 2156, 458, 2150, 430, 874, + 432, 872, 422, 882, 424, 880, 482, 822, 430, 2148, 454, 852, + 488, 816, 480, 826, 482, 852, 454, 2126, 424, 2154, 458, 876, + 454, 852, 454, 2124, 426, 880, 426, 878, 428, 2150, 486, 848, + 426, 878, 428 + }, + kArgo3AcControlStateLength, + std::vector { 0x0B, 0x36, 0x12, 0x0F, 0xC2, 0x24 }, + "Command[CH#0]: Model: 2 (WREM3), Power: On, Mode: 1 (Cool), Temp: 22C, " + "Room: 26C, Fan: 0 (Auto), Swing(V): 7 (Breeze), IFeel: Off, Night: Off, " + "Econo: Off, Max: Off, Filter: Off, Light: On"), + + + ArgoE2ETestParam( + std::vector { + 6460, 3150, 454, 2154, 426, 2152, 462, 842, 452, 2156, 424, 878, + 428, 874, 432, 2144, 456, 878, 428, 2148, 432, 870, 424, 2152, + 460, 842, 452, 2154, 426, 878, 428, 876, 430, 872, 422 + }, + kArgo3iFeelReportStateLength, + std::vector { 0x4B, 0x15 }, + "Sensor Temp[CH#0]: Model: 2 (WREM3), Room: 25C"), + + + ArgoE2ETestParam( + std::vector { + 6434, 3222, 424, 2186, 424, 2156, 422, 882, 422, 2158, 430, 874, + 430, 876, 430, 2212, 400, 874, 430, 2150, 450, 2160, 428, 2152, + 428, 908, 396, 2184, 426, 878, 426, 2152, 426, 880, 448 + }, + kArgo3iFeelReportStateLength, + std::vector {0x4B, 0x57}, + "Sensor Temp[CH#0]: Model: 2 (WREM3), Room: 27C"), + + + ArgoE2ETestParam( + std::vector { + 6468, 3154, 482, 2128, 452, 2128, 484, 820, 486, 2124, 456, 848, + 458, 848, 424, 880, 448, 2130, 482, 2126, 452, 852, 422, 2158, + 488, 816, 490, 814, 490, 814, 492, 812, 482, 822, 484, 2124, + 422, 882, 456, 2124, 488, 2122, 456, 848, 458, 846, 460, 2120, + 482, 822, 484, 822, 484, 820, 486, 818, 488, 2122, 458, 2122, + 490, 814, 492, 814, 492, 2118, 450, 854, 452, 2126, 486, 820, + 486, 818, 488, 2122, 458, 846, 458, 2120, 492, 2118, 430, 874, + 430, 876, 454, 2126, 484, 820, 486, 818, 478, 828, 488, 2120, + 426, 878, 460, 844, 430, 2148, 486, 2094, 454, 2154, 480, 826, + 480, 2128, 430, 876, 430, 872, 422, 882, 424, 882, 424, 2156, + 478, 826, 480, 2100, 458, 2152, 482, 822, 484, 822, 452, 2156, + 424, 2156, 478, 2104, 452, 852, 454, 852, 452, 2158, 478, 828, + 478, 2132, 426 + }, + kArgo3TimerStateLength, + std::vector { + 0x8B, 0x05, 0x4D, 0x98, 0xD2, 0x44, 0x2E, 0x34, 0xA7 + }, + "Timer[CH#0]: Model: 2 (WREM3), Power: On, Timer Mode: 2 (Schedule1), " + "Clock: 20:32, Day: 0 (Sun), On Timer: 09:10, Off Timer: 17:50, " + "TimerActiveDays: Mon|Tue|Fri|Sat"), + + + ArgoE2ETestParam( + std::vector { + 6464, 3156, 492, 2118, 472, 2108, 484, 820, 486, 2124, 454, 850, + 456, 848, 426, 2154, 492, 2120, 482, 822, 450, 854, 452, 2128, + 484, 2126, 454, 850, 454, 850, 456, 848, 424, 880, 480, 822, + 428, 2152, 482, 822, 484, 2126, 454, 850, 454, 850, 456, 2122, + 490, 814, 492, 2118, 452, 852, 454, 850, 454, 848, 458, 846, + 460, 2118, 482, 822, 484, 820, 486 + }, + kArgo3ConfigStateLength, + std::vector { 0xCB, 0x0C, 0x4A, 0x21 }, + "A/C Config[CH#0]: Model: 2 (WREM3), Key: 12, Value: 74")), + [](const testing::TestParamInfo& info) { + return bytesToHexString(info.param.expectedEncodedValue); + } +); diff --git a/test/ut_utils.h b/test/ut_utils.h new file mode 100644 index 000000000..9ea19396a --- /dev/null +++ b/test/ut_utils.h @@ -0,0 +1,35 @@ +// Copyright 2022 Mateusz Bronk + +#ifndef TEST_UT_UTILS_H_ +#define TEST_UT_UTILS_H_ + +#include +#include +#include +#include +#include + + +std::string bytesToHexString(const std::vector& value) { + std::ostringstream os; + os << std::hex << std::setfill('0'); // set the stream to hex with 0 fill + + std::for_each(std::begin(value), std::end(value), [&os] (int i) { + os << std::setw(2) << std::uppercase << static_cast(i); + }); + return os.str(); +} + +std::vector hexStringToBytes(const std::string& hex) { + std::vector bytes; + bytes.reserve(hex.length() / 2); + + for (size_t i = 0; i < hex.length(); i += 2) { + std::string nextByte = hex.substr(i, 2); + uint8_t byte = static_cast(strtol(nextByte.c_str(), nullptr, 16)); + bytes.emplace_back(byte); + } + return bytes; +} + +#endif // TEST_UT_UTILS_H_