[mitsubishi_cn105] Add vane and wide-vane support (#16405)

This commit is contained in:
Boris Krivonog
2026-05-13 20:25:32 +02:00
committed by GitHub
parent 1c6966b761
commit ce8810bc42
5 changed files with 178 additions and 17 deletions
@@ -50,6 +50,11 @@ template<auto Unknown, size_t N> struct LookupMap {
}
return false;
}
constexpr bool is_valid(value_type value) const {
uint8_t raw;
return reverse_lookup(value, raw);
}
};
template<auto Unknown, class T, std::size_t N> static constexpr auto make_map(const T (&values)[N]) {
@@ -78,6 +83,33 @@ static constexpr auto PROTOCOL_FAN_MODE_MAP = make_map<MitsubishiCN105::FanMode:
MitsubishiCN105::FanMode::SPEED_4 // 0x06
});
static constexpr auto PROTOCOL_VANE_MODE_MAP = make_map<MitsubishiCN105::VaneMode::UNKNOWN>({
MitsubishiCN105::VaneMode::AUTO, // 0x00
MitsubishiCN105::VaneMode::POSITION_1, // 0x01
MitsubishiCN105::VaneMode::POSITION_2, // 0x02
MitsubishiCN105::VaneMode::POSITION_3, // 0x03
MitsubishiCN105::VaneMode::POSITION_4, // 0x04
MitsubishiCN105::VaneMode::POSITION_5, // 0x05
MitsubishiCN105::VaneMode::UNKNOWN, // 0x06
MitsubishiCN105::VaneMode::SWING // 0x07
});
static constexpr auto PROTOCOL_WIDE_VANE_MODE_MAP = make_map<MitsubishiCN105::WideVaneMode::UNKNOWN>({
MitsubishiCN105::WideVaneMode::UNKNOWN, // 0x00
MitsubishiCN105::WideVaneMode::FAR_LEFT, // 0x01
MitsubishiCN105::WideVaneMode::LEFT, // 0x02
MitsubishiCN105::WideVaneMode::CENTER, // 0x03
MitsubishiCN105::WideVaneMode::RIGHT, // 0x04
MitsubishiCN105::WideVaneMode::FAR_RIGHT, // 0x05
MitsubishiCN105::WideVaneMode::UNKNOWN, // 0x06
MitsubishiCN105::WideVaneMode::UNKNOWN, // 0x07
MitsubishiCN105::WideVaneMode::LEFT_RIGHT, // 0x08
MitsubishiCN105::WideVaneMode::UNKNOWN, // 0x09
MitsubishiCN105::WideVaneMode::UNKNOWN, // 0x0A
MitsubishiCN105::WideVaneMode::UNKNOWN, // 0x0B
MitsubishiCN105::WideVaneMode::SWING // 0x0C
});
static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) {
return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0}));
}
@@ -91,7 +123,7 @@ static constexpr auto make_packet(uint8_t type, const std::array<uint8_t, Payloa
return packet;
}
static float decode_temperature(int temp_a, int temp_b, int delta) {
static constexpr float decode_temperature(int temp_a, int temp_b, int delta) {
return temp_b != 0 ? (temp_b - 128) / 2.0f : delta + temp_a;
}
@@ -290,9 +322,10 @@ bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len)
this->set_state_(State::STATUS_UPDATED);
}
bool changed = previous.power_on != this->status_.power_on || previous.mode != this->status_.mode ||
previous.fan_mode != this->status_.fan_mode ||
previous.target_temperature != this->status_.target_temperature;
bool changed =
previous.power_on != this->status_.power_on || previous.mode != this->status_.mode ||
previous.fan_mode != this->status_.fan_mode || previous.target_temperature != this->status_.target_temperature ||
previous.vane_mode != this->status_.vane_mode || previous.wide_vane_mode != this->status_.wide_vane_mode;
if (this->is_room_temperature_enabled()) {
changed |= previous.room_temperature != this->status_.room_temperature;
@@ -339,6 +372,15 @@ bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len)
this->status_.fan_mode = PROTOCOL_FAN_MODE_MAP.lookup(payload[5]);
}
if (!this->pending_updates_.contains(UpdateFlag::VANE)) {
this->status_.vane_mode = PROTOCOL_VANE_MODE_MAP.lookup(payload[6]);
}
this->set_wide_vane_high_bit_ = (payload[9] & 0xF0) == 0x80;
if (!this->pending_updates_.contains(UpdateFlag::WIDE_VANE)) {
this->status_.wide_vane_mode = PROTOCOL_WIDE_VANE_MODE_MAP.lookup(payload[9] & 0x0F);
}
return true;
}
@@ -390,8 +432,7 @@ void MitsubishiCN105::set_target_temperature(float target_temperature) {
}
void MitsubishiCN105::set_mode(Mode mode) {
uint8_t placeholder;
if (!PROTOCOL_MODE_MAP.reverse_lookup(mode, placeholder)) {
if (!PROTOCOL_MODE_MAP.is_valid(mode)) {
ESP_LOGD(TAG, "Setting invalid mode: %u", static_cast<uint8_t>(mode));
return;
}
@@ -400,8 +441,7 @@ void MitsubishiCN105::set_mode(Mode mode) {
}
void MitsubishiCN105::set_fan_mode(FanMode fan_mode) {
uint8_t placeholder;
if (!PROTOCOL_FAN_MODE_MAP.reverse_lookup(fan_mode, placeholder)) {
if (!PROTOCOL_FAN_MODE_MAP.is_valid(fan_mode)) {
ESP_LOGD(TAG, "Setting invalid fan mode: %u", static_cast<uint8_t>(fan_mode));
return;
}
@@ -409,6 +449,24 @@ void MitsubishiCN105::set_fan_mode(FanMode fan_mode) {
this->pending_updates_.set(UpdateFlag::FAN);
}
void MitsubishiCN105::set_vane_mode(VaneMode vane_mode) {
if (!PROTOCOL_VANE_MODE_MAP.is_valid(vane_mode)) {
ESP_LOGD(TAG, "Setting invalid vane mode: %u", static_cast<uint8_t>(vane_mode));
return;
}
this->status_.vane_mode = vane_mode;
this->pending_updates_.set(UpdateFlag::VANE);
}
void MitsubishiCN105::set_wide_vane_mode(WideVaneMode wide_vane_mode) {
if (!PROTOCOL_WIDE_VANE_MODE_MAP.is_valid(wide_vane_mode)) {
ESP_LOGD(TAG, "Setting invalid wide vane mode: %u", static_cast<uint8_t>(wide_vane_mode));
return;
}
this->status_.wide_vane_mode = wide_vane_mode;
this->pending_updates_.set(UpdateFlag::WIDE_VANE);
}
void MitsubishiCN105::apply_settings_() {
std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload{};
@@ -450,7 +508,21 @@ void MitsubishiCN105::apply_settings_() {
payload[1] |= 0x08;
}
this->pending_updates_.clear(UpdateFlag::POWER, UpdateFlag::TEMPERATURE, UpdateFlag::MODE, UpdateFlag::FAN);
if (this->pending_updates_.contains(UpdateFlag::VANE) &&
PROTOCOL_VANE_MODE_MAP.reverse_lookup(this->status_.vane_mode, payload[7])) {
payload[1] |= 0x10;
}
if (this->pending_updates_.contains(UpdateFlag::WIDE_VANE) &&
PROTOCOL_WIDE_VANE_MODE_MAP.reverse_lookup(this->status_.wide_vane_mode, payload[13])) {
payload[2] |= 0x01;
if (this->set_wide_vane_high_bit_) {
payload[13] |= 0x80;
}
}
this->pending_updates_.clear(UpdateFlag::POWER, UpdateFlag::TEMPERATURE, UpdateFlag::MODE, UpdateFlag::FAN,
UpdateFlag::VANE, UpdateFlag::WIDE_VANE);
}
this->send_packet_(make_packet(PACKET_TYPE_WRITE_SETTINGS_REQUEST, payload));
@@ -29,12 +29,36 @@ class MitsubishiCN105 {
UNKNOWN,
};
enum class VaneMode : uint8_t {
AUTO,
POSITION_1,
POSITION_2,
POSITION_3,
POSITION_4,
POSITION_5,
SWING,
UNKNOWN,
};
enum class WideVaneMode : uint8_t {
FAR_LEFT,
LEFT,
CENTER,
RIGHT,
FAR_RIGHT,
LEFT_RIGHT,
SWING,
UNKNOWN,
};
struct Status {
bool power_on{false};
float target_temperature{NAN};
float room_temperature{NAN};
bool power_on{false};
Mode mode{Mode::UNKNOWN};
FanMode fan_mode{FanMode::UNKNOWN};
float room_temperature{NAN};
VaneMode vane_mode{VaneMode::UNKNOWN};
WideVaneMode wide_vane_mode{WideVaneMode::UNKNOWN};
};
explicit MitsubishiCN105(uart::UARTDevice &device) : device_(device) {}
@@ -61,6 +85,8 @@ class MitsubishiCN105 {
void set_target_temperature(float target_temperature);
void set_mode(Mode mode);
void set_fan_mode(FanMode fan_mode);
void set_vane_mode(VaneMode vane_mode);
void set_wide_vane_mode(WideVaneMode mode);
void set_remote_temperature(float temperature);
void clear_remote_temperature();
@@ -98,7 +124,9 @@ class MitsubishiCN105 {
POWER = 1,
MODE = 2,
FAN = 3,
REMOTE_TEMPERATURE = 4,
VANE = 4,
WIDE_VANE = 5,
REMOTE_TEMPERATURE = 6,
};
struct UpdateFlags {
@@ -142,6 +170,7 @@ class MitsubishiCN105 {
State state_{State::NOT_CONNECTED};
UpdateFlags pending_updates_;
bool use_temperature_encoding_b_{false};
bool set_wide_vane_high_bit_{false};
FrameParser frame_parser_;
uint8_t current_status_msg_type_{0};
@@ -56,7 +56,7 @@ void MitsubishiCN105Climate::dump_config() {
ESP_LOGCONFIG(TAG, " Current temperature min interval: %" PRIu32 " ms",
this->hp_.get_room_temperature_min_interval());
} else {
ESP_LOGCONFIG(TAG, " Current temperature: disabled");
ESP_LOGCONFIG(TAG, " Current temperature: DISABLED");
}
ESP_LOGCONFIG(TAG,
" Update interval: %" PRIu32 " ms\n"
@@ -53,13 +53,15 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) {
// Settings response
ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x08, 0x07,
0x00, 0x00, 0x00, 0x00, 0x03, 0xB0, 0x00, 0x00, 0x00, 0x00, 0x99});
0x00, 0x04, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x3C});
// Settings should still have initial values
EXPECT_FALSE(ctx.sut.status().power_on);
EXPECT_THAT(ctx.sut.status().target_temperature, ::testing::IsNan());
EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::UNKNOWN);
EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::UNKNOWN);
EXPECT_EQ(ctx.sut.status().vane_mode, MitsubishiCN105::VaneMode::UNKNOWN);
EXPECT_EQ(ctx.sut.status().wide_vane_mode, MitsubishiCN105::WideVaneMode::UNKNOWN);
ctx.sut.set_current_time(300);
ASSERT_FALSE(ctx.sut.update());
@@ -70,6 +72,8 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) {
EXPECT_EQ(ctx.sut.status().target_temperature, 24.0f);
EXPECT_EQ(ctx.sut.status().mode, MitsubishiCN105::Mode::AUTO);
EXPECT_EQ(ctx.sut.status().fan_mode, MitsubishiCN105::FanMode::AUTO);
EXPECT_EQ(ctx.sut.status().vane_mode, MitsubishiCN105::VaneMode::POSITION_4);
EXPECT_EQ(ctx.sut.status().wide_vane_mode, MitsubishiCN105::WideVaneMode::SWING);
// Now fetch room temperature (0x03)
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS);
@@ -303,6 +307,30 @@ TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedB) {
EXPECT_EQ(ctx.sut.status().room_temperature, 30.0f);
}
TEST(MitsubishiCN105Tests, DecodeWideVanePackageHighBitNotSet) {
auto ctx = TestContext{};
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});
ctx.sut.update();
EXPECT_EQ(ctx.sut.status().wide_vane_mode, MitsubishiCN105::WideVaneMode::CENTER);
EXPECT_FALSE(ctx.sut.set_wide_vane_high_bit_);
}
TEST(MitsubishiCN105Tests, DecodeWideVanePackageHighBitSet) {
auto ctx = TestContext{};
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});
ctx.sut.update();
EXPECT_EQ(ctx.sut.status().wide_vane_mode, MitsubishiCN105::WideVaneMode::CENTER);
EXPECT_TRUE(ctx.sut.set_wide_vane_high_bit_);
}
TEST(MitsubishiCN105Tests, ApplySettingsPowerOn) {
auto ctx = TestContext{};
@@ -365,6 +393,37 @@ TEST(MitsubishiCN105Tests, ApplyFanModeSpeed1) {
0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x73));
}
TEST(MitsubishiCN105Tests, ApplyVaneModeSwing) {
auto ctx = TestContext{};
ctx.sut.set_vane_mode(MitsubishiCN105::VaneMode::SWING);
ctx.sut.apply_settings();
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x66));
}
TEST(MitsubishiCN105Tests, ApplyWideVaneModeLeftAndHighBitNotSet) {
auto ctx = TestContext{};
ctx.sut.set_wide_vane_mode(MitsubishiCN105::WideVaneMode::LEFT);
ctx.sut.apply_settings();
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x7A));
}
TEST(MitsubishiCN105Tests, ApplyWideVaneModeLeftAndHighBitSet) {
auto ctx = TestContext{};
ctx.sut.set_wide_vane_high_bit_ = true;
ctx.sut.set_wide_vane_mode(MitsubishiCN105::WideVaneMode::LEFT);
ctx.sut.apply_settings();
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x82, 0x00, 0x00, 0xFA));
}
TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) {
auto ctx = TestContext{};
@@ -391,15 +450,15 @@ TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) {
ctx.sut.set_target_temperature(25.0f);
ctx.sut.set_mode(MitsubishiCN105::Mode::HEAT);
ctx.sut.set_fan_mode(MitsubishiCN105::FanMode::AUTO);
ctx.sut.set_vane_mode(MitsubishiCN105::VaneMode::AUTO);
// Waiting for next status update must be interrupted and new values send to AC
ctx.sut.set_current_time(6000);
ASSERT_FALSE(ctx.sut.update());
EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 1000);
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::APPLYING_SETTINGS);
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x0F, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB2, 0x00, 0xBB));
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x01, 0x1F, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xB2, 0x00, 0xAB));
// Write ACK response
ctx.uart.push_rx({0xFC, 0x61, 0x01, 0x30, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x5E});
@@ -46,6 +46,7 @@ class TestableMitsubishiCN105 : public MitsubishiCN105 {
using MitsubishiCN105::state_;
using MitsubishiCN105::operation_start_ms_;
using MitsubishiCN105::use_temperature_encoding_b_;
using MitsubishiCN105::set_wide_vane_high_bit_;
using MitsubishiCN105::status_update_wait_credit_ms_;
using MitsubishiCN105::pending_updates_;