mirror of
https://github.com/esphome/esphome.git
synced 2026-05-24 01:10:33 +08:00
[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:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user