ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
samsung.cpp
Go to the documentation of this file.
1#include "samsung.h"
2
3#include "esphome/core/hal.h"
5#include "esphome/core/log.h"
6
7#include <cstring>
8
9namespace esphome::samsung {
10
11ESPHOME_LOG_TAG(TAG, "samsung.climate");
12
13static constexpr uint32_t CARRIER_FREQUENCY_HZ = 38000;
14static constexpr uint32_t HEADER_MARK_US = 3000;
15static constexpr uint32_t HEADER_SPACE_US = 9000;
16static constexpr uint32_t BIT_MARK_US = 500;
17static constexpr uint32_t ONE_SPACE_US = 1500;
18static constexpr uint32_t ZERO_SPACE_US = 500;
19static constexpr uint32_t SECTION_SPACE_US = 2000;
20
21static constexpr uint8_t EXTENDED_LENGTH = 3 * SAMSUNG_AC_SECTION_LENGTH;
22
23// A powered-on unit at 24 °C, cool mode, fan auto, swing off (from IRremoteESP8266); checksums are refilled on send
24static const uint8_t RESET_STATE[SAMSUNG_AC_STATE_LENGTH] PROGMEM = {0x02, 0x92, 0x0F, 0x00, 0x00, 0x00, 0xF0,
25 0x01, 0xE2, 0xFE, 0x71, 0x80, 0x11, 0xF0};
26// Middle section of a power on/off frame: an all-zero timer block
27static const uint8_t TIMER_SECTION[SAMSUNG_AC_SECTION_LENGTH] PROGMEM = {0x01, 0xD2, 0x0F, 0x00, 0x00, 0x00, 0x00};
28
29// Where each setting lives in the 14 byte state
30struct Field {
31 uint8_t byte;
32 uint8_t shift;
33 uint8_t mask;
34};
35static constexpr Field POWER_1{6, 4, 0b11};
36static constexpr Field SWING{9, 4, 0b111};
37static constexpr Field TEMP{11, 4, 0b1111};
38static constexpr Field FAN{12, 1, 0b111};
39static constexpr Field MODE{12, 4, 0b111};
40static constexpr Field POWER_2{13, 4, 0b11};
41static constexpr uint8_t POWER_ON = 0b11;
42
43static uint8_t get_field(const uint8_t *state, Field field) { return (state[field.byte] >> field.shift) & field.mask; }
44static void set_field(uint8_t *state, Field field, uint8_t value) {
45 state[field.byte] = (state[field.byte] & ~(field.mask << field.shift)) | ((value & field.mask) << field.shift);
46}
47static bool is_powered_on(const uint8_t *state) {
48 return get_field(state, POWER_1) == POWER_ON && get_field(state, POWER_2) == POWER_ON;
49}
50
51struct Code {
52 uint8_t value; // a climate enum, all of which are uint8_t
53 uint8_t code;
54};
55// The first entry of each table is the fallback in both directions
56static constexpr Code MODES[] PROGMEM = {
59};
60static constexpr Code FANS[] PROGMEM = {
65};
66static constexpr Code SWINGS[] PROGMEM = {
71};
72
73// Returns the entry whose value (or code) matches key, else the first entry. Entries are copied out of PROGMEM.
74static Code find_entry(const Code *table, size_t count, uint8_t key, bool match_code) {
75 Code entry{};
76 for (size_t i = 0; i < count; i++) {
77 progmem_memcpy(&entry, &table[i], sizeof(entry));
78 if ((match_code ? entry.code : entry.value) == key)
79 return entry;
80 }
81 progmem_memcpy(&entry, table, sizeof(entry));
82 return entry;
83}
84template<size_t N> static uint8_t to_code(const Code (&table)[N], uint8_t value) {
85 return find_entry(table, N, value, false).code;
86}
87template<typename T, size_t N> static T from_code(const Code (&table)[N], uint8_t code) {
88 return static_cast<T>(find_entry(table, N, code, true).value);
89}
90
91// Inverted count of the one bits in a section, skipping the two nibbles that hold the checksum itself.
92// See https://github.com/crankyoldgit/IRremoteESP8266/issues/1538#issuecomment-894645947
93static uint8_t section_checksum(const uint8_t *section) {
94 uint32_t ones =
95 __builtin_popcount(section[0]) + __builtin_popcount(section[1] & 0x0F) + __builtin_popcount(section[2] & 0xF0);
96 for (uint8_t i = 3; i < SAMSUNG_AC_SECTION_LENGTH; i++) {
97 ones += __builtin_popcount(section[i]);
98 }
99 return ~static_cast<uint8_t>(ones);
100}
101static uint8_t stored_checksum(const uint8_t *section) { return (section[1] >> 4) | (section[2] << 4); }
102static void store_checksum(uint8_t *section) {
103 const uint8_t sum = section_checksum(section);
104 section[1] = (section[1] & 0x0F) | (sum << 4);
105 section[2] = (section[2] & 0xF0) | (sum >> 4);
106}
107
109 : climate_ir::ClimateIR(
110 SAMSUNG_AC_TEMP_MIN, SAMSUNG_AC_TEMP_MAX, 1.0f, true, true,
114 progmem_memcpy(this->state_.data(), RESET_STATE, SAMSUNG_AC_STATE_LENGTH);
115 // The unit's power state is unknown at boot, so the first "on" command sends a power frame
116 set_field(this->state_.data(), POWER_1, 0);
117 set_field(this->state_.data(), POWER_2, 0);
118}
119
121 uint8_t *state = this->state_.data();
122 const bool was_on = is_powered_on(state);
123 const bool on = this->mode != climate::CLIMATE_MODE_OFF;
124
125 if (on) {
126 progmem_memcpy(state, RESET_STATE, SAMSUNG_AC_STATE_LENGTH);
127 set_field(state, MODE, to_code(MODES, this->mode));
128 } // else keep the last mode so the power off frame still carries a valid state
129
130 const uint8_t temp = clamp<uint8_t>(this->target_temperature, SAMSUNG_AC_TEMP_MIN, SAMSUNG_AC_TEMP_MAX);
131 set_field(state, TEMP, temp - SAMSUNG_AC_TEMP_MIN);
132 set_field(state, SWING, to_code(SWINGS, this->swing_mode));
133 set_field(state, FAN, to_code(FANS, this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO)));
134 set_field(state, POWER_1, on ? POWER_ON : 0);
135 set_field(state, POWER_2, on ? POWER_ON : 0);
136
137 if (on != was_on) {
138 // A power change is sent as an extended frame with the timer block between the two state sections
139 uint8_t timer[SAMSUNG_AC_SECTION_LENGTH];
140 progmem_memcpy(timer, TIMER_SECTION, SAMSUNG_AC_SECTION_LENGTH);
141 const uint8_t *sections[] = {state, timer, state + SAMSUNG_AC_SECTION_LENGTH};
142 this->send_sections_(sections, 3);
143 } else {
144 const uint8_t *sections[] = {state, state + SAMSUNG_AC_SECTION_LENGTH};
145 this->send_sections_(sections, 2);
146 }
147}
148
149void SamsungClimate::send_sections_(const uint8_t *const *sections, uint8_t count) {
150 auto transmit = this->transmitter_->transmit();
151 auto *data = transmit.get_data();
152
153 // Header(2) + (sections - 1) * separator(4) + bits(2 each) + trailing mark(1)
154 data->reserve(2 + (count - 1) * 4 + count * SAMSUNG_AC_SECTION_LENGTH * 8 * 2 + 1);
155 data->set_carrier_frequency(CARRIER_FREQUENCY_HZ);
156 data->item(HEADER_MARK_US, HEADER_SPACE_US);
157
158 for (uint8_t s = 0; s < count; s++) {
159 if (s != 0) {
160 data->item(BIT_MARK_US, SECTION_SPACE_US);
161 data->item(HEADER_MARK_US, HEADER_SPACE_US);
162 }
163 uint8_t section[SAMSUNG_AC_SECTION_LENGTH];
164 memcpy(section, sections[s], SAMSUNG_AC_SECTION_LENGTH);
165 store_checksum(section);
166 for (uint8_t byte : section) {
167 for (uint8_t bit = 0; bit < 8; bit++, byte >>= 1) {
168 data->item(BIT_MARK_US, (byte & 1) ? ONE_SPACE_US : ZERO_SPACE_US);
169 }
170 }
171 }
172
173 data->mark(BIT_MARK_US);
174 transmit.perform();
175}
176
178 if (!data.expect_item(HEADER_MARK_US, HEADER_SPACE_US)) {
179 return false;
180 }
181
182 // Decode into a local buffer so a bad frame leaves the stored state untouched
183 uint8_t frame[EXTENDED_LENGTH] = {0};
184 uint8_t length = EXTENDED_LENGTH;
185 for (uint8_t i = 0; i < EXTENDED_LENGTH; i++) {
186 if (i != 0 && i % SAMSUNG_AC_SECTION_LENGTH == 0) {
187 const bool have_separator =
188 data.expect_item(BIT_MARK_US, SECTION_SPACE_US) && data.expect_item(HEADER_MARK_US, HEADER_SPACE_US);
189 if (!have_separator) {
190 // A third section is optional; a missing separator after the second one ends the frame
191 if (i >= SAMSUNG_AC_STATE_LENGTH) {
192 length = i;
193 break;
194 }
195 ESP_LOGW(TAG, "Missing section separator before byte %u", i);
196 return false;
197 }
198 }
199 for (uint8_t bit = 0; bit < 8; bit++) {
200 if (data.expect_item(BIT_MARK_US, ONE_SPACE_US)) {
201 frame[i] |= 1 << bit;
202 } else if (!data.expect_item(BIT_MARK_US, ZERO_SPACE_US)) {
203 ESP_LOGW(TAG, "Bad bit %u in byte %u", bit, i);
204 return false;
205 }
206 }
207 }
208 if (!data.expect_mark(BIT_MARK_US)) {
209 ESP_LOGVV(TAG, "Footer fail");
210 return false;
211 }
212 for (uint8_t offset = 0; offset < length; offset += SAMSUNG_AC_SECTION_LENGTH) {
213 if (stored_checksum(frame + offset) != section_checksum(frame + offset)) {
214 ESP_LOGW(TAG, "Checksum mismatch in section %u", offset / SAMSUNG_AC_SECTION_LENGTH);
215 return false;
216 }
217 }
218
219 // In an extended frame the middle section is the timer block and the last one the second state section
220 uint8_t *state = this->state_.data();
221 memcpy(state, frame, SAMSUNG_AC_SECTION_LENGTH);
222 memcpy(state + SAMSUNG_AC_SECTION_LENGTH, frame + length - SAMSUNG_AC_SECTION_LENGTH, SAMSUNG_AC_SECTION_LENGTH);
223
224 this->swing_mode = from_code<climate::ClimateSwingMode>(SWINGS, get_field(state, SWING));
225 this->target_temperature = get_field(state, TEMP) + SAMSUNG_AC_TEMP_MIN;
226 this->fan_mode = from_code<climate::ClimateFanMode>(FANS, get_field(state, FAN));
227 this->mode =
228 is_powered_on(state) ? from_code<climate::ClimateMode>(MODES, get_field(state, MODE)) : climate::CLIMATE_MODE_OFF;
229
230 ESP_LOGD(TAG, "Received %u byte frame, power %s", length, ONOFF(is_powered_on(state)));
231 this->publish_state();
232 return true;
233}
234
235} // namespace esphome::samsung
ClimateMode mode
The active mode of the climate device.
Definition climate.h:304
optional< ClimateFanMode > fan_mode
The active fan mode of the climate device.
Definition climate.h:298
float target_temperature
The target temperature of the climate device.
Definition climate.h:285
ClimateSwingMode swing_mode
The active swing mode of the climate device.
Definition climate.h:310
void publish_state()
Publish the state of the climate device, to be called from integrations.
Definition climate.cpp:429
bool on_receive(remote_base::RemoteReceiveData data) override
Definition samsung.cpp:177
std::array< uint8_t, SAMSUNG_AC_STATE_LENGTH > state_
Definition samsung.h:28
void send_sections_(const uint8_t *const *sections, uint8_t count)
Send the given sections as one frame, each with its checksum filled in.
Definition samsung.cpp:149
bool state
Definition fan.h:2
unsigned count
Definition gif.cpp:51
uint16_t MODE
Definition ina2xx_base.h:4
constexpr float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM
Definition bme680.cpp:26
@ CLIMATE_SWING_OFF
The swing mode is set to Off.
@ CLIMATE_SWING_HORIZONTAL
The fan mode is set to Horizontal.
@ CLIMATE_SWING_VERTICAL
The fan mode is set to Vertical.
@ CLIMATE_SWING_BOTH
The fan mode is set to Both.
@ 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_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.
ESPHOME_LOG_TAG(TAG, "samsung.climate")
void progmem_memcpy(void *dst, const void *src, size_t len)
Definition hal.h:56
static void uint32_t