Skip to content

Commit d9bc0ff

Browse files
authored
Add Fahrenheit support for Coolix (#2214)
* Handle fahrenheit bit sneakily packed in parity. Discovered with OutEquip Summit2 A/C. * Handle Fahrenheit in IRCoolixAC::toString() * Add Coolix encoding logic for Fahrenheit values. * Formatting: Fix linter errors.
1 parent 4245de9 commit d9bc0ff

4 files changed

Lines changed: 212 additions & 3 deletions

File tree

src/IRac.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4128,6 +4128,15 @@ String resultAcToString(const decode_results * const result) {
41284128
return ac.toString();
41294129
}
41304130
#endif // DECODE_COOLIX
4131+
#if DECODE_COOLIX48
4132+
case decode_type_t::COOLIX48: {
4133+
IRCoolixAC ac(kGpioUnused);
4134+
ac.on();
4135+
// Coolix uses value instead of state.
4136+
ac.setRawFromCoolix48(result->value);
4137+
return ac.toString();
4138+
}
4139+
#endif // DECODE_COOLIX
41314140
#if DECODE_CORONA_AC
41324141
case decode_type_t::CORONA_AC: {
41334142
IRCoronaAc ac(kGpioUnused);

src/ir_Coolix.cpp

Lines changed: 88 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ void IRCoolixAC::stateReset(void) {
9999
cleanFlag = false;
100100
sleepFlag = false;
101101
swingFlag = false;
102+
tempLowF = false;
103+
tempHighF = false;
102104
}
103105

104106
/// Set up hardware to be able to send a message.
@@ -112,8 +114,34 @@ void IRCoolixAC::send(const uint16_t repeat) {
112114
// Typically repeat is `kCoolixDefaultRepeat` which is `1`, so this allows
113115
// it to be 0 normally for this command, and allows additional repeats if
114116
// requested rather always 0 for that command.
115-
_irsend.sendCOOLIX(getRaw(), kCoolixBits, repeat - (getSwingVStep() &&
116-
repeat > 0) ? 1 : 0);
117+
const uint16_t repeat_override =
118+
repeat - (getSwingVStep() && repeat > 0) ? 1 : 0;
119+
120+
if (tempHighF || tempLowF) {
121+
uint64_t coolix24_raw = getRaw();
122+
// We need to sneak our temperature range into a parity bit.
123+
uint64_t coolix48_raw = 0;
124+
125+
// First create our parity.
126+
uint64_t parity = getRaw() ^ UINT32_MAX;
127+
128+
// Interleave raw data and parity into output, in order.
129+
coolix48_raw |= ((coolix24_raw >> 16) & 0xFF) << 40;
130+
coolix48_raw |= ((parity >> 16) & 0xFF) << 32;
131+
coolix48_raw |= ((coolix24_raw >> 8) & 0xFF) << 24;
132+
coolix48_raw |= ((parity >> 8) & 0xFF) << 16;
133+
coolix48_raw |= (coolix24_raw & 0xFF) << 8;
134+
coolix48_raw |= parity & 0xFF;
135+
136+
// Mangle parity as necessary to represent Fahrenheit range.
137+
coolix48_raw |= static_cast<uint64_t>(tempLowF ? 1 : 3) << (36);
138+
139+
// Send as Coolix48.
140+
_irsend.sendCoolix48(coolix48_raw, kCoolix48Bits, repeat_override);
141+
} else {
142+
_irsend.sendCOOLIX(getRaw(), kCoolixBits, repeat_override);
143+
}
144+
117145
// make sure to remove special state from the internal state
118146
// after command has being transmitted.
119147
recoverSavedState();
@@ -140,6 +168,27 @@ void IRCoolixAC::setRaw(const uint32_t new_code) {
140168
_.raw = new_code;
141169
}
142170

171+
// Set internal state from a COOLIX48 read, handling F data hidden in parity.
172+
void IRCoolixAC::setRawFromCoolix48(const uint64_t data) {
173+
// Assume parity bits as in coolix24, let's strip them.
174+
CoolixProtocol coolix24;
175+
coolix24.raw = 0;
176+
coolix24.raw |= ((data >> 8) & 0xff);
177+
coolix24.raw |= ((data >> 24) & 0xff) << 8;
178+
coolix24.raw |= ((data >> 40) & 0xff) << 16;
179+
180+
// Delegate to coolix24 code.
181+
setRaw(coolix24.raw);
182+
183+
// Handle Fahrenheit range.
184+
uint8_t fRange = (data >> 36) & 3;
185+
if (fRange == 1) {
186+
setTempFRange(false);
187+
} else if (fRange == 3) {
188+
setTempFRange(true);
189+
}
190+
}
191+
143192
/// Is the current state is a special state?
144193
/// @return true, if it is. false if it isn't.
145194
bool IRCoolixAC::isSpecialState(void) const {
@@ -217,19 +266,53 @@ void IRCoolixAC::setTempRaw(const uint8_t code) { _.Temp = code; }
217266
/// @return The native temperature value.
218267
uint8_t IRCoolixAC::getTempRaw(void) const { return _.Temp; }
219268

269+
/// Set the fahrenheit temperature range.
270+
/// @param[in] high True if setting high F range, false if low F range.
271+
void IRCoolixAC::setTempFRange(const bool high) {
272+
tempLowF = !high;
273+
tempHighF = high;
274+
}
275+
276+
// Clear the fahrenheit temperature range bits.
277+
void IRCoolixAC::clearTempFRange() {
278+
tempLowF = false;
279+
tempHighF = false;
280+
}
281+
220282
/// Set the temperature.
221283
/// @param[in] desired The temperature in degrees celsius.
222284
void IRCoolixAC::setTemp(const uint8_t desired) {
223285
// Range check.
286+
if (desired >= kCoolixTempLowFMin &&
287+
desired <= kCoolixTempLowFMax) {
288+
setTempRaw(kCoolixTempMapLowF[desired - kCoolixTempLowFMin]);
289+
setTempFRange(false);
290+
return;
291+
}
292+
if (desired >= kCoolixTempHighFMin &&
293+
desired <= kCoolixTempHighFMax) {
294+
setTempRaw(kCoolixTempMapHighF[desired - kCoolixTempHighFMin]);
295+
setTempFRange(true);
296+
return;
297+
}
224298
uint8_t temp = std::min(desired, kCoolixTempMax);
225299
temp = std::max(temp, kCoolixTempMin);
226300
setTempRaw(kCoolixTempMap[temp - kCoolixTempMin]);
301+
clearTempFRange();
227302
}
228303

229304
/// Get the current temperature setting.
230305
/// @return The current setting for temp. in degrees celsius.
231306
uint8_t IRCoolixAC::getTemp(void) const {
232307
const uint8_t code = getTempRaw();
308+
if (tempLowF) {
309+
for (uint8_t i = 0; i < kCoolixTempLowFRange; i++)
310+
if (kCoolixTempMapLowF[i] == code) return kCoolixTempLowFMin + i;
311+
}
312+
if (tempHighF) {
313+
for (uint8_t i = 0; i < kCoolixTempHighFRange; i++)
314+
if (kCoolixTempMapHighF[i] == code) return kCoolixTempHighFMin + i;
315+
}
233316
for (uint8_t i = 0; i < kCoolixTempRange; i++)
234317
if (kCoolixTempMap[i] == code) return kCoolixTempMin + i;
235318
return kCoolixTempMax; // Not a temp we expected.
@@ -613,7 +696,9 @@ String IRCoolixAC::toString(void) const {
613696
}
614697
result += ')';
615698
// Fan mode doesn't have a temperature.
616-
if (getMode() != kCoolixFan) result += addTempToString(getTemp());
699+
bool celcius = true;
700+
if (tempHighF || tempLowF) celcius = false;
701+
if (getMode() != kCoolixFan) result += addTempToString(getTemp(), celcius);
617702
result += addBoolToString(getZoneFollow(), kZoneFollowStr);
618703
result += addLabeledString(
619704
(getSensorTemp() == kCoolixSensorTempIgnoreCode)

src/ir_Coolix.h

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,42 @@ const uint8_t kCoolixTempMap[kCoolixTempRange] = {
7979
0b1010, // 29C
8080
0b1011 // 30C
8181
};
82+
const uint8_t kCoolixTempLowFMin = 63;
83+
const uint8_t kCoolixTempLowFMax = 75;
84+
const uint8_t kCoolixTempLowFRange =
85+
kCoolixTempLowFMax - kCoolixTempLowFMin + 1;
86+
const uint8_t kCoolixTempHighFMin = kCoolixTempLowFMax + 1;
87+
const uint8_t kCoolixTempHighFMax = 86;
88+
const uint8_t kCoolixTempHighFRange =
89+
kCoolixTempHighFMax - kCoolixTempHighFMin + 1;
90+
const uint8_t kCoolixTempMapLowF[kCoolixTempLowFRange] = {
91+
0b1100, // 63F
92+
0b0010, // 64F
93+
0b1010, // 65F
94+
0b0110, // 66F
95+
0b1110, // 67F
96+
0b0001, // 68F
97+
0b1001, // 69F
98+
0b0101, // 70F
99+
0b1101, // 71F
100+
0b0011, // 72F
101+
0b1011, // 73F
102+
0b0111, // 74F
103+
0b1111 // 75F
104+
};
105+
const uint8_t kCoolixTempMapHighF[kCoolixTempHighFRange] = {
106+
0b0000, // 76F
107+
0b1000, // 77F
108+
0b0100, // 78F
109+
0b1100, // 79F
110+
0b0010, // 80F
111+
0b1010, // 81F
112+
0b0110, // 82F
113+
0b1110, // 83F
114+
0b0001, // 84F
115+
0b1001, // 85F
116+
0b0101 // 86F
117+
};
82118
const uint8_t kCoolixSensorTempMax = 30; // Celsius
83119
const uint8_t kCoolixSensorTempIgnoreCode = 0b11111; // 0x1F / 31 (DEC)
84120
// kCoolixSensorTempMask = 0b000000000000111100000000; // 0xF00
@@ -136,6 +172,8 @@ class IRCoolixAC {
136172
void off(void);
137173
void setPower(const bool on);
138174
bool getPower(void) const;
175+
void setTempFRange(const bool high = false);
176+
void clearTempFRange();
139177
void setTemp(const uint8_t temp);
140178
uint8_t getTemp(void) const;
141179
void setSensorTemp(const uint8_t temp);
@@ -160,6 +198,8 @@ class IRCoolixAC {
160198
bool getZoneFollow(void) const;
161199
uint32_t getRaw(void) const;
162200
void setRaw(const uint32_t new_code);
201+
// Convert from Coolix48 with Fahrenheit handling.
202+
void setRawFromCoolix48(const uint64_t new_code);
163203
static uint8_t convertMode(const stdAc::opmode_t mode);
164204
static uint8_t convertFan(const stdAc::fanspeed_t speed);
165205
static stdAc::opmode_t toCommonMode(const uint8_t mode);
@@ -186,6 +226,8 @@ class IRCoolixAC {
186226
bool cleanFlag;
187227
bool sleepFlag;
188228
bool swingFlag;
229+
bool tempLowF; // Indicates low-range F temperatures.
230+
bool tempHighF; // Indicates high-range F temperatures.
189231
uint8_t savedFan;
190232

191233
void setTempRaw(const uint8_t code);

test/ir_Coolix_test.cpp

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -393,6 +393,7 @@ TEST(TestCoolixACClass, SetAndGetRaw) {
393393
TEST(TestCoolixACClass, SetAndGetTemp) {
394394
IRCoolixAC ac(kGpioUnused);
395395

396+
// Celcius
396397
ac.setTemp(25);
397398
EXPECT_EQ(25, ac.getTemp());
398399
ac.setTemp(kCoolixTempMin);
@@ -403,6 +404,26 @@ TEST(TestCoolixACClass, SetAndGetTemp) {
403404
EXPECT_EQ(kCoolixTempMin, ac.getTemp());
404405
ac.setTemp(kCoolixTempMax + 1);
405406
EXPECT_EQ(kCoolixTempMax, ac.getTemp());
407+
408+
// Fahrenheit low
409+
ac.setTemp(70);
410+
EXPECT_EQ(70, ac.getTemp());
411+
ac.setTemp(kCoolixTempLowFMin);
412+
EXPECT_EQ(kCoolixTempLowFMin, ac.getTemp());
413+
ac.setTemp(kCoolixTempLowFMax);
414+
EXPECT_EQ(kCoolixTempLowFMax, ac.getTemp());
415+
ac.setTemp(kCoolixTempLowFMin - 1);
416+
EXPECT_EQ(kCoolixTempMax, ac.getTemp());
417+
418+
// Fahrenheit high
419+
ac.setTemp(80);
420+
EXPECT_EQ(80, ac.getTemp());
421+
ac.setTemp(kCoolixTempHighFMin);
422+
EXPECT_EQ(kCoolixTempHighFMin, ac.getTemp());
423+
ac.setTemp(kCoolixTempHighFMax);
424+
EXPECT_EQ(kCoolixTempHighFMax, ac.getTemp());
425+
ac.setTemp(kCoolixTempHighFMax + 1);
426+
EXPECT_EQ(kCoolixTempMax, ac.getTemp());
406427
}
407428

408429
TEST(TestCoolixACClass, SetAndGetMode) {
@@ -1104,3 +1125,55 @@ TEST(TestCoolixACClass, Issue2012) {
11041125
ac.toString());
11051126
EXPECT_EQ(kNoTempValue, ac.toCommon().sensorTemperature);
11061127
}
1128+
1129+
// Test decoding Fahrenheit from Coolix48.
1130+
TEST(TestCoolixACClass, SetRawFahrenheit) {
1131+
IRsendTest irsend(kGpioUnused);
1132+
IRrecv irrecv(kGpioUnused);
1133+
IRCoolixAC ac(kGpioUnused);
1134+
1135+
ac.stateReset();
1136+
ac.setRawFromCoolix48(0xB25DFF00C03F);
1137+
EXPECT_EQ(
1138+
"Power: On, Mode: 0 (Cool), Fan: 7 (Fixed), Temp: 63F, "
1139+
"Zone Follow: Off, Sensor Temp: Off",
1140+
ac.toString());
1141+
}
1142+
1143+
// Test sending and decoding of Fahrenheit values.
1144+
TEST(TestCoolixACClass, SendFahrenheit) {
1145+
IRrecv irrecv(kGpioUnused);
1146+
IRCoolixAC ac(kGpioUnused);
1147+
1148+
const uint32_t on_cool_63f_fan_fixed_noparity = 0xB2FFC0;
1149+
ac.begin();
1150+
ac.on();
1151+
ac.setPower(true);
1152+
ac.setMode(kCoolixCool);
1153+
ac.setFan(kCoolixFanFixed);
1154+
ac.setTemp(63);
1155+
EXPECT_EQ(on_cool_63f_fan_fixed_noparity, ac.getRaw());
1156+
1157+
ac.send();
1158+
ac._irsend.makeDecodeResult();
1159+
ASSERT_TRUE(irrecv.decode(&ac._irsend.capture));
1160+
EXPECT_EQ(COOLIX48, ac._irsend.capture.decode_type);
1161+
EXPECT_EQ(kCoolix48Bits, ac._irsend.capture.bits);
1162+
EXPECT_EQ(0xB25DFF00C03F, ac._irsend.capture.value);
1163+
EXPECT_EQ(0x0, ac._irsend.capture.address);
1164+
EXPECT_EQ(0x0, ac._irsend.capture.command);
1165+
1166+
const uint32_t on_cool_80f_fan_fixed_noparity = 0xB2FF20;
1167+
ac.setTemp(80);
1168+
EXPECT_EQ(on_cool_80f_fan_fixed_noparity, ac.getRaw());
1169+
1170+
ac._irsend.reset();
1171+
ac.send();
1172+
ac._irsend.makeDecodeResult();
1173+
ASSERT_TRUE(irrecv.decode(&ac._irsend.capture));
1174+
EXPECT_EQ(COOLIX48, ac._irsend.capture.decode_type);
1175+
EXPECT_EQ(kCoolix48Bits, ac._irsend.capture.bits);
1176+
EXPECT_EQ(0xB27DFF0020DF, ac._irsend.capture.value);
1177+
EXPECT_EQ(0x0, ac._irsend.capture.address);
1178+
EXPECT_EQ(0x0, ac._irsend.capture.command);
1179+
}

0 commit comments

Comments
 (0)