diff --git a/esphome/components/climate_ir_lg/climate_ir_lg.cpp b/esphome/components/climate_ir_lg/climate_ir_lg.cpp index 042d1dcb21f..16ed9831fd7 100644 --- a/esphome/components/climate_ir_lg/climate_ir_lg.cpp +++ b/esphome/components/climate_ir_lg/climate_ir_lg.cpp @@ -135,13 +135,14 @@ void LgIrClimate::transmit_state() { default: return; } - this->transmit_(remote_state); - this->publish_state(); - return; } else { // just toggle swing when advanced_commands_support is not set remote_state |= HEADER_BASIC; remote_state |= BASIC_SWING_TOGGLE; } + // The remote's swing button sends a fixed code with no fan speed or temperature bits, so the + // frame goes out as is instead of falling through to them + this->transmit_(remote_state); + return; } else { // Mode commands const bool climate_is_off = (this->mode_before_ == climate::CLIMATE_MODE_OFF); switch (this->mode) { @@ -208,7 +209,6 @@ void LgIrClimate::transmit_state() { } this->transmit_(remote_state); - this->publish_state(); } bool LgIrClimate::on_receive(remote_base::RemoteReceiveData data) { diff --git a/tests/components/climate_ir_lg/climate/lg_ir_climate_tests.cpp b/tests/components/climate_ir_lg/climate/lg_ir_climate_tests.cpp new file mode 100644 index 00000000000..c8c8e79db65 --- /dev/null +++ b/tests/components/climate_ir_lg/climate/lg_ir_climate_tests.cpp @@ -0,0 +1,90 @@ +#include +#include "../common.h" + +namespace esphome::climate_ir_lg::testing { + +namespace { + +constexpr uint32_t BIT_ONE_LOW = 1600; +// A complete LG frame is header (2) + 28 bit pairs (56) + trailing mark (1) +constexpr size_t FRAME_SIZE = 2 + 28 * 2 + 1; + +// A climate with timings set, so transmit_() produces a frame decode_lg_frame() can read back +void configure(LgIrClimate &sut, MockRemoteTransmitter &transmitter) { + sut.set_transmitter(&transmitter); + sut.set_header_high(8000); + sut.set_header_low(4000); + sut.set_bit_high(600); + sut.set_bit_one_low(BIT_ONE_LOW); + sut.set_bit_zero_low(550); +} + +// COOL at 20 °C with the fan on HIGH: a distinctive state the swing tests must not leak into their frame +void turn_on(LgIrClimate &sut) { + climate::ClimateCall on_call(&sut); + on_call.set_mode(climate::CLIMATE_MODE_COOL); + on_call.set_target_temperature(20); + on_call.set_fan_mode(climate::CLIMATE_FAN_HIGH); + sut.control(on_call); +} + +uint32_t last_frame(const MockRemoteTransmitter &transmitter) { + // Guard against out-of-bounds reads in decode_lg_frame() if the transmission failed + if (transmitter.last_data.size() < FRAME_SIZE) { + ADD_FAILURE() << "no complete frame was transmitted"; + return 0; + } + return decode_lg_frame(transmitter.last_data, BIT_ONE_LOW); +} + +} // namespace + +// A mode command carries the mode, the fan speed and the temperature, with the checksum nibble +TEST(LgIrClimateTests, ModeCommandCarriesModeFanSpeedAndTemperature) { + LgIrClimate sut; + MockRemoteTransmitter transmitter; + configure(sut, transmitter); + + turn_on(sut); + + // From OFF: COMMAND_ON_COOL, FAN_MAX, 20 °C encoded as (20 - 15) << 8, checksum 9 + EXPECT_EQ(last_frame(transmitter), 0x8800549u); +} + +// The swing command is a distinct, self-contained IR code: the real remote transmits it +// on its own, independent of the unit's current mode, target temperature, or fan speed. +TEST(LgIrClimateTests, SwingCommandIsIndependentOfClimateState) { + LgIrClimate sut; + MockRemoteTransmitter transmitter; + configure(sut, transmitter); + turn_on(sut); + + // Toggle swing -- this must be the fixed swing code below, unaffected by the state set above. + climate::ClimateCall swing_call(&sut); + swing_call.set_swing_mode(climate::CLIMATE_SWING_VERTICAL); + sut.control(swing_call); + + // The swing command is always this exact fixed code, regardless of climate state. + EXPECT_EQ(last_frame(transmitter), 0x8810001u); +} + +// With advanced commands the swing frames are fixed codes too, one to start and one to stop the swing +TEST(LgIrClimateTests, AdvancedSwingCommandsAreIndependentOfClimateState) { + LgIrClimate sut; + MockRemoteTransmitter transmitter; + configure(sut, transmitter); + sut.set_advanced_commands_support(true); + turn_on(sut); + + climate::ClimateCall swing_on(&sut); + swing_on.set_swing_mode(climate::CLIMATE_SWING_VERTICAL); + sut.control(swing_on); + EXPECT_EQ(last_frame(transmitter), 0x8813149u); // HEADER_ADV_SWING | VERT_SWING_ON + + climate::ClimateCall swing_off(&sut); + swing_off.set_swing_mode(climate::CLIMATE_SWING_OFF); + sut.control(swing_off); + EXPECT_EQ(last_frame(transmitter), 0x881315Au); // HEADER_ADV_SWING | VERT_SWING_OFF +} + +} // namespace esphome::climate_ir_lg::testing diff --git a/tests/components/climate_ir_lg/common.h b/tests/components/climate_ir_lg/common.h new file mode 100644 index 00000000000..7e9a79cc615 --- /dev/null +++ b/tests/components/climate_ir_lg/common.h @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include "esphome/components/climate_ir_lg/climate_ir_lg.h" +#include "esphome/components/remote_base/remote_base.h" + +namespace esphome::climate_ir_lg::testing { + +// Records the raw mark/space timings instead of driving real IR hardware. +class MockRemoteTransmitter : public remote_base::RemoteTransmitterBase { + public: + MockRemoteTransmitter() : RemoteTransmitterBase(nullptr) {} + + std::vector last_data; + + protected: + void send_internal(uint32_t send_times, uint32_t send_wait) override { this->last_data = this->temp_.get_data(); } +}; + +// Decodes the raw mark/space timings produced by LgIrClimate::transmit_() back into the +// 28-bit value that was transmitted (header + bits, ignoring the trailing mark). +inline uint32_t decode_lg_frame(const std::vector &data, uint32_t bit_one_low) { + uint32_t value = 0; + // data[0]/data[1] are the header mark/space; each bit thereafter is a mark/space pair. + for (size_t i = 0; i < 28; i++) { + int32_t space = data[2 + 2 * i + 1]; + value = (value << 1) | (static_cast(-space) == bit_one_low ? 1 : 0); + } + return value; +} + +} // namespace esphome::climate_ir_lg::testing