mirror of
https://github.com/esphome/esphome.git
synced 2026-05-25 18:47:56 +08:00
[mitsubishi_cn105] Add climate component for Mitsubishi A/C units with CN105 connector (Part 3) (#15437)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: J. Nick Koston <nick+github@koston.org> Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
This commit is contained in:
@@ -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<uint8_t, 2> CONNECT_REQUEST_PAYLOAD = {{0xCA, 0x01}};
|
||||
static constexpr std::array<uint8_t, 2> 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<uint8_t, 2> 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<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0}));
|
||||
@@ -30,19 +37,29 @@ 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) {
|
||||
return temp_b != 0 ? (temp_b - 128) / 2.0f : delta + temp_a;
|
||||
}
|
||||
|
||||
static constexpr auto CONNECT_PACKET = make_packet(PACKET_TYPE_CONNECT_REQUEST, CONNECT_REQUEST_PAYLOAD);
|
||||
|
||||
void MitsubishiCN105::initialize() { this->set_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<uint8_t, REQUEST_PAYLOAD_LEN> 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");
|
||||
}
|
||||
|
||||
@@ -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<typename T> 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<uint32_t> write_timeout_start_ms_;
|
||||
std::optional<uint32_t> 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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,27 +25,80 @@ TEST(MitsubishiCN105Tests, InitSendsConnectPacket) {
|
||||
EXPECT_EQ(ctx.sut.write_timeout_start_ms_, std::optional<uint32_t>{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<uint32_t>{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<uint32_t>{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<uint32_t>{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<uint32_t>{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<uint32_t>{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<uint32_t>{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<uint32_t>{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<uint32_t>{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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user