diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp index 3a42616d8a6..4782a2ef93e 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp @@ -50,6 +50,11 @@ template struct LookupMap { } return false; } + + constexpr bool is_valid(value_type value) const { + uint8_t raw; + return reverse_lookup(value, raw); + } }; template static constexpr auto make_map(const T (&values)[N]) { @@ -78,6 +83,33 @@ static constexpr auto PROTOCOL_FAN_MODE_MAP = make_map({ + 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, // 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(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0})); } @@ -91,7 +123,7 @@ static constexpr auto make_packet(uint8_t type, const std::arrayset_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(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(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(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(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 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)); diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h index 60ca81cf9e0..dbeb43068e9 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h @@ -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}; diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp index 284339e57f9..67a561397a1 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp @@ -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" diff --git a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp index db2fbced1c3..ef3cdd0fffe 100644 --- a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp +++ b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp @@ -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}); diff --git a/tests/components/mitsubishi_cn105/common.h b/tests/components/mitsubishi_cn105/common.h index 73e09d6c84b..59b62037326 100644 --- a/tests/components/mitsubishi_cn105/common.h +++ b/tests/components/mitsubishi_cn105/common.h @@ -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_;