ESPHome 2026.5.1
Loading...
Searching...
No Matches
noblex.cpp
Go to the documentation of this file.
1#include "noblex.h"
3#include "esphome/core/log.h"
4
5namespace esphome::noblex {
6
7static const char *const TAG = "noblex.climate";
8
9const uint16_t NOBLEX_HEADER_MARK = 9000;
10const uint16_t NOBLEX_HEADER_SPACE = 4500;
11const uint16_t NOBLEX_BIT_MARK = 660;
12const uint16_t NOBLEX_ONE_SPACE = 1640;
13const uint16_t NOBLEX_ZERO_SPACE = 520;
14const uint32_t NOBLEX_GAP = 20000;
15const uint8_t NOBLEX_POWER = 0x10;
16
18 IR_NOBLEX_MODE_AUTO = 0b000,
19 IR_NOBLEX_MODE_COOL = 0b100,
20 IR_NOBLEX_MODE_DRY = 0b010,
21 IR_NOBLEX_MODE_FAN = 0b110,
22 IR_NOBLEX_MODE_HEAT = 0b001,
23};
24
26 IR_NOBLEX_FAN_AUTO = 0b00,
27 IR_NOBLEX_FAN_LOW = 0b10,
28 IR_NOBLEX_FAN_MEDIUM = 0b01,
29 IR_NOBLEX_FAN_HIGH = 0b11,
30};
31
32// Transmit via IR the state of this climate controller.
34 uint8_t remote_state[8] = {0x80, 0x10, 0x00, 0x0A, 0x50, 0x00, 0x20, 0x00}; // OFF, COOL, 24C, FAN_AUTO
35
36 auto powered_on = this->mode != climate::CLIMATE_MODE_OFF;
37 if (powered_on) {
38 remote_state[0] |= 0x10; // power bit
39 remote_state[2] = 0x02;
40 }
41 if (powered_on != this->powered_on_assumed)
42 this->powered_on_assumed = powered_on;
43
44 auto temp = (uint8_t) roundf(clamp<float>(this->target_temperature, NOBLEX_TEMP_MIN, NOBLEX_TEMP_MAX));
45 remote_state[1] = reverse_bits(uint8_t((temp - NOBLEX_TEMP_MIN) & 0x0F));
46
47 switch (this->mode) {
49 remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_AUTO << 5);
50 remote_state[1] = 0x90; // NOBLEX_TEMP_MAP 25C
51 break;
53 remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_COOL << 5);
54 break;
56 remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_DRY << 5);
57 break;
59 remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_FAN << 5);
60 break;
62 remote_state[0] |= (IRNoblexMode::IR_NOBLEX_MODE_HEAT << 5);
63 break;
65 default:
66 powered_on = false;
67 this->powered_on_assumed = powered_on;
68 remote_state[0] &= 0xEF;
69 remote_state[2] = 0x00;
70 break;
71 }
72
73 switch (this->fan_mode.value_or(climate::CLIMATE_FAN_ON)) {
75 remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_LOW << 2);
76 break;
78 remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_MEDIUM << 2);
79 break;
81 remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_HIGH << 2);
82 break;
84 default:
85 remote_state[0] |= (IRNoblexFan::IR_NOBLEX_FAN_AUTO << 2);
86 break;
87 }
88
89 switch (this->swing_mode) {
91 remote_state[0] |= 0x02;
92 remote_state[4] = 0x58;
93 break;
95 default:
96 remote_state[0] &= 0xFD;
97 remote_state[4] = 0x50;
98 break;
99 }
100
101 uint8_t crc = 0;
102 for (uint8_t i : remote_state) {
103 crc += reverse_bits(i);
104 }
105 crc = reverse_bits(uint8_t(crc & 0x0F)) >> 4;
106
107 ESP_LOGD(TAG, "Sending noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", remote_state[0], remote_state[1],
108 remote_state[2], remote_state[3], remote_state[4], remote_state[5], remote_state[6], remote_state[7]);
109
110 ESP_LOGV(TAG, "CRC: %01X", crc);
111
112 auto transmit = this->transmitter_->transmit();
113 auto *data = transmit.get_data();
114 data->set_carrier_frequency(38000);
115
116 // Header
117 data->mark(NOBLEX_HEADER_MARK);
118 data->space(NOBLEX_HEADER_SPACE);
119 // Data (sent remote_state from the MSB to the LSB)
120 for (int byte_idx = 0; byte_idx < 8; byte_idx++) {
121 for (int8_t bit_idx = 7; bit_idx >= 0; bit_idx--) {
122 if ((byte_idx == 4) && (bit_idx == 4)) {
123 // Header intermediate
124 data->mark(NOBLEX_BIT_MARK);
125 data->space(NOBLEX_GAP); // gap en bit 36
126 } else {
127 data->mark(NOBLEX_BIT_MARK);
128 bool bit = remote_state[byte_idx] & (1 << bit_idx);
129 data->space(bit ? NOBLEX_ONE_SPACE : NOBLEX_ZERO_SPACE);
130 }
131 }
132 }
133 // send crc
134 for (int8_t i = 3; i >= 0; i--) {
135 data->mark(NOBLEX_BIT_MARK);
136 bool bit = crc & (1 << i);
137 data->space(bit ? NOBLEX_ONE_SPACE : NOBLEX_ZERO_SPACE);
138 }
139 // Footer
140 data->mark(NOBLEX_BIT_MARK);
141
142 transmit.perform();
143} // end transmit_state()
144
145// Handle received IR Buffer
147 if (data.peek_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE)) {
148 // First part: header + first 36 bits, followed by 20ms gap
149 data.expect_item(NOBLEX_HEADER_MARK, NOBLEX_HEADER_SPACE);
150 ESP_LOGV(TAG, "Header");
151 this->receiving_ = false;
152 memset(this->remote_state_, 0, sizeof(this->remote_state_));
153 for (int i = 0; i < 5; i++) {
154 for (int j = 7; j >= 0; j--) {
155 if ((i == 4) && (j == 4)) {
156 this->remote_state_[i] |= 1 << j;
157 ESP_LOGVV(TAG, "GAP");
158 this->receiving_ = true;
159 return false;
160 } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) {
161 this->remote_state_[i] |= 1 << j;
162 } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) {
163 ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j);
164 return false;
165 }
166 }
167 ESP_LOGV(TAG, "Byte %d %02X", i, this->remote_state_[i]);
168 }
169 return false;
170 }
171
172 // Second part: remaining 28 bits + 4-bit CRC + footer
173 if (!this->receiving_) {
174 return false;
175 }
176 this->receiving_ = false;
177 for (int i = 4; i < 8; i++) {
178 for (int j = 7; j >= 0; j--) {
179 if ((i == 4) && (j >= 4)) {
180 // already decoded in first part
181 } else if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) {
182 this->remote_state_[i] |= 1 << j;
183 } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) {
184 ESP_LOGVV(TAG, "Byte %d bit %d fail", i, j);
185 return false;
186 }
187 }
188 ESP_LOGV(TAG, "Byte %d %02X", i, this->remote_state_[i]);
189 }
190
191 // Read CRC
192 uint8_t crc = 0;
193 for (int i = 3; i >= 0; i--) {
194 if (data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ONE_SPACE)) {
195 crc |= 1 << i;
196 } else if (!data.expect_item(NOBLEX_BIT_MARK, NOBLEX_ZERO_SPACE)) {
197 ESP_LOGVV(TAG, "Bit %d CRC fail", i);
198 return false;
199 }
200 }
201 ESP_LOGV(TAG, "CRC %02X", crc);
202
203 // Validate footer
204 if (!data.expect_mark(NOBLEX_BIT_MARK)) {
205 ESP_LOGV(TAG, "Footer fail");
206 return false;
207 }
208
209 // Validate CRC
210 uint8_t crc_calculated = 0;
211 for (uint8_t i : this->remote_state_)
212 crc_calculated += reverse_bits(i);
213 crc_calculated = reverse_bits(uint8_t(crc_calculated & 0x0F)) >> 4;
214 ESP_LOGVV(TAG, "CRC calc %02X", crc_calculated);
215
216 if (crc != crc_calculated) {
217 ESP_LOGV(TAG, "CRC fail");
218 return false;
219 }
220
221 ESP_LOGD(TAG, "Received noblex code: %02X%02X %02X%02X %02X%02X %02X%02X", this->remote_state_[0],
222 this->remote_state_[1], this->remote_state_[2], this->remote_state_[3], this->remote_state_[4],
223 this->remote_state_[5], this->remote_state_[6], this->remote_state_[7]);
224
225 auto powered_on = false;
226 if ((this->remote_state_[0] & NOBLEX_POWER) == NOBLEX_POWER) {
227 powered_on = true;
228 this->powered_on_assumed = powered_on;
229 } else {
230 powered_on = false;
231 this->powered_on_assumed = powered_on;
233 }
234 // powr on/off button
235 ESP_LOGV(TAG, "Power: %01X", powered_on);
236
237 // Set received mode
238 if (powered_on_assumed) {
239 auto mode = (this->remote_state_[0] & 0xE0) >> 5;
240 ESP_LOGV(TAG, "Mode: %02X", mode);
241 switch (mode) {
242 case IRNoblexMode::IR_NOBLEX_MODE_AUTO:
244 break;
245 case IRNoblexMode::IR_NOBLEX_MODE_COOL:
247 break;
248 case IRNoblexMode::IR_NOBLEX_MODE_DRY:
250 break;
251 case IRNoblexMode::IR_NOBLEX_MODE_FAN:
253 break;
254 case IRNoblexMode::IR_NOBLEX_MODE_HEAT:
256 break;
257 }
258 }
259
260 // Set received temp
261 uint8_t temp = this->remote_state_[1];
262 ESP_LOGVV(TAG, "Temperature Raw: %02X", temp);
263
264 temp = 0x0F & reverse_bits(temp);
265 temp += NOBLEX_TEMP_MIN;
266 ESP_LOGV(TAG, "Temperature Climate: %u", temp);
267 this->target_temperature = temp;
268
269 // Set received fan speed
270 auto fan = (this->remote_state_[0] & 0x0C) >> 2;
271 ESP_LOGV(TAG, "Fan: %02X", fan);
272 switch (fan) {
273 case IRNoblexFan::IR_NOBLEX_FAN_HIGH:
275 break;
276 case IRNoblexFan::IR_NOBLEX_FAN_MEDIUM:
278 break;
279 case IRNoblexFan::IR_NOBLEX_FAN_LOW:
281 break;
282 case IRNoblexFan::IR_NOBLEX_FAN_AUTO:
283 default:
285 break;
286 }
287
288 // Set received swing status
289 if (this->remote_state_[0] & 0x02) {
290 ESP_LOGV(TAG, "Swing vertical");
292 } else {
293 ESP_LOGV(TAG, "Swing OFF");
295 }
296
297 this->publish_state();
298 return true;
299} // end on_receive()
300
301} // namespace esphome::noblex
ClimateMode mode
The active mode of the climate device.
Definition climate.h:293
optional< ClimateFanMode > fan_mode
The active fan mode of the climate device.
Definition climate.h:287
float target_temperature
The target temperature of the climate device.
Definition climate.h:274
ClimateSwingMode swing_mode
The active swing mode of the climate device.
Definition climate.h:299
void publish_state()
Publish the state of the climate device, to be called from integrations.
Definition climate.cpp:437
void transmit_state() override
Transmit via IR the state of this climate controller.
Definition noblex.cpp:33
bool on_receive(remote_base::RemoteReceiveData data) override
Handle received IR Buffer.
Definition noblex.cpp:146
bool expect_item(uint32_t mark, uint32_t space)
@ CLIMATE_SWING_OFF
The swing mode is set to Off.
@ CLIMATE_SWING_VERTICAL
The fan mode is set to Vertical.
@ CLIMATE_MODE_DRY
The climate device is set to dry/humidity mode.
@ CLIMATE_MODE_FAN_ONLY
The climate device only has the fan enabled, no heating or cooling is taking place.
@ CLIMATE_MODE_HEAT
The climate device is set to heat to reach the target temperature.
@ CLIMATE_MODE_COOL
The climate device is set to cool to reach the target temperature.
@ CLIMATE_MODE_HEAT_COOL
The climate device is set to heat/cool to reach the target temperature.
@ CLIMATE_MODE_OFF
The climate device is off.
@ CLIMATE_FAN_MEDIUM
The fan mode is set to Medium.
@ CLIMATE_FAN_ON
The fan mode is set to On.
@ CLIMATE_FAN_AUTO
The fan mode is set to Auto.
@ CLIMATE_FAN_LOW
The fan mode is set to Low.
@ CLIMATE_FAN_HIGH
The fan mode is set to High.
const uint16_t NOBLEX_ONE_SPACE
Definition noblex.cpp:12
const uint16_t NOBLEX_HEADER_SPACE
Definition noblex.cpp:10
const uint8_t NOBLEX_POWER
Definition noblex.cpp:15
const uint16_t NOBLEX_ZERO_SPACE
Definition noblex.cpp:13
const uint32_t NOBLEX_GAP
Definition noblex.cpp:14
const uint16_t NOBLEX_HEADER_MARK
Definition noblex.cpp:9
const uint16_t NOBLEX_BIT_MARK
Definition noblex.cpp:11
enum IRNoblexMode { IR_NOBLEX_MODE_AUTO=0b000, IR_NOBLEX_MODE_COOL=0b100, IR_NOBLEX_MODE_DRY=0b010, IR_NOBLEX_MODE_FAN=0b110, IR_NOBLEX_MODE_HEAT=0b001, } IRNoblexMode
Definition noblex.cpp:17
const uint8_t NOBLEX_TEMP_MIN
Definition noblex.h:8
const uint8_t NOBLEX_TEMP_MAX
Definition noblex.h:9
enum IRNoblexFan { IR_NOBLEX_FAN_AUTO=0b00, IR_NOBLEX_FAN_LOW=0b10, IR_NOBLEX_FAN_MEDIUM=0b01, IR_NOBLEX_FAN_HIGH=0b11, } IRNoblexFan
Definition noblex.cpp:25
uint8_t reverse_bits(uint8_t x)
Reverse the order of 8 bits.
Definition helpers.h:898
static void uint32_t