// Copyright 2019-2020 David Conran /// @file /// @brief Support for Neoclima protocols. /// Analysis by crankyoldgit, AndreyShpilevoy, & griffisc306 /// Code by crankyoldgit /// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/764 /// @see https://drive.google.com/file/d/1kjYk4zS9NQcMQhFkak-L4mp4UuaAIesW/view /// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1260 #include "ir_Neoclima.h" #include #include #include "IRrecv.h" #include "IRsend.h" #include "IRtext.h" #include "IRutils.h" // Constants const uint16_t kNeoclimaHdrMark = 6112; const uint16_t kNeoclimaHdrSpace = 7391; const uint16_t kNeoclimaBitMark = 537; const uint16_t kNeoclimaOneSpace = 1651; const uint16_t kNeoclimaZeroSpace = 571; const uint32_t kNeoclimaMinGap = kDefaultMessageGap; using irutils::addBoolToString; using irutils::addFanToString; using irutils::addIntToString; using irutils::addLabeledString; using irutils::addModeToString; using irutils::addTempToString; #if SEND_NEOCLIMA /// Send a Neoclima message. /// Status: STABLE / Known to be working. /// @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::sendNeoclima(const unsigned char data[], const uint16_t nbytes, const uint16_t repeat) { // Set IR carrier frequency enableIROut(38); for (uint16_t i = 0; i <= repeat; i++) { sendGeneric(kNeoclimaHdrMark, kNeoclimaHdrSpace, kNeoclimaBitMark, kNeoclimaOneSpace, kNeoclimaBitMark, kNeoclimaZeroSpace, kNeoclimaBitMark, kNeoclimaHdrSpace, data, nbytes, 38000, false, 0, // Repeats are already handled. 50); // Extra footer. mark(kNeoclimaBitMark); space(kNeoclimaMinGap); } } #endif // SEND_NEOCLIMA /// 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? IRNeoclimaAc::IRNeoclimaAc(const uint16_t pin, const bool inverted, const bool use_modulation) : _irsend(pin, inverted, use_modulation) { stateReset(); } /// Reset the state of the remote to a known good state/sequence. void IRNeoclimaAc::stateReset(void) { static const uint8_t kReset[kNeoclimaStateLength] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x6A, 0x00, 0x2A, 0xA5}; setRaw(kReset); } /// Set up hardware to be able to send a message. void IRNeoclimaAc::begin(void) { _irsend.begin(); } /// Calculate the checksum for a given state. /// @param[in] state The array to calc the checksum of. /// @param[in] length The length/size of the array. /// @return The calculated checksum value. uint8_t IRNeoclimaAc::calcChecksum(const uint8_t state[], const uint16_t length) { if (length == 0) return state[0]; return sumBytes(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 length/size of the array. /// @return true, if the state has a valid checksum. Otherwise, false. bool IRNeoclimaAc::validChecksum(const uint8_t state[], const uint16_t length) { if (length < 2) return true; // No checksum to compare with. Assume okay. return (state[length - 1] == calcChecksum(state, length)); } /// Calculate & update the checksum for the internal state. /// @param[in] length The length/size of the internal state. void IRNeoclimaAc::checksum(uint16_t length) { if (length < 2) return; _.Sum = calcChecksum(_.raw, length); } #if SEND_NEOCLIMA /// Send the current internal state as an IR message. /// @param[in] repeat Nr. of times the message will be repeated. void IRNeoclimaAc::send(const uint16_t repeat) { _irsend.sendNeoclima(getRaw(), kNeoclimaStateLength, repeat); } #endif // SEND_NEOCLIMA /// Get a PTR to the internal state/code for this protocol. /// @return PTR to a code for this protocol based on the current internal state. uint8_t *IRNeoclimaAc::getRaw(void) { checksum(); return _.raw; } /// Set the internal state from a valid code for this protocol. /// @param[in] new_code A valid code for this protocol. /// @param[in] length The length/size of the new_code array. void IRNeoclimaAc::setRaw(const uint8_t new_code[], const uint16_t length) { std::memcpy(_.raw, new_code, std::min(length, kNeoclimaStateLength)); } /// Set the Button/Command pressed setting of the A/C. /// @param[in] button The value of the button/command that was pressed. void IRNeoclimaAc::setButton(const uint8_t button) { switch (button) { case kNeoclimaButtonPower: case kNeoclimaButtonMode: case kNeoclimaButtonTempUp: case kNeoclimaButtonTempDown: case kNeoclimaButtonSwing: case kNeoclimaButtonFanSpeed: case kNeoclimaButtonAirFlow: case kNeoclimaButtonHold: case kNeoclimaButtonSleep: case kNeoclimaButtonLight: case kNeoclimaButtonEye: case kNeoclimaButtonFollow: case kNeoclimaButtonIon: case kNeoclimaButtonFresh: case kNeoclimaButton8CHeat: case kNeoclimaButtonTurbo: case kNeoclimaButtonEcono: case kNeoclimaButtonTempUnit: _.Button = button; break; default: _.Button = kNeoclimaButtonPower; } } /// Get the Button/Command setting of the A/C. /// @return The value of the button/command that was pressed. uint8_t IRNeoclimaAc::getButton(void) const { return _.Button; } /// Set the requested power state of the A/C to on. void IRNeoclimaAc::on(void) { setPower(true); } /// Set the requested power state of the A/C to off. void IRNeoclimaAc::off(void) { setPower(false); } /// Change the power setting. /// @param[in] on true, the setting is on. false, the setting is off. void IRNeoclimaAc::setPower(const bool on) { _.Button = kNeoclimaButtonPower; _.Power = on; } /// Get the value of the current power setting. /// @return true, the setting is on. false, the setting is off. bool IRNeoclimaAc::getPower(void) const { return _.Power; } /// Set the operating mode of the A/C. /// @param[in] mode The desired operating mode. void IRNeoclimaAc::setMode(const uint8_t mode) { switch (mode) { case kNeoclimaDry: // In this mode fan speed always LOW setFan(kNeoclimaFanLow); // FALL THRU case kNeoclimaAuto: case kNeoclimaCool: case kNeoclimaFan: case kNeoclimaHeat: _.Mode = mode; _.Button = kNeoclimaButtonMode; break; default: // If we get an unexpected mode, default to AUTO. _.Mode = kNeoclimaAuto; _.Button = kNeoclimaButtonMode; } } /// Get the operating mode setting of the A/C. /// @return The current operating mode setting. uint8_t IRNeoclimaAc::getMode(void) const { return _.Mode; } /// 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 IRNeoclimaAc::convertMode(const stdAc::opmode_t mode) { switch (mode) { case stdAc::opmode_t::kCool: return kNeoclimaCool; case stdAc::opmode_t::kHeat: return kNeoclimaHeat; case stdAc::opmode_t::kDry: return kNeoclimaDry; case stdAc::opmode_t::kFan: return kNeoclimaFan; default: return kNeoclimaAuto; } } /// 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 IRNeoclimaAc::toCommonMode(const uint8_t mode) { switch (mode) { case kNeoclimaCool: return stdAc::opmode_t::kCool; case kNeoclimaHeat: return stdAc::opmode_t::kHeat; case kNeoclimaDry: return stdAc::opmode_t::kDry; case kNeoclimaFan: return stdAc::opmode_t::kFan; default: return stdAc::opmode_t::kAuto; } } /// Set the temperature. /// @param[in] temp The temperature in degrees celsius. /// @param[in] celsius Use Fahrenheit (false) or Celsius (true). void IRNeoclimaAc::setTemp(const uint8_t temp, const bool celsius) { uint8_t oldtemp = getTemp(); _.UseFah = !celsius; const uint8_t min_temp = celsius ? kNeoclimaMinTempC : kNeoclimaMinTempF; const uint8_t max_temp = celsius ? kNeoclimaMaxTempC : kNeoclimaMaxTempF; const uint8_t newtemp = std::min(max_temp, std::max(min_temp, temp)); if (oldtemp > newtemp) _.Button = kNeoclimaButtonTempDown; else if (newtemp > oldtemp) _.Button = kNeoclimaButtonTempUp; _.Temp = newtemp - min_temp; } /// Get the current temperature setting. /// @return The current setting for temp. in degrees. /// @note The units of the temperature (F/C) is determined by `getTempUnits()`. uint8_t IRNeoclimaAc::getTemp(void) const { const uint8_t min_temp = getTempUnits() ? kNeoclimaMinTempC : kNeoclimaMinTempF; return _.Temp + min_temp; } /// Set the speed of the fan. /// @param[in] speed The desired setting. 0-3, 0 is auto, 1-3 is the speed void IRNeoclimaAc::setFan(const uint8_t speed) { _.Button = kNeoclimaButtonFanSpeed; if (_.Mode == kNeoclimaDry) { // Dry mode only allows low speed. _.Fan = kNeoclimaFanLow; return; } switch (speed) { case kNeoclimaFanAuto: case kNeoclimaFanHigh: case kNeoclimaFanMed: case kNeoclimaFanLow: _.Fan = speed; break; default: // If we get an unexpected speed, default to Auto. _.Fan = kNeoclimaFanAuto; } } /// Get the current fan speed setting. /// @return The current fan speed/mode. uint8_t IRNeoclimaAc::getFan(void) const { return _.Fan; } /// 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 IRNeoclimaAc::convertFan(const stdAc::fanspeed_t speed) { switch (speed) { case stdAc::fanspeed_t::kMin: case stdAc::fanspeed_t::kLow: return kNeoclimaFanLow; case stdAc::fanspeed_t::kMedium: return kNeoclimaFanMed; case stdAc::fanspeed_t::kHigh: case stdAc::fanspeed_t::kMax: return kNeoclimaFanHigh; default: return kNeoclimaFanAuto; } } /// 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 IRNeoclimaAc::toCommonFanSpeed(const uint8_t speed) { switch (speed) { case kNeoclimaFanHigh: return stdAc::fanspeed_t::kMax; case kNeoclimaFanMed: return stdAc::fanspeed_t::kMedium; case kNeoclimaFanLow: return stdAc::fanspeed_t::kMin; default: return stdAc::fanspeed_t::kAuto; } } /// Set the Sleep setting of the A/C. /// @param[in] on true, the setting is on. false, the setting is off. void IRNeoclimaAc::setSleep(const bool on) { _.Button = kNeoclimaButtonSleep; _.Sleep = on; } /// Get the Sleep setting of the A/C. /// @return true, the setting is on. false, the setting is off. bool IRNeoclimaAc::getSleep(void) const { return _.Sleep; } /// Set the vertical swing setting of the A/C. /// @param[in] on true, the setting is on. false, the setting is off. void IRNeoclimaAc::setSwingV(const bool on) { _.Button = kNeoclimaButtonSwing; _.SwingV = (on ? kNeoclimaSwingVOn : kNeoclimaSwingVOff); } /// Get the vertical swing setting of the A/C. /// @return true, the setting is on. false, the setting is off. bool IRNeoclimaAc::getSwingV(void) const { return _.SwingV == kNeoclimaSwingVOn; } /// Set the horizontal swing setting of the A/C. /// @param[in] on true, the setting is on. false, the setting is off. void IRNeoclimaAc::setSwingH(const bool on) { _.Button = kNeoclimaButtonAirFlow; _.SwingH = !on; // Cleared when `on` } /// Get the horizontal swing (Air Flow) setting of the A/C. /// @return true, the setting is on. false, the setting is off. bool IRNeoclimaAc::getSwingH(void) const { return !_.SwingH; } /// Set the Turbo setting of the A/C. /// @param[in] on true, the setting is on. false, the setting is off. void IRNeoclimaAc::setTurbo(const bool on) { _.Button = kNeoclimaButtonTurbo; _.Turbo = on; } /// Get the Turbo setting of the A/C. /// @return true, the setting is on. false, the setting is off. bool IRNeoclimaAc::getTurbo(void) const { return _.Turbo; } /// Set the Economy (Energy Saver) setting of the A/C. /// @param[in] on true, the setting is on. false, the setting is off. void IRNeoclimaAc::setEcono(const bool on) { _.Button = kNeoclimaButtonEcono; _.Econo = on; } /// Get the Economy (Energy Saver) setting of the A/C. /// @return true, the setting is on. false, the setting is off. bool IRNeoclimaAc::getEcono(void) const { return _.Econo; } /// Set the Fresh (air) setting of the A/C. /// @param[in] on true, the setting is on. false, the setting is off. void IRNeoclimaAc::setFresh(const bool on) { _.Button = kNeoclimaButtonFresh; _.Fresh = on; } /// Get the Fresh (air) setting of the A/C. /// @return true, the setting is on. false, the setting is off. bool IRNeoclimaAc::getFresh(void) const { return _.Fresh; } /// Set the Hold setting of the A/C. /// @param[in] on true, the setting is on. false, the setting is off. void IRNeoclimaAc::setHold(const bool on) { _.Button = kNeoclimaButtonHold; _.Hold = on; } /// Get the Hold setting of the A/C. /// @return true, the setting is on. false, the setting is off. bool IRNeoclimaAc::getHold(void) const { return _.Hold; } /// Set the Ion (filter) setting of the A/C. /// @param[in] on true, the setting is on. false, the setting is off. void IRNeoclimaAc::setIon(const bool on) { _.Button = kNeoclimaButtonIon; _.Ion = on; } /// Get the Ion (filter) setting of the A/C. /// @return true, the setting is on. false, the setting is off. bool IRNeoclimaAc::getIon(void) const { return _.Ion; } /// Set the Light(LED display) setting of the A/C. /// @param[in] on true, the setting is on. false, the setting is off. void IRNeoclimaAc::setLight(const bool on) { _.Button = kNeoclimaButtonLight; _.Light = on; } /// Get the Light (LED display) setting of the A/C. /// @return true, the setting is on. false, the setting is off. bool IRNeoclimaAc::getLight(void) const { return _.Light; } /// Set the 8°C Heat setting of the A/C. /// @param[in] on true, the setting is on. false, the setting is off. /// @note This feature maintains the room temperature steadily at 8°C and /// prevents the room from freezing by activating the heating operation /// automatically when nobody is at home over a longer period during severe /// winter. void IRNeoclimaAc::set8CHeat(const bool on) { _.Button = kNeoclimaButton8CHeat; _.CHeat = on; } /// Get the 8°C Heat setting of the A/C. /// @return true, the setting is on. false, the setting is off. bool IRNeoclimaAc::get8CHeat(void) const { return _.CHeat; } /// Set the Eye (Sensor) setting of the A/C. /// @param[in] on true, the setting is on. false, the setting is off. void IRNeoclimaAc::setEye(const bool on) { _.Button = kNeoclimaButtonEye; _.Eye = on; } /// Get the Eye (Sensor) setting of the A/C. /// @return true, the setting is on. false, the setting is off. bool IRNeoclimaAc::getEye(void) const { return _.Eye; } /// Is the A/C unit using Fahrenheit or Celsius for temperature units. /// @return false, Fahrenheit. true, Celsius. bool IRNeoclimaAc::getTempUnits(void) const { return !_.UseFah; } /* DISABLED TODO(someone): Work out why "on" is either 0x5D or 0x5F void IRNeoclimaAc::setFollow(const bool on) { setButton(kNeoclimaButtonFollow); if (on) remote_state[8] = kNeoclimaFollowMe; else remote_state[8] = 0; } */ /// Get the Follow Me setting of the A/C. /// @return true, the setting is on. false, the setting is off. bool IRNeoclimaAc::getFollow(void) const { return (_.Follow & kNeoclimaFollowMe) == kNeoclimaFollowMe; } /// Convert the current internal state into its stdAc::state_t equivalent. /// @return The stdAc equivalent of the native settings. stdAc::state_t IRNeoclimaAc::toCommon(void) const { stdAc::state_t result{}; result.protocol = decode_type_t::NEOCLIMA; result.model = -1; // No models used. result.power = _.Power; result.mode = toCommonMode(_.Mode); result.celsius = getTempUnits(); result.degrees = getTemp(); result.fanspeed = toCommonFanSpeed(_.Fan); result.swingv = getSwingV() ? stdAc::swingv_t::kAuto : stdAc::swingv_t::kOff; result.swingh = getSwingH() ? stdAc::swingh_t::kAuto : stdAc::swingh_t::kOff; result.turbo = _.Turbo; result.econo = _.Econo; result.light = _.Light; result.filter = _.Ion; result.sleep = _.Sleep ? 0 : -1; // Not supported. result.quiet = false; result.clean = false; result.beep = false; result.clock = -1; return result; } /// Convert the current internal state into a human readable string. /// @return A human readable string. String IRNeoclimaAc::toString(void) const { String result = ""; result.reserve(110); // Reserve some heap for the string to reduce fragging. result += addBoolToString(_.Power, kPowerStr, false); result += addModeToString(_.Mode, kNeoclimaAuto, kNeoclimaCool, kNeoclimaHeat, kNeoclimaDry, kNeoclimaFan); result += addTempToString(getTemp(), getTempUnits()); result += addFanToString(_.Fan, kNeoclimaFanHigh, kNeoclimaFanLow, kNeoclimaFanAuto, kNeoclimaFanAuto, kNeoclimaFanMed); result += addBoolToString(getSwingV(), kSwingVStr); result += addBoolToString(getSwingH(), kSwingHStr); result += addBoolToString(_.Sleep, kSleepStr); result += addBoolToString(_.Turbo, kTurboStr); result += addBoolToString(_.Econo, kEconoStr); result += addBoolToString(_.Hold, kHoldStr); result += addBoolToString(_.Ion, kIonStr); result += addBoolToString(_.Eye, kEyeStr); result += addBoolToString(_.Light, kLightStr); result += addBoolToString(getFollow(), kFollowStr); result += addBoolToString(_.CHeat, k8CHeatStr); result += addBoolToString(_.Fresh, kFreshStr); result += addIntToString(_.Button, kButtonStr); result += kSpaceLBraceStr; switch (_.Button) { case kNeoclimaButtonPower: result += kPowerStr; break; case kNeoclimaButtonMode: result += kModeStr; break; case kNeoclimaButtonTempUp: result += kTempUpStr; break; case kNeoclimaButtonTempDown: result += kTempDownStr; break; case kNeoclimaButtonSwing: result += kSwingStr; break; case kNeoclimaButtonFanSpeed: result += kFanStr; break; case kNeoclimaButtonAirFlow: result += kAirFlowStr; break; case kNeoclimaButtonHold: result += kHoldStr; break; case kNeoclimaButtonSleep: result += kSleepStr; break; case kNeoclimaButtonLight: result += kLightStr; break; case kNeoclimaButtonEye: result += kEyeStr; break; case kNeoclimaButtonFollow: result += kFollowStr; break; case kNeoclimaButtonIon: result += kIonStr; break; case kNeoclimaButtonFresh: result += kFreshStr; break; case kNeoclimaButton8CHeat: result += k8CHeatStr; break; case kNeoclimaButtonTurbo: result += kTurboStr; break; case kNeoclimaButtonEcono: result += kEconoStr; break; case kNeoclimaButtonTempUnit: result += kCelsiusFahrenheitStr; break; default: result += kUnknownStr; } result += ')'; return result; } #if DECODE_NEOCLIMA /// Decode the supplied Neoclima message. /// Status: STABLE / Known working /// @param[in,out] results Ptr to the data to decode & where to store the 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 True if it can decode it, false if it can't. bool IRrecv::decodeNeoclima(decode_results *results, uint16_t offset, const uint16_t nbits, const bool strict) { // Compliance if (strict && nbits != kNeoclimaBits) return false; // Incorrect nr. of bits per spec. // Match Main Header + Data + Footer uint16_t used; used = matchGeneric(results->rawbuf + offset, results->state, results->rawlen - offset, nbits, kNeoclimaHdrMark, kNeoclimaHdrSpace, kNeoclimaBitMark, kNeoclimaOneSpace, kNeoclimaBitMark, kNeoclimaZeroSpace, kNeoclimaBitMark, kNeoclimaHdrSpace, false, _tolerance, 0, false); if (!used) return false; offset += used; // Extra footer. uint64_t unused; if (!matchGeneric(results->rawbuf + offset, &unused, results->rawlen - offset, 0, 0, 0, 0, 0, 0, 0, kNeoclimaBitMark, kNeoclimaHdrSpace, true)) return false; // Compliance if (strict) { // Check we got a valid checksum. if (!IRNeoclimaAc::validChecksum(results->state, nbits / 8)) return false; } // Success results->decode_type = decode_type_t::NEOCLIMA; 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; } #endif // DECODE_NEOCLIMA