// Copyright 2017 Schmolders // Copyright 2019 crankyoldgit // Copyright 2022 Mateusz Bronk (mbronk) /// @file /// @brief Argo A/C protocol. /// @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 #endif // UNIT_TEST #include "IRremoteESP8266.h" #include "IRtext.h" #include "IRutils.h" // Constants // using SPACE modulation. MARK is always const 400u const uint16_t kArgoHdrMark = 6400; const uint16_t kArgoHdrSpace = 3300; 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; using irutils::daysBitmaskToString; using irutils::addTimerModeToString; #if SEND_ARGO /// Send a Argo A/C formatted message. /// 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, 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, _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? 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. template void IRArgoACBase::begin(void) { _irsend.begin(); } /// @cond /// @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 /// @relates IRArgoACBase\ 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 } } /// @endcond /// @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 /// @relates IRArgoACBase\ 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; } } /// @cond /// @brief Get message type from raw WREM-2 data /// 1st param ignored: WREM-2 does not carry 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 /// @relates IRArgoACBase\ 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; } /// @endcond /// @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 /// @relates IRArgoACBase\ 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 /// @relates IRArgoACBase\ 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 /// @relates IRArgoACBase\ 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); } /// @cond /// 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 The 8-bit calculated result. /// @relates IRArgoACBase\ 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); } /// @endcond /// 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 The 8-bit calculated result. /// @relates IRArgoACBase\ template<> uint8_t IRArgoACBase::calcChecksum(const uint8_t state[], const uint16_t length) { if (length < 1) { return -1; // Nothing to compute on } uint16_t payloadSizeBits = (length - 1) * 8; // Last byte carries checksum argoIrMessageType_t 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 (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. /// @relates IRArgoACBase\ 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. /// @relates IRArgoACBase\ template<> void IRArgoACBase::_checksum(ArgoProtocolWREM3 *state) { argoIrMessageType_t 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. 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. /// _param 2nd param unused (always resets to AC_CONTROL state) /// @relates IRArgoACBase\ 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; state->Post = kArgoPost; } /// 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 /// @relates IRArgoACBase\ 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); setSensorTemp(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) /// @relates IRArgoACBase\ 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); } /// @cond /// @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; } argoIrMessageType_t 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]); } /// @endcond /// 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); } /// @cond /// 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. /// @relates IRArgoACBase\ template<> void IRArgoACBase::send(const uint16_t repeat) { _irsend.sendArgoWREM3(getRaw(), getRawByteLength(), repeat); } /// @endcond /// 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); uint16_t 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); uint16_t 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. 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. 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. template void IRArgoACBase::on(void) { setPower(true); } /// Set the internal state to have the power off. template void IRArgoACBase::off(void) { setPower(false); } /// @cond /// Set the internal state to have the desired power. /// @param[in] on The desired power 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 /// @relates IRArgoACBase\ template<> void IRArgoACBase::setPower(const bool on) { _.Power = on; } /// @endcond /// @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. /// @relates IRArgoACBase\ 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. /// @relates IRArgoACBase\ 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. /// @relates IRArgoACBase\ 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. template void IRArgoACBase::setMax(const bool on) { _.Max = on; } /// Is the Max (i.e. Turbo) setting on? /// @return The current value. template bool IRArgoACBase::getMax(void) const { return _.Max; } /// Set the temperature. /// @param[in] degrees The temperature in degrees celsius. /// @note Sending 0 equals +4 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 _.Temp = temp; } /// Get the current temperature setting. /// @return The current setting for temp. in degrees celsius. template uint8_t IRArgoACBase::getTemp(void) const { return _.Temp + kArgoTempDelta; } /// @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. /// @relates IRArgoACBase\ 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. /// @relates IRArgoACBase\ template<> argoFan_t IRArgoACBase::getFanEx(void) const { return static_cast(_.Fan); } /// @cond /// 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) /// @relates IRArgoACBase\ 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: uint8_t 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; } } /// @endcond /// 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] fan The desired fan speed. /// @relates IRArgoACBase\ 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; } /// @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] flap The desired flap mode. template void IRArgoACBase::setFlap(argoFlap_t flap) { uint8_t 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)); // TODO(kaschmo): set correct bits for flap mode } /// 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; } /// 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 /// @relates IRArgoACBase\ 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. /// @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 /// @relates IRArgoACBase\ template<> argoMode_t IRArgoACBase::getModeEx(void) const { return static_cast(_.Mode); } /// @cond /// Set the desired operation mode. /// @param[in] mode The desired operation 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 /// @relates IRArgoACBase\ 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: uint8_t 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; } } /// @endcond /// @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. /// @relates IRArgoACBase\ 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: case kArgoAuto: case kArgoOff: case kArgoHeat: case kArgoHeatAuto: _.Mode = mode; 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. 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. template bool IRArgoACBase::getNight(void) const { return _.Night; } /// @brief Turn on/off the Economy mode (lowered power mode) /// @param[in] on The desired setting. 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. template bool IRArgoACBase::getiFeel(void) const { return _.iFeel; } /// @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. template void IRArgoACBase::setSensorTemp(const uint8_t degrees) { uint8_t temp = std::min(degrees, kArgoMaxRoomTemp); temp = std::max(temp, kArgoTempDelta) - kArgoTempDelta; 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. template uint8_t IRArgoACBase::getSensorTemp(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::kSensorTempReport: 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. template argoMode_t IRArgoACBase::convertMode(const stdAc::opmode_t mode) { switch (mode) { case stdAc::opmode_t::kCool: return argoMode_t::COOL; case stdAc::opmode_t::kHeat: return argoMode_t::HEAT; case stdAc::opmode_t::kDry: 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. 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 argoFan_t::FAN_LOWER; case stdAc::fanspeed_t::kMedium: 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 argoFan_t::FAN_HIGHEST; default: 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. template argoFlap_t IRArgoACBase::convertSwingV(const stdAc::swingv_t position) { switch (position) { case stdAc::swingv_t::kHighest: return argoFlap_t::FLAP_1; case stdAc::swingv_t::kHigh: return argoFlap_t::FLAP_2; case stdAc::swingv_t::kUpperMiddle: return argoFlap_t::FLAP_3; case stdAc::swingv_t::kMiddle: return argoFlap_t::FLAP_4; case stdAc::swingv_t::kLow: return argoFlap_t::FLAP_5; case stdAc::swingv_t::kLowest: 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 argoFlap_t::FLAP_AUTO; } } /// @cond /// 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. /// @relates IRArgoACBase\ 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; } } /// @endcond /// 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. /// @relates IRArgoACBase\ 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] command 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::kSensorTempReport; 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. template stdAc::opmode_t IRArgoACBase::toCommonMode(const argoMode_t mode) { switch (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; } } /// 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. template stdAc::fanspeed_t IRArgoACBase::toCommonFanSpeed(const argoFan_t speed) { switch (speed) { 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; } } /// Convert the current internal state into its stdAc::state_t equivalent. /// @return The stdAc equivalent of the native settings. 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(getModeEx()); result.celsius = true; result.degrees = getTemp(); result.sensorTemperature = getSensorTemp(); result.iFeel = getiFeel(); result.fanspeed = toCommonFanSpeed(getFanEx()); result.turbo = _.Max; result.sleep = _.Night ? 0 : -1; // Not supported. result.swingv = stdAc::swingv_t::kOff; result.swingh = stdAc::swingh_t::kOff; result.light = false; result.filter = false; result.econo = false; result.quiet = false; result.clean = false; result.beep = false; result.clock = -1; return result; } /// 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.sensorTemperature = getSensorTemp(); result.iFeel = getiFeel(); 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(); } // 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(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); result += addIntToString(_.Mode, kModeStr); result += kSpaceLBraceStr; switch (_.Mode) { case kArgoAuto: result += kAutoStr; break; case kArgoCool: result += kCoolStr; break; case kArgoHeat: result += kHeatStr; break; case kArgoDry: result += kDryStr; break; case kArgoHeatAuto: result += kHeatStr; result += ' '; result += kAutoStr; break; case kArgoOff: result += kOffStr; break; default: result += kUnknownStr; } result += ')'; result += addIntToString(_.Fan, kFanStr); result += kSpaceLBraceStr; switch (_.Fan) { case kArgoFanAuto: result += kAutoStr; break; case kArgoFan3: result += kMaxStr; break; case kArgoFan1: result += kMinStr; break; case kArgoFan2: result += kMedStr; break; default: result += kUnknownStr; } result += ')'; result += addTempToString(getTemp()); result += addTempToString(getSensorTemp(), true, true, true); result += addBoolToString(_.Max, kMaxStr); result += addBoolToString(_.iFeel, kIFeelStr); result += addBoolToString(_.Night, kNightStr); } 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) { uint8_t 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 argoWeekday& 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 { String commandTypeToString(argoIrMessageType_t type, uint8_t channel) { String result = irutils::irCommandTypeToString(to_underlying(type), to_underlying(argoIrMessageType_t::AC_CONTROL), to_underlying(argoIrMessageType_t::IFEEL_TEMP_REPORT), to_underlying(argoIrMessageType_t::TIMER_COMMAND), to_underlying(argoIrMessageType_t::CONFIG_PARAM_SET)); result += irutils::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 argoIrMessageType_t 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(getSensorTemp(), 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 += addTimerModeToString(to_underlying(getTimerType()), to_underlying(argoTimerType_t::NO_TIMER), to_underlying(argoTimerType_t::DELAY_TIMER), to_underlying(argoTimerType_t::SCHEDULE_TIMER_1), to_underlying(argoTimerType_t::SCHEDULE_TIMER_2), to_underlying(argoTimerType_t::SCHEDULE_TIMER_3)); 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(daysBitmaskToString( getTimerActiveDaysBitmap()), 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; } uint8_t preamble = state[0] & 0x0F; return preamble == kArgoWrem3Preamble; } #if DECODE_ARGO /// 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. /// @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 based soley off sendArgo(). We have no actual captures /// to test this against. If you have one of these units, please let us know. bool IRrecv::decodeArgo(decode_results *results, uint16_t offset, const uint16_t nbits, const bool strict) { if (strict && nbits != kArgoBits) return false; // Match Header + Data if (!matchGeneric(results->rawbuf + offset, results->state, results->rawlen - offset, nbits, kArgoHdrMark, kArgoHdrSpace, kArgoBitMark, kArgoOneSpace, kArgoBitMark, kArgoZeroSpace, 0, 0, // Footer (None, allegedly. This seems very wrong.) true, _tolerance, 0, false)) return false; // Compliance // Verify we got a valid checksum. if (strict && !IRArgoAC::validChecksum(results->state, kArgoStateLength)) { return false; } // Success 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; } /// 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; } 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, 0, false); if (!bytesRead) { return false; } // If 'strict', assert it is a valid WREM-3 'model' protocolar message // vs. just 'any ARGO' if (strict && !IRArgoAC_WREM3::isValidWrem3Message(results->state, nbits, true)) { 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 verifyChecksum Whether to perform checksum verification /// @return True if the message is a WREM-3 one bool IRArgoAC_WREM3::isValidWrem3Message(const uint8_t state[], const uint16_t nbits, bool verifyChecksum) { if ((nbits % 8) != 0) { return false; // WREM-3 protocol always has a full byte length commands } uint16_t stateLengthBytes = std::min(static_cast(nbits / 8), kStateSizeMax); if (!IRArgoAC_WREM3::hasValidPreamble(state, stateLengthBytes)) { return false; } argoIrMessageType_t messageType = IRArgoACBase ::getMessageType(state, stateLengthBytes); switch (messageType) { case argoIrMessageType_t::AC_CONTROL : if (stateLengthBytes != kArgo3AcControlStateLength) { return false; } break; case argoIrMessageType_t::CONFIG_PARAM_SET: if (stateLengthBytes != kArgo3ConfigStateLength) { return false; } break; case argoIrMessageType_t::TIMER_COMMAND: if (stateLengthBytes != kArgo3TimerStateLength) { return false; } break; case argoIrMessageType_t::IFEEL_TEMP_REPORT: if (stateLengthBytes != kArgo3iFeelReportStateLength) { return false; } break; default: return false; } // Compliance: Verify we got a valid checksum. if (verifyChecksum && !IRArgoAC_WREM3::validChecksum(state, stateLengthBytes)) { return false; } return true; } #endif // DECODE_ARGO // force template instantiation template class IRArgoACBase; template class IRArgoACBase;