diff --git a/esphome/components/mipi_spi/display.py b/esphome/components/mipi_spi/display.py index 6aa98e3f66..42c7ec2224 100644 --- a/esphome/components/mipi_spi/display.py +++ b/esphome/components/mipi_spi/display.py @@ -279,6 +279,10 @@ def _final_validate(config): from esphome.components.lvgl import DOMAIN as LVGL_DOMAIN + if config[CONF_BUS_MODE] == TYPE_SINGLE: + spi.final_validate_device_schema(DOMAIN, require_miso=False, require_mosi=True)( + config + ) if not requires_buffer(config) and LVGL_DOMAIN not in global_config: # If no drawing methods are configured, and LVGL is not enabled, show a test card config[CONF_SHOW_TEST_CARD] = True @@ -286,7 +290,7 @@ def _final_validate(config): if PSRAM_DOMAIN not in global_config and CONF_BUFFER_SIZE not in config: # If PSRAM is not enabled, choose a small buffer size by default if not requires_buffer(config): - return config # No buffer needed, so no need to set a buffer size + return # No need to pick a size color_depth = get_color_depth(config) frac = denominator(config) width, height, _offset_width, _offset_height = model.get_dimensions(config) @@ -298,8 +302,6 @@ def _final_validate(config): x for x in range(2, 17) if fraction >= 1 / x ) - return config - FINAL_VALIDATE_SCHEMA = _final_validate diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp index e3923bb0b8..0bce8da1ad 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.cpp @@ -8,6 +8,7 @@ static const char *const TAG = "mitsubishi_cn105.driver"; static constexpr uint32_t WRITE_TIMEOUT_MS = 2000; +static constexpr size_t REQUEST_PAYLOAD_LEN = 0x10; static constexpr size_t HEADER_LEN = 5; static constexpr uint8_t PREAMBLE = 0xFC; static constexpr uint8_t HEADER_BYTE_1 = 0x01; @@ -15,7 +16,13 @@ static constexpr uint8_t HEADER_BYTE_2 = 0x30; static constexpr uint8_t PACKET_TYPE_CONNECT_REQUEST = 0x5A; static constexpr uint8_t PACKET_TYPE_CONNECT_RESPONSE = 0x7A; -static constexpr std::array CONNECT_REQUEST_PAYLOAD = {{0xCA, 0x01}}; +static constexpr std::array CONNECT_REQUEST_PAYLOAD = {0xCA, 0x01}; + +static constexpr uint8_t PACKET_TYPE_STATUS_REQUEST = 0x42; +static constexpr uint8_t PACKET_TYPE_STATUS_RESPONSE = 0x62; +static constexpr uint8_t STATUS_MSG_SETTINGS = 0x02; +static constexpr uint8_t STATUS_MSG_ROOM_TEMP = 0x03; +static constexpr std::array STATUS_MSG_TYPES = {STATUS_MSG_SETTINGS, STATUS_MSG_ROOM_TEMP}; static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) { return static_cast(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0})); @@ -30,19 +37,29 @@ static constexpr auto make_packet(uint8_t type, const std::arrayset_state_(State::CONNECTING); } -void MitsubishiCN105::update() { +bool MitsubishiCN105::update() { + if (const auto start = this->status_update_start_ms_; + start && (get_loop_time_ms() - *start) >= this->update_interval_ms_) { + this->cancel_waiting_and_transition_to_(State::UPDATING_STATUS); + return false; + } + if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) { this->write_timeout_start_ms_.reset(); this->read_pos_ = 0; this->set_state_(State::READ_TIMEOUT); - return; + return false; } - this->read_incoming_bytes_(); + return this->read_incoming_bytes_(); } void MitsubishiCN105::set_state_(State new_state) { @@ -63,9 +80,24 @@ bool MitsubishiCN105::should_transition(State from, State to) { return from == State::NOT_CONNECTED || from == State::READ_TIMEOUT; case State::CONNECTED: - case State::READ_TIMEOUT: return from == State::CONNECTING; + case State::UPDATING_STATUS: + return from == State::CONNECTED || from == State::STATUS_UPDATED || + from == State::WAITING_FOR_SCHEDULED_STATUS_UPDATE; + + case State::STATUS_UPDATED: + return from == State::UPDATING_STATUS; + + case State::SCHEDULE_NEXT_STATUS_UPDATE: + return from == State::STATUS_UPDATED; + + case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE: + return from == State::SCHEDULE_NEXT_STATUS_UPDATE; + + case State::READ_TIMEOUT: + return from == State::UPDATING_STATUS || from == State::CONNECTING; + default: return false; } @@ -79,7 +111,30 @@ void MitsubishiCN105::did_transition_(State to) { case State::CONNECTED: this->write_timeout_start_ms_.reset(); - // TODO: read AC status after connected, next PR + this->status_msg_index_ = 0; + this->set_state_(State::UPDATING_STATUS); + break; + + case State::UPDATING_STATUS: + this->update_status_(); + break; + + case State::STATUS_UPDATED: { + this->write_timeout_start_ms_.reset(); + if (++this->status_msg_index_ >= STATUS_MSG_TYPES.size()) { + this->status_msg_index_ = 0; + } + if (this->status_msg_index_ != 0) { + this->set_state_(State::UPDATING_STATUS); + } else { + this->set_state_(State::SCHEDULE_NEXT_STATUS_UPDATE); + } + break; + } + + case State::SCHEDULE_NEXT_STATUS_UPDATE: + this->status_update_start_ms_ = get_loop_time_ms(); + this->set_state_(State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); break; case State::READ_TIMEOUT: @@ -97,13 +152,24 @@ void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) { this->write_timeout_start_ms_ = get_loop_time_ms(); } -void MitsubishiCN105::read_incoming_bytes_() { +void MitsubishiCN105::update_status_() { + ESP_LOGV(TAG, "Requesting status update, index=%u", this->status_msg_index_); + std::array payload = {STATUS_MSG_TYPES[this->status_msg_index_]}; + this->send_packet_(make_packet(PACKET_TYPE_STATUS_REQUEST, payload)); +} + +void MitsubishiCN105::cancel_waiting_and_transition_to_(State state) { + this->status_update_start_ms_.reset(); + this->set_state_(state); +} + +bool MitsubishiCN105::read_incoming_bytes_() { uint8_t watchdog = 64; while (this->device_.available() > 0 && watchdog-- > 0) { uint8_t &value = this->read_buffer_[this->read_pos_]; if (!this->device_.read_byte(&value)) { ESP_LOGW(TAG, "UART read failed while data available"); - return; + return false; } switch (++this->read_pos_) { @@ -149,23 +215,85 @@ void MitsubishiCN105::read_incoming_bytes_() { continue; } - this->process_rx_packet_(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, len_without_checksum - HEADER_LEN); + bool processed = this->process_rx_packet_(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, + len_without_checksum - HEADER_LEN); this->reset_read_position_and_dump_buffer_("RX"); + return processed; } + + return false; } -void MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) { +bool MitsubishiCN105::process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len) { switch (type) { case PACKET_TYPE_CONNECT_RESPONSE: this->set_state_(State::CONNECTED); - break; + return false; + + case PACKET_TYPE_STATUS_RESPONSE: + return this->process_status_packet_(payload, len); default: ESP_LOGVV(TAG, "RX unknown packet type 0x%02X", type); - break; + return false; } } +bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len) { + if (len == 0) { + ESP_LOGVV(TAG, "RX status packet too short"); + return false; + } + + const auto previous = this->status_; + const auto msg_type = payload[0]; + if (!this->parse_status_payload_(msg_type, payload + 1, len - 1)) { + return false; + } + + if (msg_type == STATUS_MSG_TYPES[this->status_msg_index_]) { + this->set_state_(State::STATUS_UPDATED); + } + + return previous != this->status_ && this->is_status_initialized(); +} + +bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) { + switch (msg_type) { + case STATUS_MSG_SETTINGS: + return this->parse_status_settings_(payload, len); + + case STATUS_MSG_ROOM_TEMP: + return this->parse_status_room_temperature_(payload, len); + + default: + ESP_LOGVV(TAG, "RX unsupported status msg type 0x%02X", msg_type); + return false; + } +} + +bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len) { + if (len <= 10) { + ESP_LOGVV(TAG, "RX settings payload too short"); + return false; + } + + this->status_.power_on = payload[2] != 0; + this->status_.target_temperature = decode_temperature(-payload[4], payload[10], 31); + + return true; +} + +bool MitsubishiCN105::parse_status_room_temperature_(const uint8_t *payload, size_t len) { + if (len <= 5) { + ESP_LOGVV(TAG, "RX room temperature payload too short"); + return false; + } + + this->status_.room_temperature = decode_temperature(payload[2], payload[5], 10); + return true; +} + void MitsubishiCN105::reset_read_position_and_dump_buffer_(const char *prefix) { dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_); this->read_pos_ = 0; @@ -186,6 +314,14 @@ const LogString *MitsubishiCN105::state_to_string(State state) { return LOG_STR("Connecting"); case State::CONNECTED: return LOG_STR("Connected"); + case State::UPDATING_STATUS: + return LOG_STR("UpdatingStatus"); + case State::STATUS_UPDATED: + return LOG_STR("StatusUpdated"); + case State::SCHEDULE_NEXT_STATUS_UPDATE: + return LOG_STR("ScheduleNextStatusUpdate"); + case State::WAITING_FOR_SCHEDULED_STATUS_UPDATE: + return LOG_STR("WaitingForScheduledStatusUpdate"); case State::READ_TIMEOUT: return LOG_STR("ReadTimeout"); } diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h index fc09b3bed2..d43904b313 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105.h @@ -9,23 +9,49 @@ uint32_t get_loop_time_ms(); class MitsubishiCN105 { public: + struct Status { + bool operator==(const Status &) const = default; + + bool power_on{false}; + float target_temperature{NAN}; + float room_temperature{NAN}; + }; + explicit MitsubishiCN105(uart::UARTDevice &device) : device_(device) {} void initialize(); - void update(); + bool update(); uint32_t get_update_interval() const { return this->update_interval_ms_; } void set_update_interval(uint32_t interval_ms) { this->update_interval_ms_ = interval_ms; } + const Status &status() const { return this->status_; } + bool is_status_initialized() const { return !std::isnan(status_.room_temperature); } + protected: - enum class State : uint8_t { NOT_CONNECTED, CONNECTING, CONNECTED, READ_TIMEOUT }; + enum class State : uint8_t { + NOT_CONNECTED, + CONNECTING, + CONNECTED, + UPDATING_STATUS, + STATUS_UPDATED, + SCHEDULE_NEXT_STATUS_UPDATE, + WAITING_FOR_SCHEDULED_STATUS_UPDATE, + READ_TIMEOUT + }; void set_state_(State new_state); void did_transition_(State to); - void read_incoming_bytes_(); - void process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len); + bool read_incoming_bytes_(); + bool process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len); + bool process_status_packet_(const uint8_t *payload, size_t len); + bool parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len); + bool parse_status_settings_(const uint8_t *payload, size_t len); + bool parse_status_room_temperature_(const uint8_t *payload, size_t len); void reset_read_position_and_dump_buffer_(const char *prefix); void send_packet_(const uint8_t *packet, size_t len); + void update_status_(); + void cancel_waiting_and_transition_to_(State state); template void send_packet_(const T &packet) { this->send_packet_(packet.data(), packet.size()); } static bool should_transition(State from, State to); static const LogString *state_to_string(State state); @@ -34,7 +60,10 @@ class MitsubishiCN105 { uart::UARTDevice &device_; uint32_t update_interval_ms_{1000}; std::optional write_timeout_start_ms_; + std::optional status_update_start_ms_; + Status status_{}; State state_{State::NOT_CONNECTED}; + uint8_t status_msg_index_{0}; private: static constexpr size_t READ_BUFFER_SIZE = 32; diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp index cce6bef5e4..55fc23c449 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.cpp @@ -17,13 +17,34 @@ void MitsubishiCN105Climate::dump_config() { void MitsubishiCN105Climate::setup() { this->hp_.initialize(); } -void MitsubishiCN105Climate::loop() { this->hp_.update(); } +void MitsubishiCN105Climate::loop() { + if (this->hp_.update()) { + this->apply_values_(); + } +} climate::ClimateTraits MitsubishiCN105Climate::traits() { climate::ClimateTraits traits; + + traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + + traits.set_visual_min_temperature(16.0f); + traits.set_visual_max_temperature(31.0f); + traits.set_visual_temperature_step(1.0f); + traits.set_visual_current_temperature_step(0.5f); + return traits; } void MitsubishiCN105Climate::control(const climate::ClimateCall &call) {} +void MitsubishiCN105Climate::apply_values_() { + const auto &status = this->hp_.status(); + + this->target_temperature = status.target_temperature; + this->current_temperature = status.room_temperature; + + this->publish_state(); +} + } // 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 08b482025f..da8f8d8d0a 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_climate.h @@ -21,6 +21,8 @@ class MitsubishiCN105Climate : public climate::Climate, public Component, public void set_update_interval(uint32_t ms) { hp_.set_update_interval(ms); } protected: + void apply_values_(); + MitsubishiCN105 hp_; }; diff --git a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp index 55a0a2328f..0f3fcb5648 100644 --- a/esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp +++ b/esphome/components/mitsubishi_cn105/mitsubishi_cn105_time.cpp @@ -2,6 +2,6 @@ namespace esphome::mitsubishi_cn105 { -uint32_t __attribute__((weak)) get_loop_time_ms() { return App.get_loop_component_start_time(); }; +uint32_t __attribute__((weak)) get_loop_time_ms() { return App.get_loop_component_start_time(); } } // namespace esphome::mitsubishi_cn105 diff --git a/tests/component_tests/mipi_spi/conftest.py b/tests/component_tests/mipi_spi/conftest.py index eddf0987d0..082a9e55f2 100644 --- a/tests/component_tests/mipi_spi/conftest.py +++ b/tests/component_tests/mipi_spi/conftest.py @@ -1,6 +1,7 @@ """Tests for mpip_spi configuration validation.""" from collections.abc import Callable, Generator +from unittest import mock import pytest @@ -12,6 +13,16 @@ from esphome.core import CORE from esphome.pins import gpio_pin_schema +@pytest.fixture(autouse=True) +def mock_spi_final_validate(): + """Mock spi.final_validate_device_schema since unit tests have no real SPI bus config.""" + with mock.patch( + "esphome.components.spi.final_validate_device_schema", + return_value=lambda config: None, + ): + yield + + @pytest.fixture def choose_variant_with_pins() -> Generator[Callable[[list], None]]: """ diff --git a/tests/component_tests/mipi_spi/test_display_metadata.py b/tests/component_tests/mipi_spi/test_display_metadata.py index ab42a75694..c11c7816e4 100644 --- a/tests/component_tests/mipi_spi/test_display_metadata.py +++ b/tests/component_tests/mipi_spi/test_display_metadata.py @@ -25,7 +25,9 @@ from tests.component_tests.types import SetCoreConfigCallable def validated_config(config): """Run schema + final validation and return the validated config.""" - return FINAL_VALIDATE_SCHEMA(CONFIG_SCHEMA(config)) + config = CONFIG_SCHEMA(config) + FINAL_VALIDATE_SCHEMA(config) + return config def test_metadata_native_quad_default_test_card( diff --git a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp index e01d9e69ff..5b4f84623e 100644 --- a/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp +++ b/tests/components/mitsubishi_cn105/climate/mitsubishi_cn105_tests.cpp @@ -25,27 +25,80 @@ TEST(MitsubishiCN105Tests, InitSendsConnectPacket) { EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional{123}); } -TEST(MitsubishiCN105Tests, SuccessfullyConnects) { +TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) { auto ctx = TestContext{}; ctx.sut.initialize(); ctx.uart.tx.clear(); // Remove first connect packet bytes EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); - EXPECT_TRUE(ctx.sut.write_timeout_start_ms_.has_value()); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional{0}); + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); // Connect response ctx.uart.push_rx({0xFC, 0x7A, 0x01, 0x30, 0x00, 0x55}); - ctx.sut.update(); + ctx.sut.set_current_time(200); + ASSERT_FALSE(ctx.sut.update()); - // All bytes from UART should be consumed and state = CONNECTED + // All bytes from UART should be consumed EXPECT_TRUE(ctx.uart.rx.empty()); - EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTED); - EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value()); + // After successful connect we request status, first settings (0x02) + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x42, 0x01, 0x30, 0x10, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7B)); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional{200}); + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + + // Clear TX bytes. + ctx.uart.tx.clear(); + + // 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}); + + // Settings should still have initial values + EXPECT_FALSE(ctx.sut.status().power_on); + EXPECT_THAT(ctx.sut.status().target_temperature, ::testing::IsNan()); + + ctx.sut.set_current_time(300); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_TRUE(ctx.uart.rx.empty()); + + // Check settings that we just read from received package + EXPECT_FALSE(ctx.sut.status().power_on); + EXPECT_EQ(ctx.sut.status().target_temperature, 24.0f); + + // Now fetch room temperature (0x03) + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); + EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x42, 0x01, 0x30, 0x10, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7A)); + EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional{300}); + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + + // Clear TX bytes. + ctx.uart.tx.clear(); + + // Room temperature response + ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x10, 0x03, 0x00, 0x00, 0x0B, 0x00, 0x00, + 0xAA, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5}); + + // Room temperature should still have initial value + EXPECT_THAT(ctx.sut.status().room_temperature, ::testing::IsNan()); + + ctx.sut.set_current_time(400); + EXPECT_FALSE(ctx.sut.is_status_initialized()); + ASSERT_TRUE(ctx.sut.update()); + EXPECT_TRUE(ctx.uart.rx.empty()); + EXPECT_TRUE(ctx.sut.is_status_initialized()); + + // Check room temperature we just read from received package + EXPECT_EQ(ctx.sut.status().room_temperature, 21.0f); - // Nothing should be send to UART EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + EXPECT_FALSE(ctx.sut.write_timeout_start_ms_.has_value()); + EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional{400}); } TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) { @@ -55,21 +108,21 @@ TEST(MitsubishiCN105Tests, NoResponseTriggersReconnect) { ctx.uart.tx.clear(); // Remove first connect packet bytes // No response (no RX data), no retry yet - ctx.sut.update(); + ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_TRUE(ctx.uart.tx.empty()); EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional{0}); // Still no response after 1999ms, no retry yet ctx.sut.set_current_time(1999); - ctx.sut.update(); + ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_TRUE(ctx.uart.tx.empty()); EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional{0}); // Stop waiting after 2s and retry connect ctx.sut.set_current_time(2000); - ctx.sut.update(); + ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x5A, 0x01, 0x30, 0x02, 0xCA, 0x01, 0xA8)); EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional{2000}); @@ -92,7 +145,7 @@ TEST(MitsubishiCN105Tests, RxWatchdogLimitsProcessingPerUpdate) { ASSERT_GT(ctx.uart.rx.size(), 64); // No valid response, no state change expected - ctx.sut.update(); + ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_TRUE(ctx.uart.tx.empty()); @@ -100,7 +153,7 @@ TEST(MitsubishiCN105Tests, RxWatchdogLimitsProcessingPerUpdate) { EXPECT_FALSE(ctx.uart.rx.empty()); // Next update will read remaining bytes, no state change expected - ctx.sut.update(); + ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_TRUE(ctx.uart.tx.empty()); EXPECT_TRUE(ctx.uart.rx.empty()); @@ -162,7 +215,7 @@ TEST(MitsubishiCN105Tests, ParserHandlesMixedRxStream) { // Drain RX - no valid response, no state change expected int iterations = 0; while (!ctx.uart.rx.empty() && iterations++ < 10) { - ctx.sut.update(); + ASSERT_FALSE(ctx.sut.update()); EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::CONNECTING); EXPECT_TRUE(ctx.uart.tx.empty()); } @@ -170,4 +223,81 @@ TEST(MitsubishiCN105Tests, ParserHandlesMixedRxStream) { EXPECT_TRUE(ctx.uart.rx.empty()); } +TEST(MitsubishiCN105Tests, NextStatusUpdateAfterUpdateIntervalMilliseconds) { + auto ctx = TestContext{}; + + ctx.sut.set_update_interval(2000); + ctx.sut.set_current_time(80000); + + // No scheduled status update + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); + + // Status update completed, schedule next status update + ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED; + ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE); + + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional{80000}); + + // Wait for update_interval (ms) before doing another status update + ASSERT_FALSE(ctx.sut.update()); + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + + ctx.sut.set_current_time(81999); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_TRUE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE); + + ctx.sut.set_current_time(82000); + ASSERT_FALSE(ctx.sut.update()); + EXPECT_FALSE(ctx.uart.tx.empty()); + EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS); + EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value()); +} + +TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedA) { + auto ctx = TestContext{}; + + ctx.uart.push_rx( + {0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x01, 0x03, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56}); + + ctx.sut.update(); + + EXPECT_TRUE(ctx.sut.status().power_on); + EXPECT_EQ(ctx.sut.status().target_temperature, 26.0f); +} + +TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedB) { + auto ctx = TestContext{}; + + ctx.uart.push_rx( + {0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xB7}); + + ctx.sut.update(); + + EXPECT_FALSE(ctx.sut.status().power_on); + EXPECT_EQ(ctx.sut.status().target_temperature, 18.5f); +} + +TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedA) { + auto ctx = TestContext{}; + + ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x07, 0x03, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00, 0x5D}); + + ctx.sut.update(); + + EXPECT_EQ(ctx.sut.status().room_temperature, 16.0f); +} + +TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedB) { + auto ctx = TestContext{}; + + ctx.uart.push_rx({0xFC, 0x62, 0x01, 0x30, 0x07, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0xBC, 0xA7}); + + ctx.sut.update(); + + EXPECT_EQ(ctx.sut.status().room_temperature, 30.0f); +} + } // namespace esphome::mitsubishi_cn105::testing diff --git a/tests/components/mitsubishi_cn105/common.cpp b/tests/components/mitsubishi_cn105/common.cpp index ea13d7676c..50993c5c2c 100644 --- a/tests/components/mitsubishi_cn105/common.cpp +++ b/tests/components/mitsubishi_cn105/common.cpp @@ -2,6 +2,6 @@ namespace esphome::mitsubishi_cn105 { -uint32_t get_loop_time_ms() { return testing::TestableMitsubishiCN105::test_loop_time_ms; }; +uint32_t get_loop_time_ms() { return testing::TestableMitsubishiCN105::test_loop_time_ms; } } // namespace esphome::mitsubishi_cn105 diff --git a/tests/components/mitsubishi_cn105/common.h b/tests/components/mitsubishi_cn105/common.h index c41268d723..ed55c3dc0c 100644 --- a/tests/components/mitsubishi_cn105/common.h +++ b/tests/components/mitsubishi_cn105/common.h @@ -44,6 +44,9 @@ class TestableMitsubishiCN105 : public MitsubishiCN105 { using MitsubishiCN105::State; using MitsubishiCN105::state_; using MitsubishiCN105::write_timeout_start_ms_; + using MitsubishiCN105::status_update_start_ms_; + + void set_state(State s) { this->set_state_(s); } static inline uint32_t test_loop_time_ms = 0;