Ecoclim: Add detailed A/C support (#1415)

* Add support for power, mode, temp, sensor temp, fan speed, clock, Timers, & DIP switch settings.
* Unit tests updated & extended to cover new features
* Update Common AC API (`stdAc`) with support for EcoClim.

* Rename `src` files to be consistent case.
* Fix minor issue with Daikin64 and clocks.

Fixes #1397
This commit is contained in:
David Conran
2021-02-17 18:59:52 +10:00
committed by GitHub
parent b4599e28d5
commit a2c1516f26
10 changed files with 854 additions and 121 deletions

View File

@@ -23,6 +23,7 @@
#include "ir_Coolix.h"
#include "ir_Corona.h"
#include "ir_Daikin.h"
#include "ir_Ecoclim.h"
#include "ir_Electra.h"
#include "ir_Fujitsu.h"
#include "ir_Haier.h"
@@ -182,6 +183,9 @@ bool IRac::isProtocolSupported(const decode_type_t protocol) {
#if SEND_DELONGHI_AC
case decode_type_t::DELONGHI_AC:
#endif
#if SEND_ECOCLIM
case decode_type_t::ECOCLIM:
#endif
#if SEND_ELECTRA_AC
case decode_type_t::ELECTRA_AC:
#endif
@@ -772,7 +776,7 @@ void IRac::daikin64(IRDaikin64 *ac,
ac->setTurbo(turbo);
ac->setQuiet(quiet);
ac->setSleep(sleep >= 0);
ac->setClock(clock);
if (clock >= 0) ac->setClock(clock);
ac->send();
}
#endif // SEND_DAIKIN64
@@ -802,6 +806,45 @@ void IRac::delonghiac(IRDelonghiAc *ac,
}
#endif // SEND_DELONGHI_AC
#if SEND_ECOCLIM
/// Send an EcoClim A/C message with the supplied settings.
/// @param[in, out] ac A Ptr to an IREcoclimAc object to use.
/// @param[in] on The power setting.
/// @param[in] mode The operation mode setting.
/// @param[in] degrees The temperature setting in degrees.
/// @param[in] fan The speed setting for the fan.
/// @param[in] sleep Nr. of minutes for sleep mode. -1 is Off, >= 0 is on.
/// @param[in] clock The time in Nr. of mins since midnight. < 0 is ignore.
void IRac::ecoclim(IREcoclimAc *ac,
const bool on, const stdAc::opmode_t mode,
const float degrees, const stdAc::fanspeed_t fan,
const int16_t sleep, const int16_t clock) {
ac->begin();
ac->setPower(on);
uint8_t new_mode;
if (sleep >= 0) // EcoClim has a descrete Sleep operation mode, not a setting
new_mode = kEcoclimSleep; // Override the requested operating mode.
else
new_mode = ac->convertMode(mode); // Not Sleep, so use the supplied mode.
ac->setMode(new_mode);
ac->setTemp(degrees);
ac->setSensorTemp(degrees); //< Set to the desired temp until we cab disable.
ac->setFan(ac->convertFan(fan));
// No SwingV setting available
// No SwingH setting available
// No Quiet setting available.
// No Turbo setting available.
// No Light setting available.
// No Econo setting available.
// No Filter setting available.
// No Clean setting available
// No Beep setting available.
// No Sleep setting available.
if (clock >= 0) ac->setClock(clock);
ac->send();
}
#endif // SEND_ECOCLIM
#if SEND_ELECTRA_AC
/// Send an Electra A/C message with the supplied settings.
/// @param[in, out] ac A Ptr to an IRElectraAc object to use.
@@ -830,7 +873,6 @@ void IRac::electra(IRElectraAc *ac,
// No Quiet setting available.
ac->setTurbo(turbo);
ac->setLightToggle(lighttoggle);
// No Light setting available.
// No Econo setting available.
// No Filter setting available.
ac->setClean(clean);
@@ -2385,6 +2427,14 @@ bool IRac::sendAc(const stdAc::state_t desired, const stdAc::state_t *prev) {
break;
}
#endif // SEND_DELONGHI_AC
#if SEND_ECOCLIM
case ECOCLIM:
{
IREcoclimAc ac(_pin, _inverted, _modulation);
ecoclim(&ac, send.power, send.mode, degC, send.fanspeed, send.clock);
break;
}
#endif // SEND_ECOCLIM
#if SEND_ELECTRA_AC
case ELECTRA_AC:
{
@@ -3163,6 +3213,16 @@ namespace IRAcUtils {
return ac.toString();
}
#endif // DECODE_DELONGHI_AC
#if DECODE_ECOCLIM
case decode_type_t::ECOCLIM: {
if (result->bits == kEcoclimBits) {
IREcoclimAc ac(kGpioUnused);
ac.setRaw(result->value); // EcoClim uses value instead of state.
return ac.toString();
}
return "";
}
#endif // DECODE_ECOCLIM
#if DECODE_ELECTRA_AC
case decode_type_t::ELECTRA_AC: {
IRElectraAc ac(kGpioUnused);
@@ -3563,6 +3623,18 @@ namespace IRAcUtils {
break;
}
#endif // DECODE_DELONGHI_AC
#if DECODE_ECOCLIM
case decode_type_t::ECOCLIM: {
if (decode->bits == kEcoclimBits) {
IREcoclimAc ac(kGpioUnused);
ac.setRaw(decode->value); // Uses value instead of state.
*result = ac.toCommon();
} else {
return false;
}
break;
}
#endif // DECODE_ECOCLIM
#if DECODE_ELECTRA_AC
case decode_type_t::ELECTRA_AC: {
IRElectraAc ac(kGpioUnused);

View File

@@ -16,6 +16,7 @@
#include "ir_Daikin.h"
#include "ir_Delonghi.h"
#include "ir_Fujitsu.h"
#include "ir_Ecoclim.h"
#include "ir_Electra.h"
#include "ir_Goodweather.h"
#include "ir_Gree.h"
@@ -206,6 +207,12 @@ void daikin216(IRDaikin216 *ac,
const float degrees, const stdAc::fanspeed_t fan,
const bool turbo, const int16_t sleep = -1);
#endif // SEND_DELONGHI_AC
#if SEND_ECOCLIM
void ecoclim(IREcoclimAc *ac,
const bool on, const stdAc::opmode_t mode,
const float degrees, const stdAc::fanspeed_t fan,
const int16_t sleep = -1, const int16_t clock = -1);
#endif // SEND_ECOCLIM
#if SEND_ELECTRA_AC
void electra(IRElectraAc *ac,
const bool on, const stdAc::opmode_t mode,

View File

@@ -106,6 +106,7 @@ const PROGMEM char* kHeatStr = D_STR_HEAT; ///< "Heat"
const PROGMEM char* kFanStr = D_STR_FAN; ///< "Fan"
const PROGMEM char* kDryStr = D_STR_DRY; ///< "Dry"
const PROGMEM char* kFanOnlyStr = D_STR_FANONLY; ///< "fan_only"
const PROGMEM char* kRecycleStr = D_STR_RECYCLE; ///< "Recycle"
const PROGMEM char* kMaxStr = D_STR_MAX; ///< "Max"
const PROGMEM char* kMaximumStr = D_STR_MAXIMUM; ///< "Maximum"

View File

@@ -114,6 +114,7 @@ extern const char* kPreviousPowerStr;
extern const char* kProtocolStr;
extern const char* kPurifyStr;
extern const char* kQuietStr;
extern const char* kRecycleStr;
extern const char* kRepeatStr;
extern const char* kRightMaxStr;
extern const char* kRightStr;

View File

@@ -1,114 +0,0 @@
// Copyright 2021 David Conran
/// @file
/// @brief EcoClim A/C protocol.
/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1397
// Supports:
// Brand: EcoClim, Model: HYSFR-P348 remote
// Brand: EcoClim, Model: ZC200DPO A/C
#include <algorithm>
#include <cstring>
#include "IRrecv.h"
#include "IRsend.h"
#include "IRtext.h"
#include "IRutils.h"
// Constants
const uint8_t kEcoclimSections = 3;
const uint8_t kEcoclimExtraTolerance = 5; ///< Percentage (extra)
const uint16_t kEcoclimHdrMark = 5730; ///< uSeconds
const uint16_t kEcoclimHdrSpace = 1935; ///< uSeconds
const uint16_t kEcoclimBitMark = 440; ///< uSeconds
const uint16_t kEcoclimOneSpace = 1739; ///< uSeconds
const uint16_t kEcoclimZeroSpace = 637; ///< uSeconds
const uint16_t kEcoclimFooterMark = 7820; ///< uSeconds
const uint32_t kEcoclimGap = kDefaultMessageGap; // Just a guess.
#if SEND_ECOCLIM
/// Send a EcoClim A/C formatted message.
/// Status: Alpha / Completely untested.
/// @param[in] data The message to be sent.
/// @param[in] nbits The number of bits of message to be sent.
/// @param[in] repeat The number of times the command is to be repeated.
void IRsend::sendEcoclim(const uint64_t data, const uint16_t nbits,
const uint16_t repeat) {
enableIROut(38, kDutyDefault);
for (uint16_t r = 0; r <= repeat; r++) {
for (uint8_t section = 0; section < kEcoclimSections; section++) {
// Header + Data
sendGeneric(kEcoclimHdrMark, kEcoclimHdrSpace,
kEcoclimBitMark, kEcoclimOneSpace,
kEcoclimBitMark, kEcoclimZeroSpace,
0, 0, data, nbits, 38, true, 0, kDutyDefault);
}
mark(kEcoclimFooterMark);
space(kEcoclimGap);
}
}
#endif // SEND_ECOCLIM
#if DECODE_ECOCLIM
/// Decode the supplied EcoClim A/C message.
/// Status: STABLE / Confirmed working on real remote.
/// @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.
bool IRrecv::decodeEcoclim(decode_results *results, uint16_t offset,
const uint16_t nbits, const bool strict) {
if (results->rawlen < (2 * nbits + kHeader) * kEcoclimSections +
kFooter - 1 + offset)
return false; // Can't possibly be a valid Ecoclim message.
if (strict) {
switch (nbits) {
case kEcoclimShortBits:
case kEcoclimBits:
break;
default:
return false; // Unexpected bit size.
}
}
for (uint8_t section = 0; section < kEcoclimSections; section++) {
uint16_t used;
uint64_t data;
// Header + Data Block
used = matchGeneric(results->rawbuf + offset, &data,
results->rawlen - offset, nbits,
kEcoclimHdrMark, kEcoclimHdrSpace,
kEcoclimBitMark, kEcoclimOneSpace,
kEcoclimBitMark, kEcoclimZeroSpace,
0, 0, // No footer.
false, _tolerance + kEcoclimExtraTolerance);
if (!used) return false;
DPRINTLN("DEBUG: Data section matched okay.");
offset += used;
// Compliance
if (strict) {
if (section) { // Each section should contain the same data.
if (data != results->value) return false;
} else {
results->value = data;
}
}
}
// Footer
if (!matchMark(results->rawbuf[offset++], kEcoclimFooterMark,
_tolerance + kEcoclimExtraTolerance))
return false;
if (results->rawlen <= offset && !matchAtLeast(results->rawbuf[offset++],
kEcoclimGap))
return false;
// Success
results->bits = nbits;
results->decode_type = ECOCLIM;
// No need to record the value as we stored it as we decoded it.
return true;
}
#endif // DECODE_ECOCLIM

423
src/ir_Ecoclim.cpp Normal file
View File

@@ -0,0 +1,423 @@
// Copyright 2021 David Conran
/// @file
/// @brief EcoClim A/C protocol.
/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1397
#include "ir_Ecoclim.h"
#include <algorithm>
#include <cstring>
#include "IRac.h"
#include "IRrecv.h"
#include "IRsend.h"
#include "IRtext.h"
#include "IRutils.h"
// Constants
const uint8_t kEcoclimSections = 3;
const uint8_t kEcoclimExtraTolerance = 5; ///< Percentage (extra)
const uint16_t kEcoclimHdrMark = 5730; ///< uSeconds
const uint16_t kEcoclimHdrSpace = 1935; ///< uSeconds
const uint16_t kEcoclimBitMark = 440; ///< uSeconds
const uint16_t kEcoclimOneSpace = 1739; ///< uSeconds
const uint16_t kEcoclimZeroSpace = 637; ///< uSeconds
const uint16_t kEcoclimFooterMark = 7820; ///< uSeconds
const uint32_t kEcoclimGap = kDefaultMessageGap; // Just a guess.
using irutils::addBoolToString;
using irutils::addFanToString;
using irutils::addIntToString;
using irutils::addLabeledString;
using irutils::addModeToString;
using irutils::addTempToString;
using irutils::minsToString;
#if SEND_ECOCLIM
/// Send a EcoClim A/C formatted message.
/// Status: STABLE / Confirmed working on real device.
/// @param[in] data The message to be sent.
/// @param[in] nbits The number of bits of message to be sent.
/// @param[in] repeat The number of times the command is to be repeated.
void IRsend::sendEcoclim(const uint64_t data, const uint16_t nbits,
const uint16_t repeat) {
enableIROut(38, kDutyDefault);
for (uint16_t r = 0; r <= repeat; r++) {
for (uint8_t section = 0; section < kEcoclimSections; section++) {
// Header + Data
sendGeneric(kEcoclimHdrMark, kEcoclimHdrSpace,
kEcoclimBitMark, kEcoclimOneSpace,
kEcoclimBitMark, kEcoclimZeroSpace,
0, 0, data, nbits, 38, true, 0, kDutyDefault);
}
mark(kEcoclimFooterMark);
space(kEcoclimGap);
}
}
#endif // SEND_ECOCLIM
#if DECODE_ECOCLIM
/// Decode the supplied EcoClim A/C message.
/// Status: STABLE / Confirmed working on real remote.
/// @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.
bool IRrecv::decodeEcoclim(decode_results *results, uint16_t offset,
const uint16_t nbits, const bool strict) {
if (results->rawlen < (2 * nbits + kHeader) * kEcoclimSections +
kFooter - 1 + offset)
return false; // Can't possibly be a valid Ecoclim message.
if (strict) {
switch (nbits) {
case kEcoclimShortBits:
case kEcoclimBits:
break;
default:
return false; // Unexpected bit size.
}
}
for (uint8_t section = 0; section < kEcoclimSections; section++) {
uint16_t used;
uint64_t data;
// Header + Data Block
used = matchGeneric(results->rawbuf + offset, &data,
results->rawlen - offset, nbits,
kEcoclimHdrMark, kEcoclimHdrSpace,
kEcoclimBitMark, kEcoclimOneSpace,
kEcoclimBitMark, kEcoclimZeroSpace,
0, 0, // No footer.
false, _tolerance + kEcoclimExtraTolerance);
if (!used) return false;
DPRINTLN("DEBUG: Data section matched okay.");
offset += used;
// Compliance
if (strict) {
if (section) { // Each section should contain the same data.
if (data != results->value) return false;
} else {
results->value = data;
}
}
}
// Footer
if (!matchMark(results->rawbuf[offset++], kEcoclimFooterMark,
_tolerance + kEcoclimExtraTolerance))
return false;
if (results->rawlen <= offset && !matchAtLeast(results->rawbuf[offset++],
kEcoclimGap))
return false;
// Success
results->bits = nbits;
results->decode_type = ECOCLIM;
// No need to record the value as we stored it as we decoded it.
return true;
}
#endif // DECODE_ECOCLIM
/// 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?
IREcoclimAc::IREcoclimAc(const uint16_t pin, const bool inverted,
const bool use_modulation)
: _irsend(pin, inverted, use_modulation) { stateReset(); }
/// Reset the internal state to a fixed known good state.
void IREcoclimAc::stateReset(void) { _.raw = kEcoclimDefaultState; }
/// Set up hardware to be able to send a message.
void IREcoclimAc::begin(void) { _irsend.begin(); }
#if SEND_ECOCLIM
/// Send the current internal state as an IR message.
/// @param[in] repeat Nr. of times the message will be repeated.
void IREcoclimAc::send(const uint16_t repeat) {
_irsend.sendEcoclim(getRaw(), kEcoclimBits, repeat);
}
#endif // SEND_ECOCLIM
/// Get a copy of the internal state as a valid code for this protocol.
/// @return A valid code for this protocol based on the current internal state.
uint64_t IREcoclimAc::getRaw(void) const { return _.raw; }
/// Set the internal state from a valid code for this protocol.
/// @param[in] new_code A valid code for this protocol.
void IREcoclimAc::setRaw(const uint64_t new_code) { _.raw = new_code; }
/// Set the temperature.
/// @param[in] celsius The temperature in degrees celsius.
void IREcoclimAc::setTemp(const uint8_t celsius) {
// Range check.
uint8_t temp = std::min(celsius, kEcoclimTempMax);
temp = std::max(temp, kEcoclimTempMin);
_.Temp = temp - kEcoclimTempMin;
}
/// Get the current temperature setting.
/// @return The current setting for temp. in degrees celsius.
uint8_t IREcoclimAc::getTemp(void) const { return _.Temp + kEcoclimTempMin; }
/// Set the sensor temperature.
/// @param[in] celsius The temperature in degrees celsius.
void IREcoclimAc::setSensorTemp(const uint8_t celsius) {
// Range check.
uint8_t temp = std::min(celsius, kEcoclimTempMax);
temp = std::max(temp, kEcoclimTempMin);
_.SensorTemp = temp - kEcoclimTempMin;
}
/// Get the sensor temperature setting.
/// @return The current setting for sensor temp. in degrees celsius.
uint8_t IREcoclimAc::getSensorTemp(void) const {
return _.SensorTemp + kEcoclimTempMin;
}
/// Get the value of the current power setting.
/// @return true, the setting is on. false, the setting is off.
bool IREcoclimAc::getPower(void) const { return _.Power; }
/// Change the power setting.
/// @param[in] on true, the setting is on. false, the setting is off.
void IREcoclimAc::setPower(const bool on) { _.Power = on; }
/// Change the power setting to On.
void IREcoclimAc::on(void) { setPower(true); }
/// Change the power setting to Off.
void IREcoclimAc::off(void) { setPower(false); }
/// Get the current fan speed setting.
/// @return The current fan speed.
uint8_t IREcoclimAc::getFan(void) const { return _.Fan; }
/// Set the speed of the fan.
/// @param[in] speed The desired setting.
void IREcoclimAc::setFan(const uint8_t speed) {
_.Fan = std::min(speed, kEcoclimFanAuto);
}
/// 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 IREcoclimAc::convertFan(const stdAc::fanspeed_t speed) {
switch (speed) {
case stdAc::fanspeed_t::kMin:
case stdAc::fanspeed_t::kLow: return kEcoclimFanMin;
case stdAc::fanspeed_t::kMedium: return kEcoclimFanMed;
case stdAc::fanspeed_t::kHigh:
case stdAc::fanspeed_t::kMax: return kEcoclimFanMax;
default: return kCoolixFanAuto;
}
}
/// 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 IREcoclimAc::toCommonFanSpeed(const uint8_t speed) {
switch (speed) {
case kEcoclimFanMax: return stdAc::fanspeed_t::kMax;
case kEcoclimFanMed: return stdAc::fanspeed_t::kMedium;
case kEcoclimFanMin: return stdAc::fanspeed_t::kMin;
default: return stdAc::fanspeed_t::kAuto;
}
}
/// Get the operating mode setting of the A/C.
/// @return The current operating mode setting.
uint8_t IREcoclimAc::getMode(void) const { return _.Mode; }
/// Set the operating mode of the A/C.
/// @param[in] mode The desired operating mode.
void IREcoclimAc::setMode(const uint8_t mode) {
switch (mode) {
case kEcoclimAuto:
case kEcoclimCool:
case kEcoclimDry:
case kEcoclimRecycle:
case kEcoclimFan:
case kEcoclimHeat:
case kEcoclimSleep:
_.Mode = mode;
break;
default: // Anything else, go with Auto mode.
setMode(kEcoclimAuto);
}
}
/// Convert a standard A/C mode into its native mode.
/// @param[in] mode A stdAc::opmode_t to be converted to it's native equivalent.
/// @return The corresponding native mode.
uint8_t IREcoclimAc::convertMode(const stdAc::opmode_t mode) {
switch (mode) {
case stdAc::opmode_t::kCool: return kEcoclimCool;
case stdAc::opmode_t::kHeat: return kEcoclimHeat;
case stdAc::opmode_t::kDry: return kEcoclimDry;
case stdAc::opmode_t::kFan: return kEcoclimFan;
default: return kEcoclimAuto;
}
}
/// Convert a native mode to it's common stdAc::opmode_t equivalent.
/// @param[in] mode A native operation mode to be converted.
/// @return The corresponding common stdAc::opmode_t mode.
stdAc::opmode_t IREcoclimAc::toCommonMode(const uint8_t mode) {
switch (mode) {
case kEcoclimCool: return stdAc::opmode_t::kCool;
case kEcoclimHeat: return stdAc::opmode_t::kHeat;
case kEcoclimDry: return stdAc::opmode_t::kDry;
case kEcoclimFan: return stdAc::opmode_t::kFan;
default: return stdAc::opmode_t::kAuto;
}
}
/// Get the clock time of the A/C unit.
/// @return Nr. of minutes past midnight.
uint16_t IREcoclimAc::getClock(void) const { return _.Clock; }
/// Set the clock time on the A/C unit.
/// @param[in] nr_of_mins Nr. of minutes past midnight.
void IREcoclimAc::setClock(const uint16_t nr_of_mins) {
_.Clock = std::min(nr_of_mins, (uint16_t)(24 * 60 - 1));
}
/// Get the Unit type/DIP switch settings of the remote.
/// @return The binary representation of the 4 DIP switches on the remote.
uint8_t IREcoclimAc::getType(void) const { return _.DipConfig; }
/// Set the Unit type/DIP switch settings for the remote.
/// @param[in] code The binary representation of the remote's 4 DIP switches.
void IREcoclimAc::setType(const uint8_t code) {
switch (code) {
case kEcoclimDipMaster:
case kEcoclimDipSlave:
_.DipConfig = code;
break;
default:
setType(kEcoclimDipMaster);
}
}
/// Set & enable the On Timer for the A/C.
/// @param[in] nr_of_mins The time, in minutes since midnight.
void IREcoclimAc::setOnTimer(const uint16_t nr_of_mins) {
if (nr_of_mins < 24 * 60) {
_.OnHours = nr_of_mins / 60;
_.OnTenMins = (nr_of_mins % 60) / 10; // Store in tens of mins resolution.
}
}
/// Get the On Timer for the A/C.
/// @return The On Time, in minutes since midnight.
uint16_t IREcoclimAc::getOnTimer(void) const {
return _.OnHours * 60 + _.OnTenMins * 10;
}
/// Check if the On Timer is enabled.
/// @return true, if the timer is enabled, otherwise false.
bool IREcoclimAc::isOnTimerEnabled(void) const {
return (getOnTimer() != kEcoclimTimerDisable);
}
/// Disable & clear the On Timer.
void IREcoclimAc::disableOnTimer(void) {
_.OnHours = 0x1F;
_.OnTenMins = 0x7;
}
/// Set & enable the Off Timer for the A/C.
/// @param[in] nr_of_mins The time, in minutes since midnight.
void IREcoclimAc::setOffTimer(const uint16_t nr_of_mins) {
if (nr_of_mins < 24 * 60) {
_.OffHours = nr_of_mins / 60;
_.OffTenMins = (nr_of_mins % 60) / 10; // Store in tens of mins resolution.
}
}
/// Get the Off Timer for the A/C.
/// @return The Off Time, in minutes since midnight.
uint16_t IREcoclimAc::getOffTimer(void) const {
return _.OffHours * 60 + _.OffTenMins * 10;
}
/// Check if the Off Timer is enabled.
/// @return true, if the timer is enabled, otherwise false.
bool IREcoclimAc::isOffTimerEnabled(void) const {
return (getOffTimer() != kEcoclimTimerDisable);
}
/// Disable & clear the Off Timer.
void IREcoclimAc::disableOffTimer(void) {
_.OffHours = 0x1F;
_.OffTenMins = 0x7;
}
/// Convert the current internal state into its stdAc::state_t equivalent.
/// @return The stdAc equivalent of the native settings.
stdAc::state_t IREcoclimAc::toCommon(void) const {
stdAc::state_t result;
result.protocol = decode_type_t::ECOCLIM;
result.power = _.Power;
result.mode = toCommonMode(getMode());
result.celsius = true;
result.degrees = getTemp();
result.fanspeed = toCommonFanSpeed(_.Fan);
result.sleep = (getMode() == kEcoclimSleep) ? 0 : -1;
result.clock = getClock();
// Not supported.
result.model = -1;
result.turbo = false;
result.swingv = stdAc::swingv_t::kOff;
result.swingh = stdAc::swingh_t::kOff;
result.light = false;
result.filter = false;
result.econo = false;
result.quiet = false;
result.clean = false;
result.beep = false;
return result;
}
/// Convert the internal state into a human readable string.
/// @return A string containing the settings in human-readable form.
String IREcoclimAc::toString(void) const {
String result = "";
result.reserve(140); // Reserve some heap for the string to reduce fragging.
result += addBoolToString(_.Power, kPowerStr, false);
// Custom Mode output as this protocol has Recycle and Sleep as modes.
result += addIntToString(_.Mode, kModeStr);
result += kSpaceLBraceStr;
switch (_.Mode) {
case kEcoclimAuto: result += kAutoStr; break;
case kEcoclimCool: result += kCoolStr; break;
case kEcoclimHeat: result += kHeatStr; break;
case kEcoclimDry: result += kDryStr; break;
case kEcoclimFan: result += kFanStr; break;
case kEcoclimRecycle: result += kRecycleStr; break;
case kEcoclimSleep: result += kSleepStr; break;
default: result += kUnknownStr;
}
result += ')';
result += addTempToString(getTemp());
result += kCommaSpaceStr;
result += kSensorStr;
result += addTempToString(getSensorTemp(), true, false);
result += addFanToString(_.Fan, kEcoclimFanMax,
kEcoclimFanMin,
kEcoclimFanAuto,
kEcoclimFanAuto, // Unused (No Quiet)
kEcoclimFanMed,
kEcoclimFanMax);
result += addLabeledString(minsToString(_.Clock), kClockStr);
result += addLabeledString(
isOnTimerEnabled() ? minsToString(getOnTimer()) : kOffStr, kOnTimerStr);
result += addLabeledString(
isOffTimerEnabled() ? minsToString(getOffTimer()) : kOffStr,
kOffTimerStr);
result += addIntToString(_.DipConfig, kTypeStr);
return result;
}

142
src/ir_Ecoclim.h Normal file
View File

@@ -0,0 +1,142 @@
// Copyright 2021 David Conran
/// @file
/// @brief EcoClim A/C protocol.
/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1397
// Supports:
// Brand: EcoClim, Model: HYSFR-P348 remote
// Brand: EcoClim, Model: ZC200DPO A/C
#ifndef IR_ECOCLIM_H_
#define IR_ECOCLIM_H_
#define __STDC_LIMIT_MACROS
#include <stdint.h>
#ifndef UNIT_TEST
#include <Arduino.h>
#endif
#include "IRremoteESP8266.h"
#include "IRsend.h"
#ifdef UNIT_TEST
#include "IRsend_test.h"
#endif
// Constants
// Modes
const uint8_t kEcoclimAuto = 0b000; ///< 0. a.k.a Slave
const uint8_t kEcoclimCool = 0b001; ///< 1
const uint8_t kEcoclimDry = 0b010; ///< 2
const uint8_t kEcoclimRecycle = 0b011; ///< 3
const uint8_t kEcoclimFan = 0b100; ///< 4
const uint8_t kEcoclimHeat = 0b101; ///< 5
const uint8_t kEcoclimSleep = 0b111; ///< 7
// Fan Control
const uint8_t kEcoclimFanMin = 0b00; ///< 0
const uint8_t kEcoclimFanMed = 0b01; ///< 1
const uint8_t kEcoclimFanMax = 0b10; ///< 2
const uint8_t kEcoclimFanAuto = 0b11; ///< 3
// DIP settings
const uint8_t kEcoclimDipMaster = 0b0000;
const uint8_t kEcoclimDipSlave = 0b0111;
// Temperature
const uint8_t kEcoclimTempMin = 5; // Celsius
const uint8_t kEcoclimTempMax = kEcoclimTempMin + 31; // Celsius
// Timer
const uint16_t kEcoclimTimerDisable = 0x1F * 60 + 7 * 10; // 4774
// Power: Off, Mode: Auto, Temp: 11C, Sensor: 22C, Fan: Auto, Clock: 00:00
const uint64_t kEcoclimDefaultState = 0x11063000FFFF02;
/// Native representation of a Ecoclim A/C message.
union EcoclimProtocol {
uint64_t raw; ///< The state in IR code form.
struct { // Only 56 bits (7 bytes are used.
// Byte
uint64_t :3; ///< Fixed 0b010
uint64_t :1; ///< Unknown
uint64_t DipConfig :4; ///< 0b0000 = Master, 0b0111 = Slave
// Byte
uint64_t OffTenMins :3; ///< Off Timer minutes (in tens of mins)
uint64_t OffHours :5; ///< Off Timer nr of Hours
// Byte
uint64_t OnTenMins :3; ///< On Timer minutes (in tens of mins)
uint64_t OnHours :5; ///< On Timer nr of Hours
// Byte+Byte
uint64_t Clock :11;
uint64_t :1; ///< Unknown
uint64_t Fan :2; ///< Fan Speed
uint64_t Power :1; ///< Power control
uint64_t Clear :1; // Not sure what this is
// Byte
uint64_t Temp :5; ///< Desired Temperature (Celsius)
uint64_t Mode :3; ///< Operating Mode
// Byte
uint64_t SensorTemp :5; ///< Sensed Temperature (Celsius)
uint64_t :3; ///< Fixed
};
};
// Classes
/// Class for handling detailed EcoClim A/C 56 bit messages.
/// @see https://github.com/crankyoldgit/IRremoteESP8266/issues/1397
class IREcoclimAc {
public:
explicit IREcoclimAc(const uint16_t pin, const bool inverted = false,
const bool use_modulation = true);
void stateReset(void);
#if SEND_ECOCLIM
void send(const uint16_t repeat = kNoRepeat);
/// Run the calibration to calculate uSec timing offsets for this platform.
/// @return The uSec timing offset needed per modulation of the IR Led.
/// @note This will produce a 65ms IR signal pulse at 38kHz.
/// Only ever needs to be run once per object instantiation, if at all.
int8_t calibrate(void) { return _irsend.calibrate(); }
#endif // SEND_ECOCLIM
void begin(void);
void on(void);
void off(void);
void setPower(const bool on);
bool getPower(void) const;
void setTemp(const uint8_t celsius);
uint8_t getTemp(void) const;
void setSensorTemp(const uint8_t celsius);
uint8_t getSensorTemp(void) const;
void setFan(const uint8_t speed);
uint8_t getFan(void) const;
void setMode(const uint8_t mode);
uint8_t getMode(void) const;
void setClock(const uint16_t nr_of_mins);
uint16_t getClock(void) const;
uint64_t getRaw(void) const;
void setRaw(const uint64_t new_code);
void setType(const uint8_t code);
uint8_t getType(void) const;
static uint8_t convertMode(const stdAc::opmode_t mode);
static uint8_t convertFan(const stdAc::fanspeed_t speed);
static stdAc::opmode_t toCommonMode(const uint8_t mode);
static stdAc::fanspeed_t toCommonFanSpeed(const uint8_t speed);
stdAc::state_t toCommon(void) const;
void setOnTimer(const uint16_t nr_of_mins);
uint16_t getOnTimer(void) const;
bool isOnTimerEnabled(void) const;
void disableOnTimer(void);
void setOffTimer(const uint16_t nr_of_mins);
uint16_t getOffTimer(void) const;
bool isOffTimerEnabled(void) const;
void disableOffTimer(void);
String toString(void) const;
#ifndef UNIT_TEST
private:
IRsend _irsend; ///< Instance of the IR send class
#else // UNIT_TEST
/// @cond IGNORE
IRsendTest _irsend; ///< Instance of the testing IR send class
/// @endcond
#endif // UNIT_TEST
EcoclimProtocol _; ///< The state of the IR remote in IR code form.
};
#endif // IR_ECOCLIM_H_

View File

@@ -270,6 +270,9 @@
#ifndef D_STR_SPECIAL
#define D_STR_SPECIAL "Special"
#endif // D_STR_SPECIAL
#ifndef D_STR_RECYCLE
#define D_STR_RECYCLE "Recycle"
#endif // D_STR_RECYCLE
#ifndef D_STR_AUTO
#define D_STR_AUTO "Auto"

View File

@@ -9,6 +9,7 @@
#include "ir_Corona.h"
#include "ir_Daikin.h"
#include "ir_Delonghi.h"
#include "ir_Ecoclim.h"
#include "ir_Electra.h"
#include "ir_Fujitsu.h"
#include "ir_Goodweather.h"
@@ -507,6 +508,49 @@ TEST(TestIRac, DelonghiAc) {
ASSERT_EQ(expected, IRAcUtils::resultAcToString(&ac._irsend.capture));
}
TEST(TestIRac, Ecoclim) {
IREcoclimAc ac(kGpioUnused);
IRac irac(kGpioUnused);
IRrecv capture(kGpioUnused);
char expected[] =
"Power: On, Mode: 1 (Cool), Temp: 26C, SensorTemp: 26C, Fan: 2 (High), "
"Clock: 12:34, On Timer: Off, Off Timer: Off, Type: 0";
ac.begin();
irac.ecoclim(&ac,
true, // Power
stdAc::opmode_t::kCool, // Mode
26, // Celsius
stdAc::fanspeed_t::kHigh, // Fan speed
-1, // Sleep
12 * 60 + 34); // Clock
ASSERT_EQ(expected, ac.toString());
ac._irsend.makeDecodeResult();
EXPECT_TRUE(capture.decode(&ac._irsend.capture));
ASSERT_EQ(ECOCLIM, ac._irsend.capture.decode_type);
ASSERT_EQ(kEcoclimBits, ac._irsend.capture.bits);
ASSERT_EQ(expected, IRAcUtils::resultAcToString(&ac._irsend.capture));
char expected_sleep[] =
"Power: On, Mode: 7 (Sleep), Temp: 21C, SensorTemp: 21C, Fan: 0 (Low), "
"Clock: 17:17, On Timer: Off, Off Timer: Off, Type: 0";
ac._irsend.reset();
irac.ecoclim(&ac,
true, // Power
stdAc::opmode_t::kCool, // Mode
21, // Celsius
stdAc::fanspeed_t::kLow, // Fan speed
8 * 60, // Sleep
17 * 60 + 17); // Clock
ASSERT_EQ(expected_sleep, ac.toString());
ac._irsend.makeDecodeResult();
EXPECT_TRUE(capture.decode(&ac._irsend.capture));
ASSERT_EQ(ECOCLIM, ac._irsend.capture.decode_type);
ASSERT_EQ(kEcoclimBits, ac._irsend.capture.bits);
ASSERT_EQ(expected_sleep, IRAcUtils::resultAcToString(&ac._irsend.capture));
}
TEST(TestIRac, Electra) {
IRElectraAc ac(kGpioUnused);
IRac irac(kGpioUnused);

View File

@@ -1,5 +1,7 @@
// Copyright 2021 David Conran
#include "ir_Ecoclim.h"
#include <algorithm>
#include "IRac.h"
#include "IRrecv.h"
#include "IRrecv_test.h"
@@ -12,7 +14,7 @@ TEST(TestUtils, Housekeeping) {
ASSERT_EQ("ECOCLIM", typeToString(decode_type_t::ECOCLIM));
ASSERT_EQ(decode_type_t::ECOCLIM, strToDecodeType("ECOCLIM"));
ASSERT_FALSE(hasACState(decode_type_t::ECOCLIM));
ASSERT_FALSE(IRac::isProtocolSupported(decode_type_t::ECOCLIM));
ASSERT_TRUE(IRac::isProtocolSupported(decode_type_t::ECOCLIM));
ASSERT_EQ(kEcoclimBits, IRsend::defaultBits(decode_type_t::ECOCLIM));
ASSERT_EQ(kNoRepeat, IRsend::minRepeats(decode_type_t::ECOCLIM));
}
@@ -174,9 +176,10 @@ TEST(TestDecodeEcoclim, RealExample) {
EXPECT_EQ(kEcoclimBits, irsend.capture.bits);
EXPECT_EQ(0x110673AEFFFF72, irsend.capture.value);
EXPECT_EQ(
"",
"Power: On, Mode: 0 (Auto), Temp: 11C, SensorTemp: 22C, Fan: 3 (Auto), "
"Clock: 15:42, On Timer: Off, Off Timer: Off, Type: 7",
IRAcUtils::resultAcToString(&irsend.capture));
ASSERT_FALSE(IRAcUtils::decodeToState(&irsend.capture, &r, &p));
ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &r, &p));
irsend.reset();
irsend.sendRaw(short_rawData, 97, 38000);
@@ -198,7 +201,158 @@ TEST(TestDecodeEcoclim, RealExample) {
EXPECT_EQ(kEcoclimBits, irsend.capture.bits);
EXPECT_EQ(0x15594507FFFF0A, irsend.capture.value);
EXPECT_EQ(
"",
"Power: On, Mode: 2 (Dry), Temp: 30C, SensorTemp: 26C, Fan: 0 (Low), "
"Clock: 21:27, On Timer: Off, Off Timer: Off, Type: 0",
IRAcUtils::resultAcToString(&irsend.capture));
ASSERT_FALSE(IRAcUtils::decodeToState(&irsend.capture, &r, &p));
ASSERT_TRUE(IRAcUtils::decodeToState(&irsend.capture, &r, &p));
}
TEST(TestIREcoclimAcClass, Power) {
IREcoclimAc ac(kGpioUnused);
ac.begin();
ac.on();
EXPECT_TRUE(ac.getPower());
ac.off();
EXPECT_FALSE(ac.getPower());
ac.setPower(true);
EXPECT_TRUE(ac.getPower());
ac.setPower(false);
EXPECT_FALSE(ac.getPower());
}
TEST(TestIREcoclimAcClass, SetAndGetTemp) {
IREcoclimAc ac(kGpioUnused);
ac.setTemp(25);
EXPECT_EQ(25, ac.getTemp());
ac.setTemp(kEcoclimTempMin);
EXPECT_EQ(kEcoclimTempMin, ac.getTemp());
ac.setTemp(kEcoclimTempMin - 1);
EXPECT_EQ(kEcoclimTempMin, ac.getTemp());
ac.setTemp(kEcoclimTempMax);
EXPECT_EQ(kEcoclimTempMax, ac.getTemp());
ac.setTemp(kEcoclimTempMax + 1);
EXPECT_EQ(kEcoclimTempMax, ac.getTemp());
}
TEST(TestIREcoclimAcClass, SetAndGetSensorTemp) {
IREcoclimAc ac(kGpioUnused);
ac.setSensorTemp(25);
EXPECT_EQ(25, ac.getSensorTemp());
ac.setSensorTemp(kEcoclimTempMin);
EXPECT_EQ(kEcoclimTempMin, ac.getSensorTemp());
ac.setSensorTemp(kEcoclimTempMin - 1);
EXPECT_EQ(kEcoclimTempMin, ac.getSensorTemp());
ac.setSensorTemp(kEcoclimTempMax);
EXPECT_EQ(kEcoclimTempMax, ac.getSensorTemp());
ac.setSensorTemp(kEcoclimTempMax + 1);
EXPECT_EQ(kEcoclimTempMax, ac.getSensorTemp());
}
TEST(TestIREcoclimAcClass, FanSpeed) {
IREcoclimAc ac(kGpioUnused);
ac.begin();
ac.setFan(0);
EXPECT_EQ(kEcoclimFanMin, ac.getFan());
ac.setFan(255);
EXPECT_EQ(kEcoclimFanAuto, ac.getFan());
ac.setFan(kEcoclimFanMax);
EXPECT_EQ(kEcoclimFanMax, ac.getFan());
ac.setFan(std::max(kEcoclimFanAuto, kEcoclimFanMax) + 1);
EXPECT_EQ(kEcoclimFanAuto, ac.getFan());
ac.setFan(kEcoclimFanMed);
EXPECT_EQ(kEcoclimFanMed, ac.getFan());
ac.setFan(kEcoclimFanMin);
EXPECT_EQ(kEcoclimFanMin, ac.getFan());
}
TEST(TestIREcoclimAcClass, OperatingMode) {
IREcoclimAc ac(kGpioUnused);
ac.begin();
ac.setMode(kEcoclimCool);
EXPECT_EQ(kEcoclimCool, ac.getMode());
ac.setMode(kEcoclimFan);
EXPECT_EQ(kEcoclimFan, ac.getMode());
ac.setMode(kEcoclimDry);
EXPECT_EQ(kEcoclimDry, ac.getMode());
ac.setMode(kEcoclimHeat);
EXPECT_EQ(kEcoclimHeat, ac.getMode());
ac.setMode(255);
EXPECT_EQ(kEcoclimAuto, ac.getMode());
}
TEST(TestIREcoclimAcClass, Timers) {
IREcoclimAc ac(kGpioUnused);
ac.begin();
ASSERT_FALSE(ac.isOnTimerEnabled());
EXPECT_EQ(kEcoclimTimerDisable, ac.getOnTimer());
ASSERT_FALSE(ac.isOffTimerEnabled());
EXPECT_EQ(kEcoclimTimerDisable, ac.getOffTimer());
ac.setOnTimer(0);
EXPECT_TRUE(ac.isOnTimerEnabled());
EXPECT_EQ(0, ac.getOnTimer());
EXPECT_FALSE(ac.isOffTimerEnabled());
EXPECT_EQ(kEcoclimTimerDisable, ac.getOffTimer());
ac.disableOnTimer();
EXPECT_FALSE(ac.isOnTimerEnabled());
EXPECT_EQ(kEcoclimTimerDisable, ac.getOnTimer());
ac.setOnTimer(23 * 60 + 59); // Max (23:59)
EXPECT_TRUE(ac.isOnTimerEnabled());
EXPECT_EQ(23 * 60 + 50, ac.getOnTimer()); // Rounded down to 10 min boundary.
EXPECT_FALSE(ac.isOffTimerEnabled());
EXPECT_EQ(kEcoclimTimerDisable, ac.getOffTimer());
ac.setOffTimer(3 * 60); // 3am
EXPECT_TRUE(ac.isOnTimerEnabled());
EXPECT_TRUE(ac.isOffTimerEnabled());
EXPECT_EQ(23 * 60 + 50, ac.getOnTimer()); // Rounded down to 10 min boundary.
EXPECT_EQ(3 * 60, ac.getOffTimer());
ac.disableOnTimer();
EXPECT_FALSE(ac.isOnTimerEnabled());
EXPECT_TRUE(ac.isOffTimerEnabled());
EXPECT_EQ(kEcoclimTimerDisable, ac.getOnTimer());
EXPECT_EQ(3 * 60, ac.getOffTimer());
ac.disableOffTimer();
EXPECT_FALSE(ac.isOnTimerEnabled());
EXPECT_EQ(kEcoclimTimerDisable, ac.getOnTimer());
EXPECT_FALSE(ac.isOffTimerEnabled());
EXPECT_EQ(kEcoclimTimerDisable, ac.getOffTimer());
}
TEST(TestIREcoclimAcClass, HumanReadable) {
IREcoclimAc ac(kGpioUnused);
ac.begin();
EXPECT_EQ(kEcoclimDefaultState, ac.getRaw());
EXPECT_EQ(
"Power: Off, Mode: 0 (Auto), Temp: 11C, SensorTemp: 22C, Fan: 3 (Auto), "
"Clock: 00:00, On Timer: Off, Off Timer: Off, Type: 0",
ac.toString());
ac.setPower(true);
ac.setMode(kEcoclimHeat);
ac.setTemp(25);
ac.setSensorTemp(19);
ac.setFan(kEcoclimFanMin);
ac.setClock(7 * 60 + 59);
ac.setOnTimer(8 * 60 + 0);
ac.setOffTimer(20 * 60 + 40);
ac.setType(kEcoclimDipSlave);
EXPECT_EQ(
"Power: On, Mode: 5 (Heat), Temp: 25C, SensorTemp: 19C, Fan: 0 (Low), "
"Clock: 07:59, On Timer: 08:00, Off Timer: 20:40, Type: 7",
ac.toString());
}