ESPHome 2026.11.0-dev
Loading...
Searching...
No Matches
friedrich.cpp
Go to the documentation of this file.
1#include "friedrich.h"
2
3#include <cmath>
4
6#include "esphome/core/hal.h"
8#include "esphome/core/log.h"
9
11// clang-format off
12/*
13* MW12Y3H - Currently the only supported model. Model field is retained to allow future per-model code differentiation.
14* AEHA
15* Byte0 Fixed = 0x00
16* Byte1 Fixed = 0x08
17* Byte2 Fixed = 0x08
18* Byte3 Command: Move Louver = 0x36 Off = 0x40, else Fixed = 0x7F
19* Byte4 Checksum when command, else Fixed = 0x90
20* Byte5 N/A when command, else Fixed = 0x0C
21* Byte6 N/A when command, else Temp(F), add 0x80 for On
22* Byte7 N/A when command, else Mode (Min Heat = 0xD0 and Coil Dry = 0x10 are not exposed)
23* Byte8 N/A when command, else Fan, add 0x08 for Vertical Swing
24* Byte9 N/A when command, else Fixed = 0x00 // not interested
25* Byte10 N/A when command, else Fixed = 0x00 // not interested
26* Byte11 N/A when command, else Fixed = 0x00 // not interested
27* Byte12 N/A when command, else Economy (On = 0x00, Off = 0x04)
28* Byte13 N/A when command, else CheckSum
29*/
30// clang-format on
31ESPHOME_LOG_TAG(TAG, "friedrich");
32
33static constexpr uint8_t TEMP_MIN_NOT_HEAT = 64;
34static constexpr uint8_t TEMP_DEFAULT = 72;
35static constexpr uint8_t STATE_MESSAGE_LENGTH = FriedrichClimate::STATE_MESSAGE_LENGTH;
36static constexpr uint8_t STATE_MESSAGE_LENGTH_UTIL = 5;
37static constexpr uint16_t CARRIER_ADDRESS = 0x28C6;
38static constexpr uint32_t CARRIER_FREQUENCY = 38000;
39
40static constexpr uint8_t BYTE3_POWER_OFF = 0x40;
41static constexpr uint8_t BYTE6_POWER_ON = 0x80;
42static constexpr uint8_t BYTE7_MODE_AUTO = 0x00;
43static constexpr uint8_t BYTE8_FAN_AUTO = 0x00;
44static constexpr uint8_t BYTE8_FAN_SWING = 0x08;
45
46// Every state frame: power on, economy off; bytes 6, 7, 8 and 13 are patched per state
47static constexpr uint8_t STATE_TEMPLATE[STATE_MESSAGE_LENGTH] PROGMEM = {
48 0x00, 0x08, 0x08, 0x7F, 0x90, 0x0C, BYTE6_POWER_ON, BYTE7_MODE_AUTO, BYTE8_FAN_AUTO, 0x00, 0x00, 0x00, 0x04, 0x00};
49// The off command, including its checksum (255 - byte 3)
50static constexpr uint8_t OFF_FRAME[STATE_MESSAGE_LENGTH_UTIL] PROGMEM = {0x00, 0x08, 0x08, BYTE3_POWER_OFF,
51 255 - BYTE3_POWER_OFF};
52
53struct Code {
54 uint8_t value; // a climate enum, all of which are uint8_t
55 uint8_t code;
56};
57static constexpr Code MODES[] PROGMEM = {
58 {climate::CLIMATE_MODE_HEAT_COOL, BYTE7_MODE_AUTO},
63};
64static constexpr Code FANS[] PROGMEM = {
68};
69
70// The entry whose value (or code) matches key. Entries are copied out of PROGMEM.
71template<size_t N> static optional<Code> find_entry(const Code (&table)[N], uint8_t key, bool match_code) {
72 for (const Code &slot : table) {
73 Code entry;
74 progmem_memcpy(&entry, &slot, sizeof(entry));
75 if ((match_code ? entry.code : entry.value) == key)
76 return entry;
77 }
78 return {};
79}
80
81// The temperature code is the 4-bit reversal of (F - TEMP_MIN) / TEMP_STEP; the mapping is its own inverse.
82static uint8_t reverse_nibble(uint8_t value) { return reverse_bits(value) >> 4; }
83
84static bool matches_template(const uint8_t *data, uint8_t from, uint8_t to) {
85 for (uint8_t i = from; i <= to; i++) {
86 if (data[i] != progmem_read_byte(&STATE_TEMPLATE[i]))
87 return false;
88 }
89 return true;
90}
91
92/*
93 * https://gist.github.com/GeorgeDewar/11171561
94 * 1. Reverse (flip) bytes 6 - 13)
95 * 2. Sum those bytes
96 * 3. (208 - sum) % 256
97 * 4. Reverse (flip) bytes of result
98 */
99static uint8_t checksum_state(const uint8_t *data) {
100 uint8_t sum = 0;
101 for (uint8_t i = 6; i < STATE_MESSAGE_LENGTH - 1; ++i) {
102 sum += reverse_bits(data[i]);
103 }
104 return reverse_bits(static_cast<uint8_t>(208 - sum));
105}
106
107static const LogString *model_to_string(Model model) {
108 switch (model) {
110 default:
111 return LOG_STR("MW12Y3H");
112 }
113}
114
120
122 ClimateIR::setup();
123 // The base class defaults assume Celsius. This also discards a value saved in Celsius by an earlier version.
124 if (std::isnan(this->target_temperature) || this->target_temperature < TEMP_MIN) {
125 this->target_temperature = TEMP_DEFAULT;
126 }
127 this->target_temperature = std::min<float>(this->target_temperature, TEMP_MAX);
128}
129
131 // ClimateIR::dump_config() is not called: it prints the temperature range in Celsius
132 LOG_CLIMATE("", "Friedrich Climate", this);
133 ESP_LOGCONFIG(TAG,
134 " Temperature range: %u to %u °F\n"
135 " Model: %s",
136 TEMP_MIN, TEMP_MAX, LOG_STR_ARG(model_to_string(this->model_)));
137}
138
140 if (this->mode == climate::CLIMATE_MODE_OFF) {
141 this->transmit_off_();
142 return;
143 }
144
145 ESP_LOGV(TAG, "Transmit state");
146
147 // Snap to what the unit accepts (even values, 64°F minimum unless heating) and store it,
148 // so the state published after this call matches the frame sent.
149 const uint8_t effective_min = this->mode == climate::CLIMATE_MODE_HEAT ? TEMP_MIN : TEMP_MIN_NOT_HEAT;
150 uint8_t temperature = (uint8_t) roundf(clamp<float>(this->target_temperature, effective_min, TEMP_MAX));
153
154 const auto mode = find_entry(MODES, this->mode, false);
155 const auto fan = find_entry(FANS, this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO), false);
156
157 uint8_t frame[STATE_MESSAGE_LENGTH];
158 progmem_memcpy(frame, STATE_TEMPLATE, sizeof(frame));
159 frame[6] |= reverse_nibble((temperature - TEMP_MIN) / TEMP_STEP);
160 frame[7] = mode.has_value() ? mode->code : BYTE7_MODE_AUTO;
161 frame[8] = fan.has_value() ? fan->code : BYTE8_FAN_AUTO;
163 frame[8] |= BYTE8_FAN_SWING;
164 frame[STATE_MESSAGE_LENGTH - 1] = checksum_state(frame);
165
166 this->transmit_(frame, sizeof(frame));
167}
168
170 ESP_LOGV(TAG, "Transmit off");
171
172 uint8_t frame[STATE_MESSAGE_LENGTH_UTIL];
173 progmem_memcpy(frame, OFF_FRAME, sizeof(frame));
174 this->transmit_(frame, sizeof(frame));
175}
176
177void FriedrichClimate::transmit_(const uint8_t *data, uint8_t len) {
178 ESP_LOGV(TAG, "Transmit message");
179
180 auto transmit = this->transmitter_->transmit();
181 auto *dst = transmit.get_data();
182 dst->set_carrier_frequency(CARRIER_FREQUENCY);
183 remote_base::AEHAProtocol().encode(dst, CARRIER_ADDRESS, data, len);
184 transmit.perform();
185}
186
188 ESP_LOGV(TAG, "Received message");
189 const auto odata = remote_base::AEHAProtocol().decode(src);
190 if (!odata.has_value())
191 return false;
192 const auto &d = odata->data;
193 // Other vendors also use AEHA; only accept frames with our address and fixed header bytes.
194 if (odata->address != CARRIER_ADDRESS || d.size() < STATE_MESSAGE_LENGTH_UTIL || !matches_template(d.data(), 0, 2))
195 return false;
196
197 if (d.size() == STATE_MESSAGE_LENGTH_UTIL) {
198 // Not looking for other types of messages
199 if (d[3] != BYTE3_POWER_OFF || d[4] != 255 - d[3])
200 return false;
201 ESP_LOGV(TAG, "Received off message");
203 this->publish_state();
204 return true;
205 }
206
207 if (d.size() != STATE_MESSAGE_LENGTH || d[STATE_MESSAGE_LENGTH - 1] != checksum_state(d.data()) ||
208 !matches_template(d.data(), 3, 5))
209 return false;
210
211 // Ignore frames with a mode this component does not expose (e.g. MIN_HEAT, COIL_DRY)
212 const auto mode = find_entry(MODES, d[7], true);
213 if (!mode.has_value()) {
214 ESP_LOGV(TAG, "Ignoring unknown mode 0x%02X", d[7]);
215 return false;
216 }
217 this->mode = static_cast<climate::ClimateMode>(mode->value);
218
219 // State frames always carry the power-on flag in bit 7 (power off is the separate 5-byte frame), so it is
220 // stripped rather than acted on
221 const uint8_t temp_code = d[6] & ~BYTE6_POWER_ON;
222 const uint8_t temp_index = reverse_nibble(temp_code);
223 if (temp_code <= 0x0F && temp_index <= (TEMP_MAX - TEMP_MIN) / TEMP_STEP)
224 this->target_temperature = TEMP_MIN + temp_index * TEMP_STEP;
225
226 this->swing_mode = (d[8] & BYTE8_FAN_SWING) ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
227 const auto fan = find_entry(FANS, d[8] & ~BYTE8_FAN_SWING, true);
228 this->fan_mode = fan.has_value() ? static_cast<climate::ClimateFanMode>(fan->value) : climate::CLIMATE_FAN_AUTO;
229
230 this->publish_state();
231 return true;
232}
233
234} // namespace esphome::friedrich
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
void set_temperature_unit(TemperatureUnit unit)
void transmit_state() override
Transmit via IR the state of this climate controller.
static constexpr uint8_t TEMP_STEP
Definition friedrich.h:18
static constexpr uint8_t TEMP_MAX
Definition friedrich.h:17
bool on_receive(remote_base::RemoteReceiveData src) override
Parse incoming message.
climate::ClimateTraits traits() override
void transmit_off_()
Transmit via IR power off command.
static constexpr uint8_t STATE_MESSAGE_LENGTH
Definition friedrich.h:19
static constexpr uint8_t TEMP_MIN
Definition friedrich.h:16
void transmit_(const uint8_t *data, uint8_t len)
Transmit message as IR pulses.
optional< AEHAData > decode(RemoteReceiveData src)
void encode(RemoteTransmitData *dst, const AEHAData &data)
void set_carrier_frequency(uint32_t carrier_frequency)
Definition remote_base.h:31
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_VERTICAL
The fan mode is set to Vertical.
ClimateMode
Enum for all modes a climate device can be in.
@ 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.
ClimateFanMode
NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value.
@ 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_QUIET
The fan mode is set to Quiet.
@ CLIMATE_FAN_HIGH
The fan mode is set to High.
ESPHOME_LOG_TAG(TAG, "friedrich")
void progmem_memcpy(void *dst, const void *src, size_t len)
Definition hal.h:56
const void size_t len
Definition hal.h:68
const void * src
Definition hal.h:68
uint8_t reverse_bits(uint8_t x)
Reverse the order of 8 bits.
Definition helpers.h:1019
uint8_t progmem_read_byte(const uint8_t *addr)
Definition hal.h:51
static void uint32_t
uint16_t temperature
Definition sun_gtil2.cpp:12