[friedrich] New Climate IR component (#9178)

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
rwrozelle
2026-10-12 17:11:21 -10:00
committed by GitHub
co-authored by Claude Sonnet 4.6 J. Nick Koston J. Nick Koston
parent 5017165800
commit e289a94451
10 changed files with 558 additions and 0 deletions
+1
View File
@@ -214,6 +214,7 @@ esphome/components/feedback/* @ianchi
esphome/components/file/* @esphome/core
esphome/components/fingerprint_grow/* @alexborro @loongyh @OnFreund
esphome/components/font/* @clydebarrow @esphome/core
esphome/components/friedrich/* @rwrozelle
esphome/components/fs3000/* @kahrendt
esphome/components/ft5x06/* @clydebarrow
esphome/components/ft63x6/* @gpambrozio
+1
View File
@@ -0,0 +1 @@
DOMAIN = "friedrich"
+32
View File
@@ -0,0 +1,32 @@
import esphome.codegen as cg
from esphome.components import climate_ir, remote_base
import esphome.config_validation as cv
from esphome.const import CONF_MODEL, CONF_SENSOR
from esphome.types import ConfigType
AUTO_LOAD = ["climate_ir"]
CODEOWNERS = ["@rwrozelle"]
friedrich_ns = cg.esphome_ns.namespace("friedrich")
FriedrichClimate = friedrich_ns.class_("FriedrichClimate", climate_ir.ClimateIR)
Model = friedrich_ns.enum("Model", is_class=True)
MODELS = {
"MW12Y3H": Model.MODEL_MW12Y3H,
}
CONFIG_SCHEMA = climate_ir.climate_ir_with_receiver_schema(FriedrichClimate).extend(
{
cv.Optional(CONF_MODEL, default="MW12Y3H"): cv.enum(MODELS, upper=True),
# The unit works in Fahrenheit, so a Celsius temperature sensor cannot feed it
cv.Optional(CONF_SENSOR): cv.invalid(
"'sensor' is not supported by the Friedrich climate component"
),
}
)
async def to_code(config: ConfigType) -> None:
remote_base.request_protocol("aeha") # used from C++
var = await climate_ir.new_climate_ir(config)
cg.add(var.set_model(config[CONF_MODEL]))
+234
View File
@@ -0,0 +1,234 @@
#include "friedrich.h"
#include <cmath>
#include "esphome/components/remote_base/aeha_protocol.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
namespace esphome::friedrich {
// clang-format off
/*
* MW12Y3H - Currently the only supported model. Model field is retained to allow future per-model code differentiation.
* AEHA
* Byte0 Fixed = 0x00
* Byte1 Fixed = 0x08
* Byte2 Fixed = 0x08
* Byte3 Command: Move Louver = 0x36 Off = 0x40, else Fixed = 0x7F
* Byte4 Checksum when command, else Fixed = 0x90
* Byte5 N/A when command, else Fixed = 0x0C
* Byte6 N/A when command, else Temp(F), add 0x80 for On
* Byte7 N/A when command, else Mode (Min Heat = 0xD0 and Coil Dry = 0x10 are not exposed)
* Byte8 N/A when command, else Fan, add 0x08 for Vertical Swing
* Byte9 N/A when command, else Fixed = 0x00 // not interested
* Byte10 N/A when command, else Fixed = 0x00 // not interested
* Byte11 N/A when command, else Fixed = 0x00 // not interested
* Byte12 N/A when command, else Economy (On = 0x00, Off = 0x04)
* Byte13 N/A when command, else CheckSum
*/
// clang-format on
ESPHOME_LOG_TAG(TAG, "friedrich");
static constexpr uint8_t TEMP_MIN_NOT_HEAT = 64;
static constexpr uint8_t TEMP_DEFAULT = 72;
static constexpr uint8_t STATE_MESSAGE_LENGTH = FriedrichClimate::STATE_MESSAGE_LENGTH;
static constexpr uint8_t STATE_MESSAGE_LENGTH_UTIL = 5;
static constexpr uint16_t CARRIER_ADDRESS = 0x28C6;
static constexpr uint32_t CARRIER_FREQUENCY = 38000;
static constexpr uint8_t BYTE3_POWER_OFF = 0x40;
static constexpr uint8_t BYTE6_POWER_ON = 0x80;
static constexpr uint8_t BYTE7_MODE_AUTO = 0x00;
static constexpr uint8_t BYTE8_FAN_AUTO = 0x00;
static constexpr uint8_t BYTE8_FAN_SWING = 0x08;
// Every state frame: power on, economy off; bytes 6, 7, 8 and 13 are patched per state
static constexpr uint8_t STATE_TEMPLATE[STATE_MESSAGE_LENGTH] PROGMEM = {
0x00, 0x08, 0x08, 0x7F, 0x90, 0x0C, BYTE6_POWER_ON, BYTE7_MODE_AUTO, BYTE8_FAN_AUTO, 0x00, 0x00, 0x00, 0x04, 0x00};
// The off command, including its checksum (255 - byte 3)
static constexpr uint8_t OFF_FRAME[STATE_MESSAGE_LENGTH_UTIL] PROGMEM = {0x00, 0x08, 0x08, BYTE3_POWER_OFF,
255 - BYTE3_POWER_OFF};
struct Code {
uint8_t value; // a climate enum, all of which are uint8_t
uint8_t code;
};
static constexpr Code MODES[] PROGMEM = {
{climate::CLIMATE_MODE_HEAT_COOL, BYTE7_MODE_AUTO},
{climate::CLIMATE_MODE_COOL, 0x80},
{climate::CLIMATE_MODE_DRY, 0x40},
{climate::CLIMATE_MODE_FAN_ONLY, 0xC0},
{climate::CLIMATE_MODE_HEAT, 0x20},
};
static constexpr Code FANS[] PROGMEM = {
{climate::CLIMATE_FAN_AUTO, BYTE8_FAN_AUTO}, {climate::CLIMATE_FAN_HIGH, 0x80},
{climate::CLIMATE_FAN_MEDIUM, 0x40}, {climate::CLIMATE_FAN_LOW, 0xC0},
{climate::CLIMATE_FAN_QUIET, 0x20},
};
// The entry whose value (or code) matches key. Entries are copied out of PROGMEM.
template<size_t N> static optional<Code> find_entry(const Code (&table)[N], uint8_t key, bool match_code) {
for (const Code &slot : table) {
Code entry;
progmem_memcpy(&entry, &slot, sizeof(entry));
if ((match_code ? entry.code : entry.value) == key)
return entry;
}
return {};
}
// The temperature code is the 4-bit reversal of (F - TEMP_MIN) / TEMP_STEP; the mapping is its own inverse.
static uint8_t reverse_nibble(uint8_t value) { return reverse_bits(value) >> 4; }
static bool matches_template(const uint8_t *data, uint8_t from, uint8_t to) {
for (uint8_t i = from; i <= to; i++) {
if (data[i] != progmem_read_byte(&STATE_TEMPLATE[i]))
return false;
}
return true;
}
/*
* https://gist.github.com/GeorgeDewar/11171561
* 1. Reverse (flip) bytes 6 - 13)
* 2. Sum those bytes
* 3. (208 - sum) % 256
* 4. Reverse (flip) bytes of result
*/
static uint8_t checksum_state(const uint8_t *data) {
uint8_t sum = 0;
for (uint8_t i = 6; i < STATE_MESSAGE_LENGTH - 1; ++i) {
sum += reverse_bits(data[i]);
}
return reverse_bits(static_cast<uint8_t>(208 - sum));
}
static const LogString *model_to_string(Model model) {
switch (model) {
case Model::MODEL_MW12Y3H:
default:
return LOG_STR("MW12Y3H");
}
}
climate::ClimateTraits FriedrichClimate::traits() {
auto traits = ClimateIR::traits();
traits.set_temperature_unit(TemperatureUnit::FAHRENHEIT);
return traits;
}
void FriedrichClimate::setup() {
ClimateIR::setup();
// The base class defaults assume Celsius. This also discards a value saved in Celsius by an earlier version.
if (std::isnan(this->target_temperature) || this->target_temperature < TEMP_MIN) {
this->target_temperature = TEMP_DEFAULT;
}
this->target_temperature = std::min<float>(this->target_temperature, TEMP_MAX);
}
void FriedrichClimate::dump_config() {
// ClimateIR::dump_config() is not called: it prints the temperature range in Celsius
LOG_CLIMATE("", "Friedrich Climate", this);
ESP_LOGCONFIG(TAG,
" Temperature range: %u to %u °F\n"
" Model: %s",
TEMP_MIN, TEMP_MAX, LOG_STR_ARG(model_to_string(this->model_)));
}
void FriedrichClimate::transmit_state() {
if (this->mode == climate::CLIMATE_MODE_OFF) {
this->transmit_off_();
return;
}
ESP_LOGV(TAG, "Transmit state");
// Snap to what the unit accepts (even values, 64°F minimum unless heating) and store it,
// so the state published after this call matches the frame sent.
const uint8_t effective_min = this->mode == climate::CLIMATE_MODE_HEAT ? TEMP_MIN : TEMP_MIN_NOT_HEAT;
uint8_t temperature = (uint8_t) roundf(clamp<float>(this->target_temperature, effective_min, TEMP_MAX));
temperature += temperature % 2;
this->target_temperature = temperature;
const auto mode = find_entry(MODES, this->mode, false);
const auto fan = find_entry(FANS, this->fan_mode.value_or(climate::CLIMATE_FAN_AUTO), false);
uint8_t frame[STATE_MESSAGE_LENGTH];
progmem_memcpy(frame, STATE_TEMPLATE, sizeof(frame));
frame[6] |= reverse_nibble((temperature - TEMP_MIN) / TEMP_STEP);
frame[7] = mode.has_value() ? mode->code : BYTE7_MODE_AUTO;
frame[8] = fan.has_value() ? fan->code : BYTE8_FAN_AUTO;
if (this->swing_mode != climate::CLIMATE_SWING_OFF)
frame[8] |= BYTE8_FAN_SWING;
frame[STATE_MESSAGE_LENGTH - 1] = checksum_state(frame);
this->transmit_(frame, sizeof(frame));
}
void FriedrichClimate::transmit_off_() {
ESP_LOGV(TAG, "Transmit off");
uint8_t frame[STATE_MESSAGE_LENGTH_UTIL];
progmem_memcpy(frame, OFF_FRAME, sizeof(frame));
this->transmit_(frame, sizeof(frame));
}
void FriedrichClimate::transmit_(const uint8_t *data, uint8_t len) {
ESP_LOGV(TAG, "Transmit message");
auto transmit = this->transmitter_->transmit();
auto *dst = transmit.get_data();
dst->set_carrier_frequency(CARRIER_FREQUENCY);
remote_base::AEHAProtocol().encode(dst, CARRIER_ADDRESS, data, len);
transmit.perform();
}
bool FriedrichClimate::on_receive(remote_base::RemoteReceiveData src) {
ESP_LOGV(TAG, "Received message");
const auto odata = remote_base::AEHAProtocol().decode(src);
if (!odata.has_value())
return false;
const auto &d = odata->data;
// Other vendors also use AEHA; only accept frames with our address and fixed header bytes.
if (odata->address != CARRIER_ADDRESS || d.size() < STATE_MESSAGE_LENGTH_UTIL || !matches_template(d.data(), 0, 2))
return false;
if (d.size() == STATE_MESSAGE_LENGTH_UTIL) {
// Not looking for other types of messages
if (d[3] != BYTE3_POWER_OFF || d[4] != 255 - d[3])
return false;
ESP_LOGV(TAG, "Received off message");
this->mode = climate::CLIMATE_MODE_OFF;
this->publish_state();
return true;
}
if (d.size() != STATE_MESSAGE_LENGTH || d[STATE_MESSAGE_LENGTH - 1] != checksum_state(d.data()) ||
!matches_template(d.data(), 3, 5))
return false;
// Ignore frames with a mode this component does not expose (e.g. MIN_HEAT, COIL_DRY)
const auto mode = find_entry(MODES, d[7], true);
if (!mode.has_value()) {
ESP_LOGV(TAG, "Ignoring unknown mode 0x%02X", d[7]);
return false;
}
this->mode = static_cast<climate::ClimateMode>(mode->value);
// State frames always carry the power-on flag in bit 7 (power off is the separate 5-byte frame), so it is
// stripped rather than acted on
const uint8_t temp_code = d[6] & ~BYTE6_POWER_ON;
const uint8_t temp_index = reverse_nibble(temp_code);
if (temp_code <= 0x0F && temp_index <= (TEMP_MAX - TEMP_MIN) / TEMP_STEP)
this->target_temperature = TEMP_MIN + temp_index * TEMP_STEP;
this->swing_mode = (d[8] & BYTE8_FAN_SWING) ? climate::CLIMATE_SWING_VERTICAL : climate::CLIMATE_SWING_OFF;
const auto fan = find_entry(FANS, d[8] & ~BYTE8_FAN_SWING, true);
this->fan_mode = fan.has_value() ? static_cast<climate::ClimateFanMode>(fan->value) : climate::CLIMATE_FAN_AUTO;
this->publish_state();
return true;
}
} // namespace esphome::friedrich
+47
View File
@@ -0,0 +1,47 @@
#pragma once
#include "esphome/components/climate_ir/climate_ir.h"
namespace esphome::friedrich {
// Extension point for other units: add new models here and branch on model_ where they differ.
// The default must stay MW12Y3H so existing configurations do not change.
enum class Model : uint8_t {
MODEL_MW12Y3H, // MW12Y3H built from a remote that only provided Fahrenheit
};
class FriedrichClimate : public climate_ir::ClimateIR {
public:
// The unit reports temperatures in Fahrenheit (see traits()); the unit only accepts even values.
static constexpr uint8_t TEMP_MIN = 60; // actually 64 for anything but heating
static constexpr uint8_t TEMP_MAX = 88;
static constexpr uint8_t TEMP_STEP = 2;
static constexpr uint8_t STATE_MESSAGE_LENGTH = 14;
FriedrichClimate()
: ClimateIR(TEMP_MIN, TEMP_MAX, TEMP_STEP, true, true,
{climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_QUIET},
{climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL}) {}
void set_model(Model model) { this->model_ = model; }
protected:
void setup() override;
void dump_config() override;
climate::ClimateTraits traits() override;
/// Transmit via IR the state of this climate controller.
void transmit_state() override;
/// Transmit via IR power off command.
void transmit_off_();
/// Parse incoming message
bool on_receive(remote_base::RemoteReceiveData src) override;
/// Transmit message as IR pulses
void transmit_(const uint8_t *data, uint8_t len);
Model model_{Model::MODEL_MW12Y3H};
};
} // namespace esphome::friedrich
+15
View File
@@ -0,0 +1,15 @@
from esphome.types import ConfigType
from tests.testing_helpers import ComponentManifestOverride
def override_manifest(manifest: ComponentManifestOverride) -> None:
from esphome.components.remote_base import request_protocol
# The harness walks the component manifest, not the climate platform, so declare its auto-load here
manifest.auto_load = ["climate_ir", "climate"]
async def to_code_testing(config: ConfigType) -> None:
# The AEHA source is compiled only behind its define; climate.py requests it in the real build.
request_protocol("aeha")
manifest.to_code = to_code_testing
+5
View File
@@ -0,0 +1,5 @@
climate:
- platform: friedrich
name: Friedrich Climate
transmitter_id: xmitr
receiver_id: rcvr
@@ -0,0 +1,215 @@
#include <gtest/gtest.h>
#include <functional>
#include "esphome/components/friedrich/friedrich.h"
#include "esphome/components/remote_base/aeha_protocol.h"
#include "esphome/core/helpers.h"
namespace esphome::friedrich::testing {
using remote_base::AEHAData;
using remote_base::AEHAProtocol;
// Records the last transmission instead of sending it
class CapturingTransmitter : public remote_base::RemoteTransmitterBase {
public:
CapturingTransmitter() : RemoteTransmitterBase(nullptr) {}
int sends{0};
remote_base::RawTimings last;
protected:
void send_internal(uint32_t, uint32_t) override {
this->sends++;
this->last = this->temp_.get_data();
}
};
class TestFriedrich : public FriedrichClimate {
public:
using FriedrichClimate::on_receive;
using FriedrichClimate::setup;
using FriedrichClimate::traits;
using FriedrichClimate::transmit_state;
};
constexpr uint16_t ADDRESS = 0x28C6;
constexpr uint8_t STATE_MESSAGE_LENGTH = FriedrichClimate::STATE_MESSAGE_LENGTH;
// Byte 6 holds the temperature code (bit 7 is the power flag), byte 7 the mode, byte 8 the fan
constexpr uint8_t TEMP_72_CODE = 0x06;
struct Fixture {
TestFriedrich sut;
CapturingTransmitter transmitter;
Fixture() { this->sut.set_transmitter(&this->transmitter); }
// The last transmission decoded, or an empty frame if it could not be decoded
AEHAData sent() { return AEHAProtocol().decode(rx_of(this->transmitter.last)).value_or(AEHAData{}); }
bool receive(const AEHAData &frame) {
remote_base::RemoteTransmitData tx;
AEHAProtocol().encode(&tx, frame);
return this->sut.on_receive(rx_of(tx.get_data()));
}
static remote_base::RemoteReceiveData rx_of(const remote_base::RawTimings &timings) {
return remote_base::RemoteReceiveData(timings, 25, remote_base::TOLERANCE_MODE_PERCENTAGE);
}
};
// Independent implementation of the 14 byte frame checksum
static AEHAData state_frame(uint8_t temp_code, uint8_t mode, uint8_t fan = 0x00) {
AEHAData f{ADDRESS, {0x00, 0x08, 0x08, 0x7F, 0x90, 0x0C, (uint8_t) (temp_code | 0x80), mode, fan, 0, 0, 0, 0x04, 0}};
uint8_t sum = 0;
for (int i = 6; i < 13; i++)
sum += reverse_bits(f.data[i]);
f.data[13] = reverse_bits(static_cast<uint8_t>(208 - sum));
return f;
}
static AEHAData off_frame() { return AEHAData{ADDRESS, {0x00, 0x08, 0x08, 0x40, 0xBF}}; }
TEST(FriedrichTests, TraitsReportFahrenheit) {
Fixture f;
auto traits = f.sut.traits();
EXPECT_EQ(traits.get_temperature_unit(), TemperatureUnit::FAHRENHEIT);
EXPECT_FLOAT_EQ(traits.get_visual_min_temperature(), 60.0f);
EXPECT_FLOAT_EQ(traits.get_visual_max_temperature(), 88.0f);
EXPECT_FLOAT_EQ(traits.get_visual_target_temperature_step(), 2.0f);
}
TEST(FriedrichTests, OffIsAlwaysSentAsFiveByteFrame) {
Fixture f;
f.sut.mode = climate::CLIMATE_MODE_OFF;
f.sut.transmit_state();
ASSERT_EQ(f.transmitter.sends, 1);
EXPECT_EQ(f.sent(), off_frame());
}
TEST(FriedrichTests, StateFrameEncodesFahrenheitTarget) {
Fixture f;
f.sut.mode = climate::CLIMATE_MODE_COOL;
f.sut.target_temperature = 72;
f.sut.fan_mode = climate::CLIMATE_FAN_AUTO;
f.sut.swing_mode = climate::CLIMATE_SWING_OFF;
f.sut.transmit_state();
EXPECT_EQ(f.sent(), state_frame(TEMP_72_CODE, 0x80));
}
TEST(FriedrichTests, OddTargetStepsUpToEven) {
Fixture f;
f.sut.mode = climate::CLIMATE_MODE_COOL;
f.sut.target_temperature = 71;
f.sut.transmit_state();
auto data = f.sent();
ASSERT_EQ(data.data.size(), STATE_MESSAGE_LENGTH);
EXPECT_EQ(data.data[6], TEMP_72_CODE | 0x80);
EXPECT_FLOAT_EQ(f.sut.target_temperature, 72.0f); // stored value matches what was sent
}
// Codes captured from the remote
TEST(FriedrichTests, EveryTemperatureEncodesToRemoteCode) {
static constexpr uint8_t CODES[] = {0x00, 0x08, 0x04, 0x0C, 0x02, 0x0A, 0x06, 0x0E,
0x01, 0x09, 0x05, 0x0D, 0x03, 0x0B, 0x07};
for (size_t i = 0; i < sizeof(CODES); i++) {
const auto fahrenheit = static_cast<uint8_t>(60 + i * 2);
SCOPED_TRACE(fahrenheit);
Fixture f;
f.sut.mode = climate::CLIMATE_MODE_HEAT;
f.sut.target_temperature = fahrenheit;
f.sut.transmit_state();
EXPECT_EQ(f.sent(), state_frame(CODES[i], 0x20));
Fixture g;
EXPECT_TRUE(g.receive(state_frame(CODES[i], 0x20)));
EXPECT_FLOAT_EQ(g.sut.target_temperature, fahrenheit);
}
}
TEST(FriedrichTests, NonHeatModesAreRaisedToSixtyFour) {
Fixture f;
f.sut.mode = climate::CLIMATE_MODE_COOL;
f.sut.target_temperature = 60;
f.sut.transmit_state();
EXPECT_FLOAT_EQ(f.sut.target_temperature, 64.0f);
auto data = f.sent();
ASSERT_EQ(data.data.size(), STATE_MESSAGE_LENGTH);
EXPECT_EQ(data.data[6], 0x04 | 0x80); // 64 F
f.sut.mode = climate::CLIMATE_MODE_HEAT;
f.sut.target_temperature = 60;
f.sut.transmit_state();
EXPECT_FLOAT_EQ(f.sut.target_temperature, 60.0f);
}
TEST(FriedrichTests, ReceivesStateFrame) {
Fixture f;
EXPECT_TRUE(f.receive(state_frame(TEMP_72_CODE, 0x20, 0x80)));
EXPECT_EQ(f.sut.mode, climate::CLIMATE_MODE_HEAT);
EXPECT_FLOAT_EQ(f.sut.target_temperature, 72.0f);
EXPECT_EQ(f.sut.fan_mode, climate::CLIMATE_FAN_HIGH);
}
TEST(FriedrichTests, ReceivesOffFrame) {
Fixture f;
f.sut.mode = climate::CLIMATE_MODE_COOL;
EXPECT_TRUE(f.receive(off_frame()));
EXPECT_EQ(f.sut.mode, climate::CLIMATE_MODE_OFF);
}
TEST(FriedrichTests, IgnoresUnknownModeWithoutChangingState) {
Fixture f;
f.sut.mode = climate::CLIMATE_MODE_COOL;
f.sut.target_temperature = 80;
EXPECT_FALSE(f.receive(state_frame(TEMP_72_CODE, 0xD0))); // MIN_HEAT is not exposed
EXPECT_EQ(f.sut.mode, climate::CLIMATE_MODE_COOL);
EXPECT_FLOAT_EQ(f.sut.target_temperature, 80.0f);
}
TEST(FriedrichTests, IgnoresCorruptedFrames) {
struct Case {
const char *name;
AEHAData frame;
std::function<void(AEHAData &)> corrupt;
};
const Case cases[] = {
{"other address", off_frame(), [](AEHAData &f) { f.address = 0x1234; }},
{"bad header byte", off_frame(), [](AEHAData &f) { f.data[1] = 0x09; }},
{"bad off checksum", off_frame(), [](AEHAData &f) { f.data[4] ^= 0xFF; }},
{"bad state checksum", state_frame(TEMP_72_CODE, 0x20), [](AEHAData &f) { f.data[13] ^= 0xFF; }},
};
for (const auto &c : cases) {
SCOPED_TRACE(c.name);
Fixture f;
f.sut.mode = climate::CLIMATE_MODE_COOL;
AEHAData frame = c.frame;
c.corrupt(frame);
EXPECT_FALSE(f.receive(frame));
EXPECT_EQ(f.sut.mode, climate::CLIMATE_MODE_COOL);
}
}
TEST(FriedrichTests, OwnTransmissionIsAccepted) {
Fixture f;
f.sut.mode = climate::CLIMATE_MODE_FAN_ONLY;
f.sut.target_temperature = 76;
f.sut.fan_mode = climate::CLIMATE_FAN_LOW;
f.sut.swing_mode = climate::CLIMATE_SWING_VERTICAL;
f.sut.transmit_state();
Fixture g;
EXPECT_TRUE(g.sut.on_receive(Fixture::rx_of(f.transmitter.last)));
EXPECT_EQ(g.sut.mode, climate::CLIMATE_MODE_FAN_ONLY);
EXPECT_FLOAT_EQ(g.sut.target_temperature, 76.0f);
EXPECT_EQ(g.sut.fan_mode, climate::CLIMATE_FAN_LOW);
EXPECT_EQ(g.sut.swing_mode, climate::CLIMATE_SWING_VERTICAL);
}
TEST(FriedrichTests, SetupDefaultsTargetToSeventyTwoFahrenheit) {
Fixture f;
f.sut.setup();
EXPECT_FLOAT_EQ(f.sut.target_temperature, 72.0f);
EXPECT_EQ(f.sut.mode, climate::CLIMATE_MODE_OFF);
}
} // namespace esphome::friedrich::testing
@@ -0,0 +1,4 @@
packages:
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp32-idf.yaml
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp32-idf.yaml
friedrich: !include common.yaml
@@ -0,0 +1,4 @@
packages:
remote_receiver: !include ../../test_build_components/common/remote_receiver/esp8266-ard.yaml
remote_transmitter: !include ../../test_build_components/common/remote_transmitter/esp8266-ard.yaml
friedrich: !include common.yaml