[mitsubishi_cn105] Add vertical vane direction select (#16723)

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Boris Krivonog
2026-08-10 17:58:37 -05:00
committed by GitHub
co-authored by J. Nick Koston J. Nick Koston
parent 82a63658f9
commit c8d2c3691a
8 changed files with 261 additions and 33 deletions
@@ -133,9 +133,7 @@ void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {
}
}
if (this->parent_->is_status_initialized()) {
this->apply_values_();
}
this->parent_->publish_status();
}
void MitsubishiCN105Climate::apply_values_() {
@@ -38,6 +38,12 @@ class MitsubishiCN105Component : public Component, public uart::UARTDevice {
this->status_callback_.add(std::forward<F>(callback));
}
void publish_status() {
if (this->is_status_initialized()) {
this->status_callback_.call();
}
}
protected:
MitsubishiCN105 hp_;
CallbackManager<void()> status_callback_;
@@ -0,0 +1,47 @@
import esphome.codegen as cg
from esphome.components import select
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.types import ConfigType
from .. import (
MITSUBISHI_CN105_DEVICE_SCHEMA,
MitsubishiCN105Component,
mitsubishi_ns,
register_mitsubishi_cn105_device,
)
DEPENDENCIES = ["mitsubishi_cn105"]
CONF_VERTICAL_VANE_DIRECTION = "vertical_vane_direction"
# The insertion order must match VALUES in mitsubishi_cn105_vane_select_vertical.cpp.
VERTICAL_VANE_DIRECTIONS = ["Auto", "1", "2", "3", "4", "5", "Swing"]
MitsubishiCN105VerticalVaneDirectionSelect = mitsubishi_ns.class_(
"MitsubishiCN105VerticalVaneDirectionSelect",
select.Select,
cg.Component,
cg.Parented.template(MitsubishiCN105Component),
)
CONFIG_SCHEMA = cv.Schema(
{
cv.Optional(CONF_VERTICAL_VANE_DIRECTION): select.select_schema(
MitsubishiCN105VerticalVaneDirectionSelect,
icon="mdi:arrow-up-down",
),
}
).extend(MITSUBISHI_CN105_DEVICE_SCHEMA)
async def to_code(config: ConfigType) -> None:
if vertical_vane_direction := config.get(CONF_VERTICAL_VANE_DIRECTION):
var = cg.new_Pvariable(vertical_vane_direction[CONF_ID])
await cg.register_component(var, vertical_vane_direction)
await select.register_select(
var,
vertical_vane_direction,
options=VERTICAL_VANE_DIRECTIONS,
)
await register_mitsubishi_cn105_device(var, config)
@@ -0,0 +1,39 @@
#include "mitsubishi_cn105_vane_select_vertical.h"
#include <array>
namespace esphome::mitsubishi_cn105 {
// NOTE: This order must match VERTICAL_VANE_DIRECTIONS in select.py.
// MitsubishiCN105VerticalVaneDirectionSelect uses the preferred index-based
// Select API, so Python option order and this array must stay aligned.
static constexpr std::array VALUES{
MitsubishiCN105::VaneMode::AUTO, MitsubishiCN105::VaneMode::POSITION_1, MitsubishiCN105::VaneMode::POSITION_2,
MitsubishiCN105::VaneMode::POSITION_3, MitsubishiCN105::VaneMode::POSITION_4, MitsubishiCN105::VaneMode::POSITION_5,
MitsubishiCN105::VaneMode::SWING,
};
void MitsubishiCN105VerticalVaneDirectionSelect::setup() {
this->parent_->add_on_status_callback([this]() { this->publish_vane_state(this->parent_->status().vane_mode); });
if (this->parent_->is_status_initialized()) {
this->publish_vane_state(this->parent_->status().vane_mode);
}
}
void MitsubishiCN105VerticalVaneDirectionSelect::control(size_t index) {
if (index < VALUES.size()) {
this->parent_->set_vane_mode(VALUES[index]);
this->parent_->publish_status();
}
}
void MitsubishiCN105VerticalVaneDirectionSelect::publish_vane_state(MitsubishiCN105::VaneMode mode) {
for (size_t i = 0; i < VALUES.size(); ++i) {
if (VALUES[i] == mode) {
this->publish_state(i);
return;
}
}
}
} // namespace esphome::mitsubishi_cn105
@@ -0,0 +1,21 @@
#pragma once
#include "../mitsubishi_cn105_component.h"
#include "esphome/components/select/select.h"
#include "esphome/core/component.h"
namespace esphome::mitsubishi_cn105 {
class MitsubishiCN105VerticalVaneDirectionSelect : public select::Select,
public Component,
public Parented<MitsubishiCN105Component> {
public:
void setup() override;
void publish_vane_state(MitsubishiCN105::VaneMode mode);
protected:
void control(size_t index) override;
};
} // namespace esphome::mitsubishi_cn105
@@ -2,16 +2,16 @@
namespace esphome::mitsubishi_cn105::testing {
struct TestContext {
struct MitsubishiCN105TestsContext {
MockUARTComponent uart;
uart::UARTDevice device{&uart};
TestableMitsubishiCN105 sut{device};
TestContext() { this->sut.set_current_time(0); }
MitsubishiCN105TestsContext() { this->sut.set_current_time(0); }
};
TEST(MitsubishiCN105Tests, InitSendsConnectPacket) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.set_current_time(123);
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::NOT_CONNECTED);
@@ -26,7 +26,7 @@ TEST(MitsubishiCN105Tests, InitSendsConnectPacket) {
}
TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.initialize();
ctx.uart.tx.clear(); // Remove first connect packet bytes
@@ -106,7 +106,7 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) {
}
TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.initialize();
ctx.uart.tx.clear(); // Remove first connect packet bytes
@@ -133,7 +133,7 @@ TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) {
}
TEST(MitsubishiCN105Tests, RxWatchdogLimitsProcessingPerUpdate) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.initialize();
ctx.uart.tx.clear(); // Remove first connect packet bytes
@@ -164,7 +164,7 @@ TEST(MitsubishiCN105Tests, RxWatchdogLimitsProcessingPerUpdate) {
}
TEST(MitsubishiCN105Tests, ParserHandlesMixedRxStream) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.initialize();
ctx.uart.tx.clear(); // Remove first connect packet bytes
@@ -228,7 +228,7 @@ TEST(MitsubishiCN105Tests, ParserHandlesMixedRxStream) {
}
TEST(MitsubishiCN105Tests, NextStatusUpdateAfterUpdateIntervalMilliseconds) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.set_update_interval(2000);
ctx.sut.set_current_time(80000);
@@ -258,7 +258,7 @@ TEST(MitsubishiCN105Tests, NextStatusUpdateAfterUpdateIntervalMilliseconds) {
}
TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedA) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.uart.push_rx(
{0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x01, 0x03, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55});
@@ -273,7 +273,7 @@ TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedA) {
}
TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedB) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.uart.push_rx(
{0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xAD});
@@ -288,7 +288,7 @@ TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedB) {
}
TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedA) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x07, 0x03, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5D});
@@ -298,7 +298,7 @@ TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedA) {
}
TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedB) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x07, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBC, 0xA7});
@@ -308,7 +308,7 @@ TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedB) {
}
TEST(MitsubishiCN105Tests, DecodeWideVanePackageHighBitNotSet) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x58});
@@ -320,7 +320,7 @@ TEST(MitsubishiCN105Tests, DecodeWideVanePackageHighBitNotSet) {
}
TEST(MitsubishiCN105Tests, DecodeWideVanePackageHighBitSet) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x83, 0x00, 0x00, 0x00, 0x00, 0x00, 0xD8});
@@ -332,7 +332,7 @@ TEST(MitsubishiCN105Tests, DecodeWideVanePackageHighBitSet) {
}
TEST(MitsubishiCN105Tests, ApplySettingsPowerOn) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.set_power(true);
ctx.sut.apply_settings();
@@ -342,7 +342,7 @@ TEST(MitsubishiCN105Tests, ApplySettingsPowerOn) {
}
TEST(MitsubishiCN105Tests, ApplySettingsTemperatureEncodedA) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.set_target_temperature(23.0f);
ctx.sut.apply_settings();
@@ -352,7 +352,7 @@ TEST(MitsubishiCN105Tests, ApplySettingsTemperatureEncodedA) {
}
TEST(MitsubishiCN105Tests, ApplySettingsTemperatureEncodedB) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.use_temperature_encoding_b_ = true;
ctx.sut.set_target_temperature(26.0f);
@@ -363,7 +363,7 @@ TEST(MitsubishiCN105Tests, ApplySettingsTemperatureEncodedB) {
}
TEST(MitsubishiCN105Tests, ApplySettingsHalfDegreeTemperatureEncodedB) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.use_temperature_encoding_b_ = true;
ctx.sut.set_target_temperature(26.5f);
@@ -374,7 +374,7 @@ TEST(MitsubishiCN105Tests, ApplySettingsHalfDegreeTemperatureEncodedB) {
}
TEST(MitsubishiCN105Tests, ApplyModeCool) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.set_mode(MitsubishiCN105::Mode::COOL);
ctx.sut.apply_settings();
@@ -384,7 +384,7 @@ TEST(MitsubishiCN105Tests, ApplyModeCool) {
}
TEST(MitsubishiCN105Tests, ApplyFanModeSpeed1) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.set_fan_mode(MitsubishiCN105::FanMode::SPEED_1);
ctx.sut.apply_settings();
@@ -394,7 +394,7 @@ TEST(MitsubishiCN105Tests, ApplyFanModeSpeed1) {
}
TEST(MitsubishiCN105Tests, ApplyVaneModeSwing) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.set_vane_mode(MitsubishiCN105::VaneMode::SWING);
ctx.sut.apply_settings();
@@ -404,7 +404,7 @@ TEST(MitsubishiCN105Tests, ApplyVaneModeSwing) {
}
TEST(MitsubishiCN105Tests, ApplyWideVaneModeLeftAndHighBitNotSet) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.set_wide_vane_mode(MitsubishiCN105::WideVaneMode::LEFT);
ctx.sut.apply_settings();
@@ -414,7 +414,7 @@ TEST(MitsubishiCN105Tests, ApplyWideVaneModeLeftAndHighBitNotSet) {
}
TEST(MitsubishiCN105Tests, ApplyWideVaneModeLeftAndHighBitSet) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.set_wide_vane_high_bit_ = true;
ctx.sut.set_wide_vane_mode(MitsubishiCN105::WideVaneMode::LEFT);
@@ -425,7 +425,7 @@ TEST(MitsubishiCN105Tests, ApplyWideVaneModeLeftAndHighBitSet) {
}
TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.set_update_interval(2000);
ctx.sut.set_current_time(5000);
@@ -470,7 +470,7 @@ TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) {
}
TEST(MitsubishiCN105Tests, SetAndClearRemoteRoomTemp) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
// Set remote temperature
ctx.sut.set_remote_temperature(28.5f);
@@ -505,7 +505,7 @@ TEST(MitsubishiCN105Tests, SetAndClearRemoteRoomTemp) {
}
TEST(MitsubishiCN105Tests, ApplyQueuedSettingsThenRemoteRoomTempInSecondWrite) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
// Queue normal settings plus remote temperature together.
ctx.sut.use_temperature_encoding_b_ = true;
@@ -545,7 +545,7 @@ TEST(MitsubishiCN105Tests, ApplyQueuedSettingsThenRemoteRoomTempInSecondWrite) {
}
TEST(MitsubishiCN105Tests, WriteTimeoutClearsStatusUpdateWaitCreditOnReconnect) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.set_update_interval(2000);
ctx.sut.set_current_time(5000);
@@ -578,7 +578,7 @@ TEST(MitsubishiCN105Tests, WriteTimeoutClearsStatusUpdateWaitCreditOnReconnect)
}
TEST(MitsubishiCN105Tests, SetOutOfRangeRemoteRoomTempIsIgnored) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.set_remote_temperature(7.0f);
EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE));
@@ -591,13 +591,13 @@ TEST(MitsubishiCN105Tests, SetOutOfRangeRemoteRoomTempIsIgnored) {
}
TEST(MitsubishiCN105Tests, SetMinRemoteRoomTemp) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.set_remote_temperature(8.0f);
EXPECT_TRUE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE));
}
TEST(MitsubishiCN105Tests, SetMaxRemoteRoomTemp) {
auto ctx = TestContext{};
MitsubishiCN105TestsContext ctx;
ctx.sut.set_remote_temperature(39.5f);
EXPECT_TRUE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE));
}
@@ -10,6 +10,12 @@ climate:
name: "AC Test"
supported_swing_modes: BOTH
select:
- platform: mitsubishi_cn105
mitsubishi_cn105_id: ac
vertical_vane_direction:
name: "Vertical Vane"
esphome:
on_boot:
then:
@@ -0,0 +1,111 @@
#include "../common.h"
#include "esphome/components/mitsubishi_cn105/select/mitsubishi_cn105_vane_select_vertical.h"
namespace esphome::mitsubishi_cn105::testing {
class TestableMitsubishiCN105Component : public MitsubishiCN105Component {
public:
MitsubishiCN105::Status &mutable_status() { return const_cast<MitsubishiCN105::Status &>(this->status()); }
void notify_status() { this->status_callback_.call(); }
};
class TestableMitsubishiCN105VerticalVaneDirectionSelect : public MitsubishiCN105VerticalVaneDirectionSelect {
public:
using MitsubishiCN105VerticalVaneDirectionSelect::control;
};
struct VerticalVaneDirectionSelectTestContext {
TestableMitsubishiCN105Component hub;
TestableMitsubishiCN105VerticalVaneDirectionSelect select;
VerticalVaneDirectionSelectTestContext() {
this->select.traits.set_options({"Auto", "1", "2", "3", "4", "5", "Swing"});
this->select.set_parent(&this->hub);
this->select.setup();
}
};
TEST(MitsubishiCN105VerticalVaneDirectionSelectTests, MapsIndexesToVaneModes) {
VerticalVaneDirectionSelectTestContext ctx;
constexpr std::array expected_modes{
MitsubishiCN105::VaneMode::AUTO, MitsubishiCN105::VaneMode::POSITION_1,
MitsubishiCN105::VaneMode::POSITION_2, MitsubishiCN105::VaneMode::POSITION_3,
MitsubishiCN105::VaneMode::POSITION_4, MitsubishiCN105::VaneMode::POSITION_5,
MitsubishiCN105::VaneMode::SWING,
};
for (size_t i = 0; i < expected_modes.size(); ++i) {
SCOPED_TRACE(i);
ctx.select.control(i);
EXPECT_EQ(ctx.hub.status().vane_mode, expected_modes[i]);
}
}
TEST(MitsubishiCN105VerticalVaneDirectionSelectTests, PublishesIncomingVaneModes) {
VerticalVaneDirectionSelectTestContext ctx;
constexpr std::array modes{
MitsubishiCN105::VaneMode::AUTO, MitsubishiCN105::VaneMode::POSITION_1,
MitsubishiCN105::VaneMode::POSITION_2, MitsubishiCN105::VaneMode::POSITION_3,
MitsubishiCN105::VaneMode::POSITION_4, MitsubishiCN105::VaneMode::POSITION_5,
MitsubishiCN105::VaneMode::SWING,
};
for (size_t i = 0; i < modes.size(); ++i) {
SCOPED_TRACE(i);
ctx.hub.mutable_status().vane_mode = modes[i];
ctx.hub.notify_status();
EXPECT_EQ(ctx.select.active_index(), std::optional{i});
}
ctx.hub.mutable_status().vane_mode = MitsubishiCN105::VaneMode::UNKNOWN;
ctx.hub.notify_status();
EXPECT_EQ(ctx.select.active_index(), std::optional{modes.size() - 1});
}
TEST(MitsubishiCN105VerticalVaneDirectionSelectTests, ControlPublishesSelectAndClimateThroughHub) {
VerticalVaneDirectionSelectTestContext ctx;
MitsubishiCN105Climate climate_entity;
climate_entity.set_parent(&ctx.hub);
climate_entity.set_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL);
ctx.hub.mutable_status().room_temperature = 20.0f;
climate_entity.setup();
ctx.select.control(6);
EXPECT_EQ(ctx.select.active_index(), std::optional<size_t>{6});
EXPECT_EQ(climate_entity.swing_mode, climate::CLIMATE_SWING_VERTICAL);
ctx.select.control(3);
EXPECT_EQ(ctx.select.active_index(), std::optional<size_t>{3});
EXPECT_EQ(climate_entity.swing_mode, climate::CLIMATE_SWING_OFF);
}
TEST(MitsubishiCN105VerticalVaneDirectionSelectTests, ClimateControlPublishesSelectThroughHub) {
VerticalVaneDirectionSelectTestContext ctx;
MitsubishiCN105Climate climate_entity;
climate_entity.set_parent(&ctx.hub);
climate_entity.set_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL);
ctx.hub.mutable_status().room_temperature = 20.0f;
climate_entity.setup();
climate_entity.make_call().set_swing_mode(climate::CLIMATE_SWING_VERTICAL).perform();
EXPECT_EQ(ctx.select.active_index(), std::optional<size_t>{6});
climate_entity.make_call().set_swing_mode(climate::CLIMATE_SWING_OFF).perform();
EXPECT_EQ(ctx.select.active_index(), std::optional<size_t>{0});
}
TEST(MitsubishiCN105VerticalVaneDirectionSelectTests, BeforeInitializationDoesNotPublishSelectState) {
VerticalVaneDirectionSelectTestContext ctx;
ctx.select.control(3);
EXPECT_EQ(ctx.hub.status().vane_mode, MitsubishiCN105::VaneMode::POSITION_3);
EXPECT_FALSE(ctx.select.has_state());
}
} // namespace esphome::mitsubishi_cn105::testing