[mitsubishi_cn105] Add C++ API for setting/clearing remote room temperature (#15558)

Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Boris Krivonog
2026-05-12 22:39:21 +02:00
committed by GitHub
parent ee72efa760
commit 66e4a1dfa8
7 changed files with 338 additions and 39 deletions
+69 -2
View File
@@ -1,8 +1,11 @@
from esphome import automation
import esphome.codegen as cg
from esphome.components import climate, uart
import esphome.config_validation as cv
from esphome.const import CONF_UPDATE_INTERVAL
from esphome.types import ConfigType
from esphome.const import CONF_ID, CONF_TEMPERATURE, CONF_UPDATE_INTERVAL
from esphome.core import ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType, TemplateArgsType
DEPENDENCIES = ["uart"]
AUTO_LOAD = ["climate"]
@@ -19,6 +22,18 @@ MitsubishiCN105Climate = mitsubishi_ns.class_(
uart.UARTDevice,
)
SetRemoteTemperatureAction = mitsubishi_ns.class_(
"SetRemoteTemperatureAction",
automation.Action,
cg.Parented.template(MitsubishiCN105Climate),
)
ClearRemoteTemperatureAction = mitsubishi_ns.class_(
"ClearRemoteTemperatureAction",
automation.Action,
cg.Parented.template(MitsubishiCN105Climate),
)
CONFIG_SCHEMA = (
climate.climate_schema(MitsubishiCN105Climate)
.extend(uart.UART_DEVICE_SCHEMA)
@@ -53,3 +68,55 @@ async def to_code(config: ConfigType) -> None:
config[CONF_CURRENT_TEMPERATURE_MIN_INTERVAL]
)
)
@automation.register_action(
"climate.mitsubishi_cn105.set_remote_temperature",
SetRemoteTemperatureAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(MitsubishiCN105Climate),
cv.Required(CONF_TEMPERATURE): cv.templatable(
cv.All(
cv.temperature,
cv.Range(min=8.0, max=39.5),
)
),
}
),
synchronous=True,
)
async def set_remote_temperature_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
temperature = await cg.templatable(config[CONF_TEMPERATURE], args, float)
cg.add(var.set_temperature(temperature))
return var
@automation.register_action(
"climate.mitsubishi_cn105.clear_remote_temperature",
ClearRemoteTemperatureAction,
cv.Schema(
{
cv.Required(CONF_ID): cv.use_id(MitsubishiCN105Climate),
}
),
synchronous=True,
)
async def clear_remote_temperature_action_to_code(
config: ConfigType,
action_id: ID,
template_arg: cg.TemplateArguments,
args: TemplateArgsType,
) -> MockObj:
var = cg.new_Pvariable(action_id, template_arg)
await cg.register_parented(var, config[CONF_ID])
return var
@@ -1,3 +1,4 @@
#include <algorithm>
#include <array>
#include <cmath>
#include <numeric>
@@ -92,6 +93,7 @@ void MitsubishiCN105::initialize() { this->set_state_(State::CONNECTING); }
bool MitsubishiCN105::update() {
if (const auto start = this->status_update_start_ms_) {
if (this->pending_updates_.any()) {
this->status_update_wait_credit_ms_ = std::min(this->update_interval_ms_, get_loop_time_ms() - *start);
this->cancel_waiting_and_transition_to_(State::APPLYING_SETTINGS);
return false;
}
@@ -105,6 +107,7 @@ bool MitsubishiCN105::update() {
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->frame_parser_.reset();
this->status_update_wait_credit_ms_ = 0;
this->set_state_(State::READ_TIMEOUT);
return false;
}
@@ -191,14 +194,14 @@ void MitsubishiCN105::did_transition_(State to) {
}
case State::SCHEDULE_NEXT_STATUS_UPDATE:
this->status_update_start_ms_ = get_loop_time_ms();
this->status_update_start_ms_ = get_loop_time_ms() - this->status_update_wait_credit_ms_;
this->status_update_wait_credit_ms_ = 0;
this->current_status_msg_type_ = STATUS_MSG_SETTINGS;
this->set_state_(State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
break;
case State::APPLYING_SETTINGS:
this->apply_settings_();
this->pending_updates_.clear();
break;
case State::SETTINGS_APPLIED:
@@ -309,21 +312,21 @@ bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len)
return false;
}
if (!this->pending_updates_.has(UpdateFlag::POWER)) {
if (!this->pending_updates_.contains(UpdateFlag::POWER)) {
this->status_.power_on = payload[2] != 0;
}
this->use_temperature_encoding_b_ = payload[10] != 0;
if (!this->pending_updates_.has(UpdateFlag::TEMPERATURE)) {
if (!this->pending_updates_.contains(UpdateFlag::TEMPERATURE)) {
this->status_.target_temperature = decode_temperature(-payload[4], payload[10], TARGET_TEMPERATURE_ENC_A_OFFSET);
}
if (!this->pending_updates_.has(UpdateFlag::MODE)) {
if (!this->pending_updates_.contains(UpdateFlag::MODE)) {
const bool i_see = payload[3] > 0x08;
this->status_.mode = lookup(PROTOCOL_MODE_MAP, payload[3] - (i_see ? 0x08 : 0)).value_or(Mode::UNKNOWN);
}
if (!this->pending_updates_.has(UpdateFlag::FAN)) {
if (!this->pending_updates_.contains(UpdateFlag::FAN)) {
this->status_.fan_mode = lookup(PROTOCOL_FAN_MODE_MAP, payload[5]).value_or(FanMode::UNKNOWN);
}
@@ -342,6 +345,27 @@ bool MitsubishiCN105::parse_status_room_temperature_(const uint8_t *payload, siz
return true;
}
void MitsubishiCN105::set_remote_temperature(float temperature) {
if (std::isnan(temperature)) {
ESP_LOGD(TAG, "Ignoring NaN remote temperature");
return;
}
if (temperature < 8.0f || temperature > 39.5f) {
ESP_LOGD(TAG, "Ignoring out-of-range remote temperature: %.1f", temperature);
return;
}
this->set_remote_temperature_half_deg_(static_cast<uint8_t>(std::round(temperature * 2.0f)));
}
void MitsubishiCN105::clear_remote_temperature() {
this->set_remote_temperature_half_deg_(REMOTE_TEMPERATURE_DISABLED);
}
void MitsubishiCN105::set_remote_temperature_half_deg_(uint8_t temperature_half_deg) {
this->remote_temperature_half_deg_ = temperature_half_deg;
this->pending_updates_.set(UpdateFlag::REMOTE_TEMPERATURE);
}
void MitsubishiCN105::set_power(bool power_on) {
this->status_.power_on = power_on;
this->pending_updates_.set(UpdateFlag::POWER);
@@ -377,30 +401,47 @@ void MitsubishiCN105::set_fan_mode(FanMode fan_mode) {
}
void MitsubishiCN105::apply_settings_() {
std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {0x01};
std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload{};
if (this->pending_updates_.has(UpdateFlag::POWER)) {
payload[1] |= 0x01;
payload[3] = this->status_.power_on ? 0x01 : 0x00;
}
if (this->pending_updates_.has(UpdateFlag::TEMPERATURE)) {
payload[1] |= 0x04;
if (this->use_temperature_encoding_b_) {
payload[14] = static_cast<uint8_t>(std::round(this->status_.target_temperature * 2.0f) + 128);
// Apply all other pending settings first; handle REMOTE_TEMPERATURE last
if (this->pending_updates_.contains_only(UpdateFlag::REMOTE_TEMPERATURE)) {
payload[0] = 0x07;
if (this->remote_temperature_half_deg_ == REMOTE_TEMPERATURE_DISABLED) {
payload[3] = 0x80;
} else {
payload[5] = static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - std::round(this->status_.target_temperature));
payload[1] = 0x01;
payload[2] = static_cast<uint8_t>(this->remote_temperature_half_deg_ - 16);
payload[3] = static_cast<uint8_t>(this->remote_temperature_half_deg_ + 128);
}
this->pending_updates_.clear(UpdateFlag::REMOTE_TEMPERATURE);
} else {
payload[0] = 0x01;
if (this->pending_updates_.contains(UpdateFlag::POWER)) {
payload[1] |= 0x01;
payload[3] = this->status_.power_on ? 0x01 : 0x00;
}
}
if (this->pending_updates_.has(UpdateFlag::MODE) &&
reverse_lookup(PROTOCOL_MODE_MAP, this->status_.mode, payload[4])) {
payload[1] |= 0x02;
}
if (this->pending_updates_.contains(UpdateFlag::TEMPERATURE)) {
payload[1] |= 0x04;
if (this->use_temperature_encoding_b_) {
payload[14] = static_cast<uint8_t>(std::round(this->status_.target_temperature * 2.0f) + 128);
} else {
payload[5] =
static_cast<uint8_t>(TARGET_TEMPERATURE_ENC_A_OFFSET - std::round(this->status_.target_temperature));
}
}
if (this->pending_updates_.has(UpdateFlag::FAN) &&
reverse_lookup(PROTOCOL_FAN_MODE_MAP, this->status_.fan_mode, payload[6])) {
payload[1] |= 0x08;
if (this->pending_updates_.contains(UpdateFlag::MODE) &&
reverse_lookup(PROTOCOL_MODE_MAP, this->status_.mode, payload[4])) {
payload[1] |= 0x02;
}
if (this->pending_updates_.contains(UpdateFlag::FAN) &&
reverse_lookup(PROTOCOL_FAN_MODE_MAP, this->status_.fan_mode, payload[6])) {
payload[1] |= 0x08;
}
this->pending_updates_.clear(UpdateFlag::POWER, UpdateFlag::TEMPERATURE, UpdateFlag::MODE, UpdateFlag::FAN);
}
this->send_packet_(make_packet(PACKET_TYPE_WRITE_SETTINGS_REQUEST, payload));
@@ -2,6 +2,7 @@
#include <optional>
#include "esphome/components/uart/uart.h"
#include "esphome/core/finite_set_mask.h"
namespace esphome::mitsubishi_cn105 {
@@ -60,6 +61,8 @@ class MitsubishiCN105 {
void set_target_temperature(float target_temperature);
void set_mode(Mode mode);
void set_fan_mode(FanMode fan_mode);
void set_remote_temperature(float temperature);
void clear_remote_temperature();
protected:
enum class State : uint8_t {
@@ -91,20 +94,25 @@ class MitsubishiCN105 {
};
enum class UpdateFlag : uint8_t {
TEMPERATURE = 1 << 0,
POWER = 1 << 1,
MODE = 1 << 2,
FAN = 1 << 3,
TEMPERATURE = 0,
POWER = 1,
MODE = 2,
FAN = 3,
REMOTE_TEMPERATURE = 4,
};
struct UpdateFlags {
void set(UpdateFlag f) { flags_ |= static_cast<uint8_t>(f); }
void clear() { flags_ = 0; }
bool any() const { return flags_ != 0; }
bool has(UpdateFlag f) const { return (flags_ & static_cast<uint8_t>(f)) != 0; }
template<typename... Flags> void set(Flags... flags) { (this->mask_.insert(flags), ...); }
template<typename... Flags> void clear(Flags... flags) { (this->mask_.erase(flags), ...); }
bool any() const { return !this->mask_.empty(); }
bool contains(UpdateFlag flag) const { return this->mask_.count(flag); }
bool contains_only(UpdateFlag flag) const { return this->mask_.get_mask() == Mask{flag}.get_mask(); }
protected:
uint8_t flags_{0};
using Mask =
FiniteSetMask<UpdateFlag, DefaultBitPolicy<UpdateFlag, static_cast<int>(UpdateFlag::REMOTE_TEMPERATURE) + 1>>;
Mask mask_;
};
void set_state_(State new_state);
@@ -119,12 +127,14 @@ class MitsubishiCN105 {
void cancel_waiting_and_transition_to_(State state);
bool should_request_room_temperature_() const;
void apply_settings_();
void set_remote_temperature_half_deg_(uint8_t temperature_half_deg);
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);
uart::UARTDevice &device_;
uint32_t update_interval_ms_{1000};
uint32_t status_update_wait_credit_ms_{0};
uint32_t room_temperature_min_interval_ms_{60000};
std::optional<uint32_t> write_timeout_start_ms_;
std::optional<uint32_t> status_update_start_ms_;
@@ -133,8 +143,11 @@ class MitsubishiCN105 {
State state_{State::NOT_CONNECTED};
UpdateFlags pending_updates_;
bool use_temperature_encoding_b_{false};
uint8_t current_status_msg_type_{0};
FrameParser frame_parser_;
uint8_t current_status_msg_type_{0};
static constexpr uint8_t REMOTE_TEMPERATURE_DISABLED = 0;
uint8_t remote_temperature_half_deg_{REMOTE_TEMPERATURE_DISABLED};
};
} // namespace esphome::mitsubishi_cn105
@@ -1,5 +1,6 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
@@ -18,8 +19,11 @@ class MitsubishiCN105Climate : public climate::Climate, public Component, public
climate::ClimateTraits traits() override;
void control(const climate::ClimateCall &call) override;
void set_update_interval(uint32_t ms) { hp_.set_update_interval(ms); }
void set_current_temperature_min_interval(uint32_t ms) { hp_.set_room_temperature_min_interval(ms); }
void set_update_interval(uint32_t ms) { this->hp_.set_update_interval(ms); }
void set_current_temperature_min_interval(uint32_t ms) { this->hp_.set_room_temperature_min_interval(ms); }
void set_remote_temperature(float temperature) { this->hp_.set_remote_temperature(temperature); }
void clear_remote_temperature() { this->hp_.clear_remote_temperature(); }
protected:
void apply_values_();
@@ -27,4 +31,18 @@ class MitsubishiCN105Climate : public climate::Climate, public Component, public
MitsubishiCN105 hp_;
};
template<typename... Ts>
class SetRemoteTemperatureAction : public Action<Ts...>, public Parented<MitsubishiCN105Climate> {
public:
TEMPLATABLE_VALUE(float, temperature)
void play(const Ts &...x) override { this->parent_->set_remote_temperature(this->temperature_.value(x...)); }
};
template<typename... Ts>
class ClearRemoteTemperatureAction : public Action<Ts...>, public Parented<MitsubishiCN105Climate> {
public:
void play(const Ts &...x) override { this->parent_->clear_remote_temperature(); }
};
} // namespace esphome::mitsubishi_cn105
@@ -375,14 +375,22 @@ TEST(MitsubishiCN105Tests, ApplyFanModeSpeed1) {
TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) {
auto ctx = TestContext{};
ctx.sut.set_update_interval(2000);
ctx.sut.set_current_time(5000);
// Waiting for next scheduled 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>{5000});
EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 0);
// Nothing to do in update (rx empty, no timeout)
ctx.sut.set_current_time(5500);
ASSERT_FALSE(ctx.sut.update());
EXPECT_TRUE(ctx.uart.tx.empty());
EXPECT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{5000});
EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 0);
// Write new values
ctx.sut.use_temperature_encoding_b_ = true;
@@ -392,11 +400,52 @@ TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) {
ctx.sut.set_fan_mode(MitsubishiCN105::FanMode::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_FALSE(ctx.sut.status_update_start_ms_.has_value());
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));
// 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});
ctx.sut.set_current_time(6500);
ASSERT_FALSE(ctx.sut.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>{6500 - 1000});
EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 0);
}
TEST(MitsubishiCN105Tests, SetAndClearRemoteRoomTemp) {
auto ctx = TestContext{};
// Set remote temperature
ctx.sut.set_remote_temperature(28.5f);
ctx.sut.state_ = TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE;
ctx.sut.set_state(TestableMitsubishiCN105::State::APPLYING_SETTINGS);
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x07, 0x01, 0x29, 0xB9, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x94));
// 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});
ASSERT_FALSE(ctx.sut.update());
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
ctx.uart.tx.clear();
// Clear remote temperature
ctx.sut.clear_remote_temperature();
ctx.sut.set_state(TestableMitsubishiCN105::State::APPLYING_SETTINGS);
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x07, 0x00, 0x00, 0x80, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xF7));
// 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});
@@ -404,4 +453,102 @@ TEST(MitsubishiCN105Tests, WriteInterruptsWaitingForNextStatusUpdate) {
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
}
TEST(MitsubishiCN105Tests, ApplyQueuedSettingsThenRemoteRoomTempInSecondWrite) {
auto ctx = TestContext{};
// Queue normal settings plus remote temperature together.
ctx.sut.use_temperature_encoding_b_ = true;
ctx.sut.set_power(false);
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_remote_temperature(28.5f);
// First apply sends only the normal settings write.
ctx.sut.state_ = TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE;
ctx.sut.set_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_TRUE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE));
EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::POWER));
EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::TEMPERATURE));
EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::MODE));
EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::FAN));
// ACK the first write. Remote temperature should still be pending afterward.
ctx.uart.tx.clear();
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});
ASSERT_FALSE(ctx.sut.update());
EXPECT_TRUE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE));
// The next apply sends the remote-temperature packet and clears the last pending flag.
ctx.uart.tx.clear();
ctx.sut.set_state(TestableMitsubishiCN105::State::APPLYING_SETTINGS);
EXPECT_THAT(ctx.uart.tx, ::testing::ElementsAre(0xFC, 0x41, 0x01, 0x30, 0x10, 0x07, 0x01, 0x29, 0xB9, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x94));
EXPECT_FALSE(ctx.sut.pending_updates_.any());
}
TEST(MitsubishiCN105Tests, WriteTimeoutClearsStatusUpdateWaitCreditOnReconnect) {
auto ctx = TestContext{};
ctx.sut.set_update_interval(2000);
ctx.sut.set_current_time(5000);
// Start in the scheduled status update wait state.
ctx.sut.state_ = TestableMitsubishiCN105::State::STATUS_UPDATED;
ctx.sut.set_state(TestableMitsubishiCN105::State::SCHEDULE_NEXT_STATUS_UPDATE);
ASSERT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
ASSERT_EQ(ctx.sut.status_update_start_ms_, std::optional<uint32_t>{5000});
ASSERT_EQ(ctx.sut.status_update_wait_credit_ms_, 0);
// Interrupt that wait with a write so credit is accumulated.
ctx.sut.use_temperature_encoding_b_ = true;
ctx.sut.set_power(false);
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_current_time(6000);
ASSERT_FALSE(ctx.sut.update());
ASSERT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::APPLYING_SETTINGS);
ASSERT_FALSE(ctx.sut.status_update_start_ms_.has_value());
ASSERT_EQ(ctx.sut.status_update_wait_credit_ms_, 1000);
// Do not ACK the write. Advance time far enough to force timeout/reconnect
// handling and verify that stale wait credit is cleared during recovery.
ctx.sut.set_current_time(36000);
ASSERT_FALSE(ctx.sut.update());
EXPECT_NE(ctx.sut.state_, TestableMitsubishiCN105::State::APPLYING_SETTINGS);
EXPECT_EQ(ctx.sut.status_update_wait_credit_ms_, 0);
EXPECT_FALSE(ctx.sut.status_update_start_ms_.has_value());
}
TEST(MitsubishiCN105Tests, SetOutOfRangeRemoteRoomTempIsIgnored) {
auto ctx = TestContext{};
ctx.sut.set_remote_temperature(7.0f);
EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE));
ctx.sut.set_remote_temperature(40.0f);
EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE));
ctx.sut.set_remote_temperature(NAN);
EXPECT_FALSE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE));
}
TEST(MitsubishiCN105Tests, SetMinRemoteRoomTemp) {
auto ctx = TestContext{};
ctx.sut.set_remote_temperature(8.0f);
EXPECT_TRUE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE));
}
TEST(MitsubishiCN105Tests, SetMaxRemoteRoomTemp) {
auto ctx = TestContext{};
ctx.sut.set_remote_temperature(39.5f);
EXPECT_TRUE(ctx.sut.pending_updates_.contains(TestableMitsubishiCN105::UpdateFlag::REMOTE_TEMPERATURE));
}
} // namespace esphome::mitsubishi_cn105::testing
@@ -42,10 +42,13 @@ class TestableMitsubishiCN105 : public MitsubishiCN105 {
public:
using MitsubishiCN105::MitsubishiCN105;
using MitsubishiCN105::State;
using MitsubishiCN105::UpdateFlag;
using MitsubishiCN105::state_;
using MitsubishiCN105::write_timeout_start_ms_;
using MitsubishiCN105::status_update_start_ms_;
using MitsubishiCN105::use_temperature_encoding_b_;
using MitsubishiCN105::status_update_wait_credit_ms_;
using MitsubishiCN105::pending_updates_;
void set_state(State s) { this->set_state_(s); }
void apply_settings() { this->apply_settings_(); }
@@ -1,4 +1,14 @@
climate:
- platform: mitsubishi_cn105
id: ac
name: "AC Test"
uart_id: uart_bus
esphome:
on_boot:
then:
- climate.mitsubishi_cn105.set_remote_temperature:
id: ac
temperature: 22.0
- climate.mitsubishi_cn105.clear_remote_temperature:
id: ac