From a257edba626ab1455f882f58a8a3f5787745a231 Mon Sep 17 00:00:00 2001 From: Boris Krivonog Date: Mon, 25 May 2026 23:46:33 +0200 Subject: [PATCH] [mitsubishi_cn105] Add basic swing support (#15653) Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../components/mitsubishi_cn105/climate.py | 12 +- .../mitsubishi_cn105/mitsubishi_cn105.h | 1 + .../mitsubishi_cn105_climate.cpp | 90 ++++++++++ .../mitsubishi_cn105_climate.h | 5 + .../mitsubishi_cn105_climate_tests.cpp | 165 ++++++++++++++++++ tests/components/mitsubishi_cn105/common.h | 11 ++ tests/components/mitsubishi_cn105/common.yaml | 3 + 7 files changed, 286 insertions(+), 1 deletion(-) create mode 100644 tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_climate_tests.cpp diff --git a/esphome/components/mitsubishi_cn105/climate.py b/esphome/components/mitsubishi_cn105/climate.py index cc44494d89..522b9218fc 100644 --- a/esphome/components/mitsubishi_cn105/climate.py +++ b/esphome/components/mitsubishi_cn105/climate.py @@ -1,8 +1,14 @@ from esphome import automation import esphome.codegen as cg from esphome.components import climate, uart +from esphome.components.climate import validate_climate_swing_mode import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_TEMPERATURE, CONF_UPDATE_INTERVAL +from esphome.const import ( + CONF_ID, + CONF_SUPPORTED_SWING_MODES, + CONF_TEMPERATURE, + CONF_UPDATE_INTERVAL, +) from esphome.core import ID from esphome.cpp_generator import MockObj from esphome.types import ConfigType, TemplateArgsType @@ -43,6 +49,9 @@ CONFIG_SCHEMA = ( cv.Optional( CONF_CURRENT_TEMPERATURE_MIN_INTERVAL, default="60s" ): cv.update_interval, + cv.Optional( + CONF_SUPPORTED_SWING_MODES, default="OFF" + ): validate_climate_swing_mode, } ) ) @@ -63,6 +72,7 @@ async def to_code(config: ConfigType) -> None: var = await climate.new_climate(config) await cg.register_component(var, config) await uart.register_uart_device(var, config) + cg.add(var.set_supported_swing_mode(config[CONF_SUPPORTED_SWING_MODES])) cg.add( var.set_current_temperature_min_interval( config[CONF_CURRENT_TEMPERATURE_MIN_INTERVAL] diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h index dbeb43068e..742d8e18a9 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include "esphome/components/uart/uart.h" #include "esphome/core/finite_set_mask.h" diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp index 67a561397a..afffe7ea5e 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp @@ -84,6 +84,8 @@ climate::ClimateTraits MitsubishiCN105Climate::traits() { traits.add_supported_fan_mode(p.second); } + traits.set_supported_swing_modes(this->supported_swing_modes_); + traits.set_visual_min_temperature(16.0f); traits.set_visual_max_temperature(31.0f); traits.set_visual_temperature_step(1.0f); @@ -114,6 +116,37 @@ void MitsubishiCN105Climate::control(const climate::ClimateCall &call) { this->hp_.set_fan_mode(*fan_mode); } + if (const auto swing_mode = call.get_swing_mode()) { + auto vane = this->last_non_swing_vane_mode_; + auto wide = this->last_non_swing_wide_vane_mode_; + + switch (*swing_mode) { + case climate::CLIMATE_SWING_BOTH: + vane = MitsubishiCN105::VaneMode::SWING; + wide = MitsubishiCN105::WideVaneMode::SWING; + break; + + case climate::CLIMATE_SWING_VERTICAL: + vane = MitsubishiCN105::VaneMode::SWING; + break; + + case climate::CLIMATE_SWING_HORIZONTAL: + wide = MitsubishiCN105::WideVaneMode::SWING; + break; + + case climate::CLIMATE_SWING_OFF: + default: + break; + } + + if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_VERTICAL)) { + this->hp_.set_vane_mode(vane); + } + if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_HORIZONTAL)) { + this->hp_.set_wide_vane_mode(wide); + } + } + if (this->hp_.is_status_initialized()) { this->apply_values_(); } @@ -143,7 +176,64 @@ void MitsubishiCN105Climate::apply_values_() { ESP_LOGD(TAG, "Unable to map fan mode"); } + if (!this->supported_swing_modes_.empty()) { + bool vertical_swinging = false; + bool horizontal_swinging = false; + + if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_VERTICAL)) { + if (status.vane_mode == MitsubishiCN105::VaneMode::SWING) { + vertical_swinging = true; + } else if (status.vane_mode != MitsubishiCN105::VaneMode::UNKNOWN) { + this->last_non_swing_vane_mode_ = status.vane_mode; + } + } + + if (this->supported_swing_modes_.count(climate::CLIMATE_SWING_HORIZONTAL)) { + if (status.wide_vane_mode == MitsubishiCN105::WideVaneMode::SWING) { + horizontal_swinging = true; + } else if (status.wide_vane_mode != MitsubishiCN105::WideVaneMode::UNKNOWN) { + this->last_non_swing_wide_vane_mode_ = status.wide_vane_mode; + } + } + + if (vertical_swinging && horizontal_swinging) { + this->swing_mode = climate::CLIMATE_SWING_BOTH; + } else if (vertical_swinging) { + this->swing_mode = climate::CLIMATE_SWING_VERTICAL; + } else if (horizontal_swinging) { + this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; + } else { + this->swing_mode = climate::CLIMATE_SWING_OFF; + } + } + this->publish_state(); } +void MitsubishiCN105Climate::set_supported_swing_mode(climate::ClimateSwingMode mode) { + this->supported_swing_modes_.clear(); + switch (mode) { + case climate::CLIMATE_SWING_VERTICAL: + this->supported_swing_modes_.insert(climate::CLIMATE_SWING_OFF); + this->supported_swing_modes_.insert(climate::CLIMATE_SWING_VERTICAL); + break; + + case climate::CLIMATE_SWING_HORIZONTAL: + this->supported_swing_modes_.insert(climate::CLIMATE_SWING_OFF); + this->supported_swing_modes_.insert(climate::CLIMATE_SWING_HORIZONTAL); + break; + + case climate::CLIMATE_SWING_BOTH: + this->supported_swing_modes_.insert(climate::CLIMATE_SWING_OFF); + this->supported_swing_modes_.insert(climate::CLIMATE_SWING_VERTICAL); + this->supported_swing_modes_.insert(climate::CLIMATE_SWING_HORIZONTAL); + this->supported_swing_modes_.insert(climate::CLIMATE_SWING_BOTH); + break; + + case climate::CLIMATE_SWING_OFF: + default: + break; + } +} + } // namespace esphome::mitsubishi_cn105 diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h index e09158bfcf..c83a5519c1 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h @@ -25,10 +25,15 @@ class MitsubishiCN105Climate : public climate::Climate, public Component, public void set_remote_temperature(float temperature) { this->hp_.set_remote_temperature(temperature); } void clear_remote_temperature() { this->hp_.clear_remote_temperature(); } + void set_supported_swing_mode(climate::ClimateSwingMode mode); + protected: void apply_values_(); MitsubishiCN105 hp_; + climate::ClimateSwingModeMask supported_swing_modes_{}; + MitsubishiCN105::VaneMode last_non_swing_vane_mode_{MitsubishiCN105::VaneMode::AUTO}; + MitsubishiCN105::WideVaneMode last_non_swing_wide_vane_mode_{MitsubishiCN105::WideVaneMode::CENTER}; }; template diff --git a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_climate_tests.cpp b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_climate_tests.cpp new file mode 100644 index 0000000000..36e0fc90b4 --- /dev/null +++ b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_climate_tests.cpp @@ -0,0 +1,165 @@ +#include "../common.h" + +namespace esphome::mitsubishi_cn105::testing { + +TEST(MitsubishiCN105ClimateTests, SupportedSwingModeOffLeavesTraitsEmpty) { + TestableMitsubishiCN105Climate sut; + + sut.set_supported_swing_mode(climate::CLIMATE_SWING_OFF); + + EXPECT_FALSE(sut.traits().get_supports_swing_modes()); +} + +TEST(MitsubishiCN105ClimateTests, SupportedSwingModeVerticalExposesOffAndVertical) { + TestableMitsubishiCN105Climate sut; + + sut.set_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); + + EXPECT_TRUE(sut.traits().supports_swing_mode(climate::CLIMATE_SWING_OFF)); + EXPECT_TRUE(sut.traits().supports_swing_mode(climate::CLIMATE_SWING_VERTICAL)); + EXPECT_FALSE(sut.traits().supports_swing_mode(climate::CLIMATE_SWING_HORIZONTAL)); + EXPECT_FALSE(sut.traits().supports_swing_mode(climate::CLIMATE_SWING_BOTH)); +} + +TEST(MitsubishiCN105ClimateTests, SupportedSwingModeHorizontalExposesOffAndHorizontal) { + TestableMitsubishiCN105Climate sut; + + sut.set_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL); + + EXPECT_TRUE(sut.traits().supports_swing_mode(climate::CLIMATE_SWING_OFF)); + EXPECT_FALSE(sut.traits().supports_swing_mode(climate::CLIMATE_SWING_VERTICAL)); + EXPECT_TRUE(sut.traits().supports_swing_mode(climate::CLIMATE_SWING_HORIZONTAL)); + EXPECT_FALSE(sut.traits().supports_swing_mode(climate::CLIMATE_SWING_BOTH)); +} + +TEST(MitsubishiCN105ClimateTests, SupportedSwingModeBothExposesAllExpectedModes) { + TestableMitsubishiCN105Climate sut; + + sut.set_supported_swing_mode(climate::CLIMATE_SWING_BOTH); + + EXPECT_TRUE(sut.traits().supports_swing_mode(climate::CLIMATE_SWING_OFF)); + EXPECT_TRUE(sut.traits().supports_swing_mode(climate::CLIMATE_SWING_VERTICAL)); + EXPECT_TRUE(sut.traits().supports_swing_mode(climate::CLIMATE_SWING_HORIZONTAL)); + EXPECT_TRUE(sut.traits().supports_swing_mode(climate::CLIMATE_SWING_BOTH)); +} + +TEST(MitsubishiCN105ClimateTests, ApplyValuesMapsVerticalSwingWhenSupported) { + TestableMitsubishiCN105Climate sut; + + sut.set_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); + + sut.status().vane_mode = MitsubishiCN105::VaneMode::SWING; + sut.status().wide_vane_mode = MitsubishiCN105::WideVaneMode::CENTER; + + sut.apply_values_(); + + EXPECT_EQ(sut.swing_mode, climate::CLIMATE_SWING_VERTICAL); +} + +TEST(MitsubishiCN105ClimateTests, ApplyValuesMapsHorizontalSwingWhenSupported) { + TestableMitsubishiCN105Climate sut; + + sut.set_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL); + + sut.status().vane_mode = MitsubishiCN105::VaneMode::AUTO; + sut.status().wide_vane_mode = MitsubishiCN105::WideVaneMode::SWING; + + sut.apply_values_(); + + EXPECT_EQ(sut.swing_mode, climate::CLIMATE_SWING_HORIZONTAL); +} + +TEST(MitsubishiCN105ClimateTests, ApplyValuesMapsBothSwingWhenSupported) { + TestableMitsubishiCN105Climate sut; + + sut.set_supported_swing_mode(climate::CLIMATE_SWING_BOTH); + + sut.status().vane_mode = MitsubishiCN105::VaneMode::SWING; + sut.status().wide_vane_mode = MitsubishiCN105::WideVaneMode::SWING; + + sut.apply_values_(); + + EXPECT_EQ(sut.swing_mode, climate::CLIMATE_SWING_BOTH); +} + +TEST(MitsubishiCN105ClimateTests, ApplyValuesMapsSwingOffWhenNoSwingActive) { + TestableMitsubishiCN105Climate sut; + + sut.set_supported_swing_mode(climate::CLIMATE_SWING_BOTH); + + sut.status().vane_mode = MitsubishiCN105::VaneMode::POSITION_3; + sut.status().wide_vane_mode = MitsubishiCN105::WideVaneMode::CENTER; + + sut.apply_values_(); + + EXPECT_EQ(sut.swing_mode, climate::CLIMATE_SWING_OFF); +} + +TEST(MitsubishiCN105ClimateTests, ApplyValuesRemembersLastNonSwingPositions) { + TestableMitsubishiCN105Climate sut; + + sut.set_supported_swing_mode(climate::CLIMATE_SWING_BOTH); + + sut.status().vane_mode = MitsubishiCN105::VaneMode::POSITION_4; + sut.status().wide_vane_mode = MitsubishiCN105::WideVaneMode::RIGHT; + + sut.apply_values_(); + + EXPECT_EQ(sut.last_non_swing_vane_mode_, MitsubishiCN105::VaneMode::POSITION_4); + EXPECT_EQ(sut.last_non_swing_wide_vane_mode_, MitsubishiCN105::WideVaneMode::RIGHT); + + sut.status().vane_mode = MitsubishiCN105::VaneMode::SWING; + sut.status().wide_vane_mode = MitsubishiCN105::WideVaneMode::SWING; + + sut.apply_values_(); + + EXPECT_EQ(sut.last_non_swing_vane_mode_, MitsubishiCN105::VaneMode::POSITION_4); + EXPECT_EQ(sut.last_non_swing_wide_vane_mode_, MitsubishiCN105::WideVaneMode::RIGHT); + EXPECT_EQ(sut.swing_mode, climate::CLIMATE_SWING_BOTH); +} + +TEST(MitsubishiCN105ClimateTests, ApplyValuesDoesNotOverwriteRememberedPositionWithUnknownValues) { + TestableMitsubishiCN105Climate sut; + + sut.set_supported_swing_mode(climate::CLIMATE_SWING_BOTH); + + sut.last_non_swing_vane_mode_ = MitsubishiCN105::VaneMode::POSITION_2; + sut.last_non_swing_wide_vane_mode_ = MitsubishiCN105::WideVaneMode::LEFT; + + sut.status().vane_mode = MitsubishiCN105::VaneMode::UNKNOWN; + sut.status().wide_vane_mode = MitsubishiCN105::WideVaneMode::UNKNOWN; + + sut.apply_values_(); + + EXPECT_EQ(sut.last_non_swing_vane_mode_, MitsubishiCN105::VaneMode::POSITION_2); + EXPECT_EQ(sut.last_non_swing_wide_vane_mode_, MitsubishiCN105::WideVaneMode::LEFT); + EXPECT_EQ(sut.swing_mode, climate::CLIMATE_SWING_OFF); +} + +TEST(MitsubishiCN105ClimateTests, ApplyValuesIgnoresUnsupportedVerticalSwingState) { + TestableMitsubishiCN105Climate sut; + + sut.set_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL); + + sut.status().vane_mode = MitsubishiCN105::VaneMode::SWING; + sut.status().wide_vane_mode = MitsubishiCN105::WideVaneMode::CENTER; + + sut.apply_values_(); + + EXPECT_EQ(sut.swing_mode, climate::CLIMATE_SWING_OFF); +} + +TEST(MitsubishiCN105ClimateTests, ApplyValuesIgnoresUnsupportedHorizontalSwingState) { + TestableMitsubishiCN105Climate sut; + + sut.set_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL); + + sut.status().vane_mode = MitsubishiCN105::VaneMode::AUTO; + sut.status().wide_vane_mode = MitsubishiCN105::WideVaneMode::SWING; + + sut.apply_values_(); + + EXPECT_EQ(sut.swing_mode, climate::CLIMATE_SWING_OFF); +} + +} // namespace esphome::mitsubishi_cn105::testing diff --git a/tests/components/mitsubishi_cn105/common.h b/tests/components/mitsubishi_cn105/common.h index 59b6203732..798f7283f6 100644 --- a/tests/components/mitsubishi_cn105/common.h +++ b/tests/components/mitsubishi_cn105/common.h @@ -8,6 +8,7 @@ #include #include "esphome/components/uart/uart_component.h" #include "esphome/components/mitsubishi_cn105/mitsubishi_cn105.h" +#include "esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h" namespace esphome::mitsubishi_cn105::testing { @@ -44,6 +45,7 @@ class TestableMitsubishiCN105 : public MitsubishiCN105 { using MitsubishiCN105::State; using MitsubishiCN105::UpdateFlag; using MitsubishiCN105::state_; + using MitsubishiCN105::status_; using MitsubishiCN105::operation_start_ms_; using MitsubishiCN105::use_temperature_encoding_b_; using MitsubishiCN105::set_wide_vane_high_bit_; @@ -58,4 +60,13 @@ class TestableMitsubishiCN105 : public MitsubishiCN105 { void set_current_time(uint32_t ms) { test_loop_time_ms = ms; } }; +class TestableMitsubishiCN105Climate : public MitsubishiCN105Climate { + public: + using MitsubishiCN105Climate::apply_values_; + using MitsubishiCN105Climate::last_non_swing_vane_mode_; + using MitsubishiCN105Climate::last_non_swing_wide_vane_mode_; + + MitsubishiCN105::Status &status() { return static_cast(this->hp_).status_; } +}; + } // namespace esphome::mitsubishi_cn105::testing diff --git a/tests/components/mitsubishi_cn105/common.yaml b/tests/components/mitsubishi_cn105/common.yaml index 4b64f51261..5b9c3aaaf6 100644 --- a/tests/components/mitsubishi_cn105/common.yaml +++ b/tests/components/mitsubishi_cn105/common.yaml @@ -3,6 +3,9 @@ climate: id: ac name: "AC Test" uart_id: uart_bus + update_interval: 30s + current_temperature_min_interval: 120s + supported_swing_modes: BOTH esphome: on_boot: