diff --git a/src/ir_Hitachi.cpp b/src/ir_Hitachi.cpp index bbde07f37..6221d19df 100644 --- a/src/ir_Hitachi.cpp +++ b/src/ir_Hitachi.cpp @@ -44,6 +44,7 @@ using irutils::addModeToString; using irutils::addFanToString; using irutils::addTempToString; using irutils::setBit; +using irutils::setBits; #if (SEND_HITACHI_AC || SEND_HITACHI_AC2) // Send a Hitachi A/C message. @@ -520,3 +521,222 @@ bool IRrecv::decodeHitachiAc424(decode_results *results, const uint16_t nbits, return true; } #endif // DECODE_HITACHI_AC424 + +// Class for handling the remote control on a Hitachi_AC424 53 byte A/C message +IRHitachiAc424::IRHitachiAc424(const uint16_t pin, const bool inverted, + const bool use_modulation) + : _irsend(pin, inverted, use_modulation) { stateReset(); } + +// Reset to auto fan, cooling, 23° Celcius +void IRHitachiAc424::stateReset(void) { + for (uint8_t i = 0; i < kHitachiAc424StateLength; i++) + remote_state[i] = 0x00; + + remote_state[0] = 0x01; + remote_state[1] = 0x10; + remote_state[3] = 0x40; + remote_state[5] = 0xFF; + remote_state[7] = 0xCC; + remote_state[11] = 0x13; // Button Action + remote_state[33] = 0x80; + remote_state[35] = 0x03; + remote_state[37] = 0x01; + remote_state[39] = 0x88; + remote_state[45] = 0xFF; + remote_state[47] = 0xFF; + remote_state[49] = 0xFF; + remote_state[51] = 0xFF; + + setTemp(23); + setPower(true); + setMode(kHitachiAc424Cool); + setFan(kHitachiAc424FanAuto); + setInvertedStates(); +} + +void IRHitachiAc424::setInvertedStates(void) { + for (uint8_t i = 3; i < kHitachiAc424StateLength; i+=2) + remote_state[i+1] = ~remote_state[i]; +} + +void IRHitachiAc424::begin(void) { _irsend.begin(); } + +uint8_t *IRHitachiAc424::getRaw(void) { + setInvertedStates(); + return remote_state; +} + +void IRHitachiAc424::setRaw(const uint8_t new_code[], const uint16_t length) { + memcpy(remote_state, new_code, std::min(length, kHitachiAc424StateLength)); +} + +#if SEND_HITACHI_AC424 +void IRHitachiAc424::send(const uint16_t repeat) { + _irsend.sendHitachiAc424(getRaw(), kHitachiAc424StateLength, repeat); +} +#endif // SEND_HITACHI_AC424 + +bool IRHitachiAc424::getPower(void) { + return GETBIT8(remote_state[27], + kHitachiAc424PowerOffset); +} + +void IRHitachiAc424::setPower(const bool on) { + setBit(&remote_state[27], kHitachiAc424PowerOffset, on); +} + +void IRHitachiAc424::on(void) { setPower(true); } + +void IRHitachiAc424::off(void) { setPower(false); } + +uint8_t IRHitachiAc424::getMode(void) { + return GETBITS8(remote_state[25], kLowNibble, kNibbleSize); +} + +void IRHitachiAc424::setMode(const uint8_t mode) { + uint8_t newMode = mode; + switch (mode) { + // Fan mode sets a special temp. + case kHitachiAc424Fan: setTemp(27, false); break; + case kHitachiAc424Heat: + case kHitachiAc424Cool: + case kHitachiAc424Dry: break; + default: newMode = kHitachiAc424Cool; + } + setBits(&remote_state[25], kLowNibble, kNibbleSize, newMode); + if (newMode != kHitachiAc424Fan) setTemp(_previoustemp); + setFan(getFan()); // Reset the fan speed after the mode change. +} + +uint8_t IRHitachiAc424::getTemp(void) { + return remote_state[13] >> 2; +} + +void IRHitachiAc424::setTemp(const uint8_t celsius, bool setPrevious) { + uint8_t temp; + if (setPrevious) _previoustemp = celsius; + temp = std::min(celsius, kHitachiAc424MaxTemp); + temp = std::max(temp, kHitachiAc424MinTemp); + remote_state[13] = temp << 2; +} + +uint8_t IRHitachiAc424::getFan(void) { + return GETBITS8(remote_state[25], kHighNibble, kNibbleSize); +} + +void IRHitachiAc424::setFan(const uint8_t speed) { + uint8_t newSpeed = std::max(speed, kHitachiAc424FanMin); + uint8_t fanMax = kHitachiAc424FanMax; + + // Only 2 x low speeds in Dry mode or Auto + if (getMode() == kHitachiAc424Dry && speed == kHitachiAc424FanAuto) { + fanMax = kHitachiAc424FanAuto; + } else if (getMode() == kHitachiAc424Dry) { + fanMax = kHitachiAc424FanMaxDry; + } else if (getMode() == kHitachiAc424Fan && speed == kHitachiAc424FanAuto) { + // Fan Mode does not have auto. Set to safe low + newSpeed = kHitachiAc424FanMin; + } + + newSpeed = std::min(newSpeed, fanMax); + setBits(&remote_state[25], kHighNibble, kNibbleSize, newSpeed); + remote_state[9] = 0x92; + remote_state[29] = 0x00; + + // When fan is at min/max, additional bytes seem to be set + if (newSpeed == kHitachiAc424FanMin) remote_state[9] = 0x98; + if (newSpeed == kHitachiAc424FanMax) { + remote_state[9] = 0xA9; + remote_state[29] = 0x30; + } +} + +// Convert a standard A/C mode into its native mode. +uint8_t IRHitachiAc424::convertMode(const stdAc::opmode_t mode) { + switch (mode) { + case stdAc::opmode_t::kCool: return kHitachiAc424Cool; + case stdAc::opmode_t::kHeat: return kHitachiAc424Heat; + case stdAc::opmode_t::kDry: return kHitachiAc424Dry; + case stdAc::opmode_t::kFan: return kHitachiAc424Fan; + default: return kHitachiAc424Cool; + } +} + +// Convert a standard A/C Fan speed into its native fan speed. +uint8_t IRHitachiAc424::convertFan(const stdAc::fanspeed_t speed) { + switch (speed) { + case stdAc::fanspeed_t::kMin: return kHitachiAc424FanMin; + case stdAc::fanspeed_t::kLow: return kHitachiAc424FanLow; + case stdAc::fanspeed_t::kMedium: return kHitachiAc424FanMedium; + case stdAc::fanspeed_t::kHigh: return kHitachiAc424FanHigh; + case stdAc::fanspeed_t::kMax: return kHitachiAc424FanMax; + default: return kHitachiAc424FanAuto; + } +} + +// Convert a native mode to it's common equivalent. +stdAc::opmode_t IRHitachiAc424::toCommonMode(const uint8_t mode) { + switch (mode) { + case kHitachiAc424Cool: return stdAc::opmode_t::kCool; + case kHitachiAc424Heat: return stdAc::opmode_t::kHeat; + case kHitachiAc424Dry: return stdAc::opmode_t::kDry; + case kHitachiAc424Fan: return stdAc::opmode_t::kFan; + default: return stdAc::opmode_t::kCool; + } +} + +// Convert a native fan speed to it's common equivalent. +stdAc::fanspeed_t IRHitachiAc424::toCommonFanSpeed(const uint8_t speed) { + switch (speed) { + case kHitachiAc424FanMax: return stdAc::fanspeed_t::kMax; + case kHitachiAc424FanHigh: return stdAc::fanspeed_t::kHigh; + case kHitachiAc424FanMedium: return stdAc::fanspeed_t::kMedium; + case kHitachiAc424FanLow: return stdAc::fanspeed_t::kLow; + case kHitachiAc424FanMin: return stdAc::fanspeed_t::kMin; + default: return stdAc::fanspeed_t::kAuto; + } +} + +// Convert the A/C state to it's common equivalent. +stdAc::state_t IRHitachiAc424::toCommon(void) { + stdAc::state_t result; + result.protocol = decode_type_t::HITACHI_AC424; + result.model = -1; // No models used. + result.power = this->getPower(); + result.mode = this->toCommonMode(this->getMode()); + result.celsius = true; + result.degrees = this->getTemp(); + result.fanspeed = this->toCommonFanSpeed(this->getFan()); + + // todo: + result.swingv = stdAc::swingv_t::kOff; + + // Not supported. + result.swingh = stdAc::swingh_t::kOff; + result.quiet = false; + result.turbo = false; + result.clean = false; + result.econo = false; + result.filter = false; + result.light = false; + result.beep = false; + result.sleep = -1; + result.clock = -1; + return result; +} + +// Convert the internal state into a human readable string. +String IRHitachiAc424::toString(void) { + String result = ""; + result.reserve(110); // Reserve some heap for the string to reduce fragging. + result += addBoolToString(getPower(), kPowerStr, false); + result += addModeToString(getMode(), 0, kHitachiAc424Cool, + kHitachiAc424Heat, kHitachiAc424Dry, + kHitachiAc424Fan); + result += addTempToString(getTemp()); + // Todo: Does not handle max fan, overload method? + result += addFanToString(getFan(), kHitachiAc424FanHigh, kHitachiAc424FanLow, + kHitachiAc424FanAuto, kHitachiAc424FanMin, + kHitachiAc424FanMedium); + return result; +} diff --git a/src/ir_Hitachi.h b/src/ir_Hitachi.h index 790070a3a..023957ce8 100644 --- a/src/ir_Hitachi.h +++ b/src/ir_Hitachi.h @@ -39,6 +39,21 @@ const uint8_t kHitachiAcMaxTemp = 32; // 32C const uint8_t kHitachiAcAutoTemp = 23; // 23C const uint8_t kHitachiAcPowerOffset = 0; const uint8_t kHitachiAcSwingOffset = 7; +// HitachiAc424 +const uint8_t kHitachiAc424Fan = 1; +const uint8_t kHitachiAc424Cool = 3; +const uint8_t kHitachiAc424Dry = 5; +const uint8_t kHitachiAc424Heat = 6; +const uint8_t kHitachiAc424MinTemp = 16; // 16C +const uint8_t kHitachiAc424MaxTemp = 32; // 32C +const uint8_t kHitachiAc424FanMin = 1; +const uint8_t kHitachiAc424FanLow = 2; +const uint8_t kHitachiAc424FanMedium = 3; +const uint8_t kHitachiAc424FanHigh = 4; +const uint8_t kHitachiAc424FanAuto = 5; +const uint8_t kHitachiAc424FanMax = 6; +const uint8_t kHitachiAc424FanMaxDry = 2; +const uint8_t kHitachiAc424PowerOffset = 4; // Classes class IRHitachiAc { @@ -92,4 +107,47 @@ class IRHitachiAc { uint8_t _previoustemp; }; +class IRHitachiAc424 { + public: + explicit IRHitachiAc424(const uint16_t pin, const bool inverted = false, + const bool use_modulation = true); + + void stateReset(void); +#if SEND_HITACHI_AC424 + void send(const uint16_t repeat = kHitachiAcDefaultRepeat); + uint8_t calibrate(void) { return _irsend.calibrate(); } +#endif // SEND_HITACHI_AC424 + void begin(void); + void on(void); + void off(void); + void setPower(const bool on); + bool getPower(void); + void setTemp(const uint8_t temp, bool setPrevious = true); + uint8_t getTemp(void); + void setFan(const uint8_t speed); + uint8_t getFan(void); + void setMode(const uint8_t mode); + uint8_t getMode(void); + uint8_t* getRaw(void); + void setRaw(const uint8_t new_code[], + const uint16_t length = kHitachiAc424StateLength); + uint8_t convertMode(const stdAc::opmode_t mode); + 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); + String toString(void); +#ifndef UNIT_TEST + + private: + IRsend _irsend; +#else + IRsendTest _irsend; +#endif + // The state of the IR remote in IR code form. + uint8_t remote_state[kHitachiAc424StateLength]; + void setInvertedStates(void); + uint8_t _previoustemp; +}; + #endif // IR_HITACHI_H_ diff --git a/test/ir_Hitachi_test.cpp b/test/ir_Hitachi_test.cpp index f1d6eec27..d7497160a 100644 --- a/test/ir_Hitachi_test.cpp +++ b/test/ir_Hitachi_test.cpp @@ -932,3 +932,162 @@ TEST(TestDecodeHitachiAc424, SyntheticExample) { ASSERT_EQ(kHitachiAc424Bits, irsend.capture.bits); EXPECT_STATE_EQ(expected, irsend.capture.state, irsend.capture.bits); } + +// Tests for IRHitachiAc424 class. +TEST(TestIRHitachiAc424Class, SetInvertedStates) { + IRHitachiAc424 ac(0); + + uint8_t raw[kHitachiAc424StateLength] = { + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00}; + uint8_t expected[kHitachiAc424StateLength] = { + 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, 0xFF, 0x00, + 0xFF, 0x00, 0xFF}; + + ac.setRaw(raw); + EXPECT_STATE_EQ(expected, ac.getRaw(), kHitachiAc424Bits); +} + +TEST(TestIRHitachiAc424Class, SetAndGetPower) { + IRHitachiAc424 ac(0); + 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(TestIRHitachiAc424Class, SetAndGetTemp) { + IRHitachiAc424 ac(0); + ac.setTemp(25); + EXPECT_EQ(25, ac.getTemp()); + ac.setTemp(kHitachiAc424MinTemp); + EXPECT_EQ(kHitachiAc424MinTemp, ac.getTemp()); + ac.setTemp(kHitachiAc424MinTemp - 1); + EXPECT_EQ(kHitachiAc424MinTemp, ac.getTemp()); + ac.setTemp(kHitachiAc424MaxTemp); + EXPECT_EQ(kHitachiAc424MaxTemp, ac.getTemp()); + ac.setTemp(kHitachiAc424MaxTemp + 1); + EXPECT_EQ(kHitachiAc424MaxTemp, ac.getTemp()); +} + +TEST(TestIRHitachiAc424Class, SetAndGetMode) { + IRHitachiAc424 ac(0); + ac.setMode(kHitachiAc424Cool); + ac.setFan(kHitachiAc424FanAuto); + ac.setTemp(25); + EXPECT_EQ(25, ac.getTemp()); + EXPECT_EQ(kHitachiAc424Cool, ac.getMode()); + EXPECT_EQ(kHitachiAc424FanAuto, ac.getFan()); + ac.setMode(kHitachiAc424Fan); + EXPECT_EQ(kHitachiAc424Fan, ac.getMode()); + EXPECT_EQ(27, ac.getTemp()); + EXPECT_NE(kHitachiAc424FanAuto, ac.getFan()); + ac.setMode(kHitachiAc424Heat); + EXPECT_EQ(25, ac.getTemp()); + EXPECT_EQ(kHitachiAc424Heat, ac.getMode()); + ac.setMode(kHitachiAc424Dry); + EXPECT_EQ(kHitachiAc424Dry, ac.getMode()); + EXPECT_NE(kHitachiAc424FanAuto, ac.getFan()); +} + +TEST(TestIRHitachiAc424Class, SetAndGetFan) { + IRHitachiAc424 ac(0); + ac.setMode(kHitachiAc424Cool); // All fan options are available in this mode. + ac.setFan(kHitachiAc424FanAuto); + EXPECT_EQ(kHitachiAc424FanAuto, ac.getFan()); + ac.setFan(kHitachiAc424FanLow); + EXPECT_EQ(kHitachiAc424FanLow, ac.getFan()); + ac.setFan(kHitachiAc424FanHigh); + EXPECT_EQ(kHitachiAc424FanHigh, ac.getFan()); + ac.setFan(kHitachiAc424FanMax + 1); + EXPECT_EQ(kHitachiAc424FanMax, ac.getFan()); + ac.setFan(kHitachiAc424FanMin - 1); + EXPECT_EQ(kHitachiAc424FanMin, ac.getFan()); + + ac.setFan(kHitachiAc424FanAuto); + ac.setMode(kHitachiAc424Fan); // No auto-fan in Fan mode. + EXPECT_EQ(kHitachiAc424FanMin, ac.getFan()); + ac.setFan(kHitachiAc424FanMax); + EXPECT_EQ(kHitachiAc424FanMax, ac.getFan()); + + // Only min, low and auto fan settings in Dry mode. + ac.setMode(kHitachiAc424Dry); + EXPECT_EQ(kHitachiAc424FanLow, ac.getFan()); + ac.setFan(kHitachiAc424FanHigh); + EXPECT_EQ(kHitachiAc424FanLow, ac.getFan()); + ac.setFan(kHitachiAc424FanMin); + EXPECT_EQ(kHitachiAc424FanMin, ac.getFan()); + ac.setFan(kHitachiAc424FanAuto); + EXPECT_EQ(kHitachiAc424FanAuto, ac.getFan()); + + // Check additional bytes set by min & max fan + ac.setMode(kHitachiAc424Cool); + ac.setFan(kHitachiAc424FanMax); + EXPECT_EQ(ac.getRaw()[9], 0xA9); + EXPECT_EQ(ac.getRaw()[29], 0x30); + ac.setFan(kHitachiAc424FanMin); + EXPECT_EQ(ac.getRaw()[9], 0x98); + EXPECT_EQ(ac.getRaw()[29], 0x00); + ac.setFan(kHitachiAc424FanLow); + EXPECT_EQ(ac.getRaw()[9], 0x92); + EXPECT_EQ(ac.getRaw()[29], 0x00); +} + +TEST(TestIRHitachiAc424Class, HumanReadable) { + IRHitachiAc424 ac(0); + + ac.setMode(kHitachiAc424Heat); + ac.setTemp(kHitachiAc424MaxTemp); + ac.on(); + ac.setFan(kHitachiAc424FanHigh); + EXPECT_EQ( + "Power: On, Mode: 6 (Heat), Temp: 32C, Fan: 4 (High)", + ac.toString()); + ac.setMode(kHitachiAc424Cool); + ac.setTemp(kHitachiAc424MinTemp); + ac.setFan(kHitachiAc424FanMin); + EXPECT_EQ( + "Power: On, Mode: 3 (Cool), Temp: 16C, Fan: 1 (Quiet)", + ac.toString()); +} + +TEST(TestIRHitachiAcClass424, toCommon) { + IRHitachiAc424 ac(0); + ac.setPower(true); + ac.setMode(kHitachiAc424Cool); + ac.setTemp(20); + ac.setFan(kHitachiAc424FanMax); + // Now test it. + ASSERT_EQ(decode_type_t::HITACHI_AC424, ac.toCommon().protocol); + ASSERT_EQ(-1, ac.toCommon().model); + ASSERT_TRUE(ac.toCommon().power); + ASSERT_TRUE(ac.toCommon().celsius); + ASSERT_EQ(20, ac.toCommon().degrees); + ASSERT_EQ(stdAc::opmode_t::kCool, ac.toCommon().mode); + ASSERT_EQ(stdAc::fanspeed_t::kMax, ac.toCommon().fanspeed); + // Todo: + ASSERT_EQ(stdAc::swingv_t::kOff, ac.toCommon().swingv); + // Unsupported. + ASSERT_EQ(stdAc::swingh_t::kOff, ac.toCommon().swingh); + ASSERT_FALSE(ac.toCommon().turbo); + ASSERT_FALSE(ac.toCommon().clean); + ASSERT_FALSE(ac.toCommon().light); + ASSERT_FALSE(ac.toCommon().quiet); + ASSERT_FALSE(ac.toCommon().econo); + ASSERT_FALSE(ac.toCommon().filter); + ASSERT_FALSE(ac.toCommon().beep); + ASSERT_EQ(-1, ac.toCommon().sleep); + ASSERT_EQ(-1, ac.toCommon().clock); +}