mirror of
https://github.com/esphome/esphome.git
synced 2026-06-01 01:19:45 +08:00
[mitsubishi_cn105] Add climate component for Mitsubishi A/C units with CN105 connector (Part 4) (#15462)
This commit is contained in:
@@ -8,6 +8,8 @@ DEPENDENCIES = ["uart"]
|
|||||||
AUTO_LOAD = ["climate"]
|
AUTO_LOAD = ["climate"]
|
||||||
CODEOWNERS = ["@crnjan"]
|
CODEOWNERS = ["@crnjan"]
|
||||||
|
|
||||||
|
CONF_CURRENT_TEMPERATURE_MIN_INTERVAL = "current_temperature_min_interval"
|
||||||
|
|
||||||
mitsubishi_ns = cg.esphome_ns.namespace("mitsubishi_cn105")
|
mitsubishi_ns = cg.esphome_ns.namespace("mitsubishi_cn105")
|
||||||
|
|
||||||
MitsubishiCN105Climate = mitsubishi_ns.class_(
|
MitsubishiCN105Climate = mitsubishi_ns.class_(
|
||||||
@@ -20,7 +22,14 @@ MitsubishiCN105Climate = mitsubishi_ns.class_(
|
|||||||
CONFIG_SCHEMA = (
|
CONFIG_SCHEMA = (
|
||||||
climate.climate_schema(MitsubishiCN105Climate)
|
climate.climate_schema(MitsubishiCN105Climate)
|
||||||
.extend(uart.UART_DEVICE_SCHEMA)
|
.extend(uart.UART_DEVICE_SCHEMA)
|
||||||
.extend({cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval})
|
.extend(
|
||||||
|
{
|
||||||
|
cv.Optional(CONF_UPDATE_INTERVAL, default="1s"): cv.update_interval,
|
||||||
|
cv.Optional(
|
||||||
|
CONF_CURRENT_TEMPERATURE_MIN_INTERVAL, default="60s"
|
||||||
|
): cv.update_interval,
|
||||||
|
}
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
FINAL_VALIDATE_SCHEMA = cv.All(
|
FINAL_VALIDATE_SCHEMA = cv.All(
|
||||||
@@ -39,3 +48,8 @@ async def to_code(config: ConfigType) -> None:
|
|||||||
var = await climate.new_climate(config)
|
var = await climate.new_climate(config)
|
||||||
await cg.register_component(var, config)
|
await cg.register_component(var, config)
|
||||||
await uart.register_uart_device(var, config)
|
await uart.register_uart_device(var, config)
|
||||||
|
cg.add(
|
||||||
|
var.set_current_temperature_min_interval(
|
||||||
|
config[CONF_CURRENT_TEMPERATURE_MIN_INTERVAL]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|||||||
@@ -22,7 +22,33 @@ static constexpr uint8_t PACKET_TYPE_STATUS_REQUEST = 0x42;
|
|||||||
static constexpr uint8_t PACKET_TYPE_STATUS_RESPONSE = 0x62;
|
static constexpr uint8_t PACKET_TYPE_STATUS_RESPONSE = 0x62;
|
||||||
static constexpr uint8_t STATUS_MSG_SETTINGS = 0x02;
|
static constexpr uint8_t STATUS_MSG_SETTINGS = 0x02;
|
||||||
static constexpr uint8_t STATUS_MSG_ROOM_TEMP = 0x03;
|
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 std::array<std::optional<MitsubishiCN105::Mode>, 9> PROTOCOL_MODE_MAP = {
|
||||||
|
std::nullopt, // 0x00
|
||||||
|
MitsubishiCN105::Mode::HEAT, // 0x01
|
||||||
|
MitsubishiCN105::Mode::DRY, // 0x02
|
||||||
|
MitsubishiCN105::Mode::COOL, // 0x03
|
||||||
|
std::nullopt, // 0x04
|
||||||
|
std::nullopt, // 0x05
|
||||||
|
std::nullopt, // 0x06
|
||||||
|
MitsubishiCN105::Mode::FAN_ONLY, // 0x07
|
||||||
|
MitsubishiCN105::Mode::AUTO // 0x08
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr std::array<std::optional<MitsubishiCN105::FanMode>, 7> PROTOCOL_FAN_MODE_MAP = {
|
||||||
|
MitsubishiCN105::FanMode::AUTO, // 0x00
|
||||||
|
MitsubishiCN105::FanMode::QUIET, // 0x01
|
||||||
|
MitsubishiCN105::FanMode::SPEED_1, // 0x02
|
||||||
|
MitsubishiCN105::FanMode::SPEED_2, // 0x03
|
||||||
|
std::nullopt, // 0x04
|
||||||
|
MitsubishiCN105::FanMode::SPEED_3, // 0x05
|
||||||
|
MitsubishiCN105::FanMode::SPEED_4 // 0x06
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename T, size_t N>
|
||||||
|
static constexpr std::optional<T> lookup(const std::array<std::optional<T>, N> &table, uint8_t value) {
|
||||||
|
return (value < N) ? table[value] : std::nullopt;
|
||||||
|
}
|
||||||
|
|
||||||
static constexpr uint8_t checksum(const uint8_t *bytes, size_t length) {
|
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}));
|
return static_cast<uint8_t>(0xFC - std::accumulate(bytes, bytes + length, uint8_t{0}));
|
||||||
@@ -54,12 +80,14 @@ bool MitsubishiCN105::update() {
|
|||||||
|
|
||||||
if (const auto start = this->write_timeout_start_ms_; start && (get_loop_time_ms() - *start) >= WRITE_TIMEOUT_MS) {
|
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->write_timeout_start_ms_.reset();
|
||||||
this->read_pos_ = 0;
|
this->frame_parser_.reset();
|
||||||
this->set_state_(State::READ_TIMEOUT);
|
this->set_state_(State::READ_TIMEOUT);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this->read_incoming_bytes_();
|
return this->frame_parser_.read_and_parse(this->device_, [this](uint8_t type, const uint8_t *payload, size_t len) {
|
||||||
|
return this->process_rx_packet_(type, payload, len);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void MitsubishiCN105::set_state_(State new_state) {
|
void MitsubishiCN105::set_state_(State new_state) {
|
||||||
@@ -111,7 +139,7 @@ void MitsubishiCN105::did_transition_(State to) {
|
|||||||
|
|
||||||
case State::CONNECTED:
|
case State::CONNECTED:
|
||||||
this->write_timeout_start_ms_.reset();
|
this->write_timeout_start_ms_.reset();
|
||||||
this->status_msg_index_ = 0;
|
this->current_status_msg_type_ = STATUS_MSG_SETTINGS;
|
||||||
this->set_state_(State::UPDATING_STATUS);
|
this->set_state_(State::UPDATING_STATUS);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -121,10 +149,8 @@ void MitsubishiCN105::did_transition_(State to) {
|
|||||||
|
|
||||||
case State::STATUS_UPDATED: {
|
case State::STATUS_UPDATED: {
|
||||||
this->write_timeout_start_ms_.reset();
|
this->write_timeout_start_ms_.reset();
|
||||||
if (++this->status_msg_index_ >= STATUS_MSG_TYPES.size()) {
|
if (this->current_status_msg_type_ == STATUS_MSG_SETTINGS && this->should_request_room_temperature_()) {
|
||||||
this->status_msg_index_ = 0;
|
this->current_status_msg_type_ = STATUS_MSG_ROOM_TEMP;
|
||||||
}
|
|
||||||
if (this->status_msg_index_ != 0) {
|
|
||||||
this->set_state_(State::UPDATING_STATUS);
|
this->set_state_(State::UPDATING_STATUS);
|
||||||
} else {
|
} else {
|
||||||
this->set_state_(State::SCHEDULE_NEXT_STATUS_UPDATE);
|
this->set_state_(State::SCHEDULE_NEXT_STATUS_UPDATE);
|
||||||
@@ -134,6 +160,7 @@ void MitsubishiCN105::did_transition_(State to) {
|
|||||||
|
|
||||||
case State::SCHEDULE_NEXT_STATUS_UPDATE:
|
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->current_status_msg_type_ = STATUS_MSG_SETTINGS;
|
||||||
this->set_state_(State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
this->set_state_(State::WAITING_FOR_SCHEDULED_STATUS_UPDATE);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
@@ -146,15 +173,26 @@ void MitsubishiCN105::did_transition_(State to) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool MitsubishiCN105::should_request_room_temperature_() const {
|
||||||
|
if (!this->is_room_temperature_enabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this->last_room_temperature_update_ms_.has_value()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (get_loop_time_ms() - *this->last_room_temperature_update_ms_) >= this->room_temperature_min_interval_ms_;
|
||||||
|
}
|
||||||
|
|
||||||
void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) {
|
void MitsubishiCN105::send_packet_(const uint8_t *packet, size_t len) {
|
||||||
dump_buffer_vv("TX", packet, len);
|
FrameParser::dump_buffer_vv("TX", packet, len);
|
||||||
this->device_.write_array(packet, len);
|
this->device_.write_array(packet, len);
|
||||||
this->write_timeout_start_ms_ = get_loop_time_ms();
|
this->write_timeout_start_ms_ = get_loop_time_ms();
|
||||||
}
|
}
|
||||||
|
|
||||||
void MitsubishiCN105::update_status_() {
|
void MitsubishiCN105::update_status_() {
|
||||||
ESP_LOGV(TAG, "Requesting status update, index=%u", this->status_msg_index_);
|
std::array<uint8_t, REQUEST_PAYLOAD_LEN> payload = {this->current_status_msg_type_};
|
||||||
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));
|
this->send_packet_(make_packet(PACKET_TYPE_STATUS_REQUEST, payload));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,67 +201,6 @@ void MitsubishiCN105::cancel_waiting_and_transition_to_(State state) {
|
|||||||
this->set_state_(state);
|
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 false;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (++this->read_pos_) {
|
|
||||||
case 1:
|
|
||||||
if (value != PREAMBLE) {
|
|
||||||
this->reset_read_position_and_dump_buffer_("RX ignoring preamble");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case 3:
|
|
||||||
if (value != HEADER_BYTE_1) {
|
|
||||||
this->reset_read_position_and_dump_buffer_("RX invalid: header 1 mismatch");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case 4:
|
|
||||||
if (value != HEADER_BYTE_2) {
|
|
||||||
this->reset_read_position_and_dump_buffer_("RX invalid: header 2 mismatch");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
|
|
||||||
case HEADER_LEN:
|
|
||||||
static_assert(READ_BUFFER_SIZE > HEADER_LEN);
|
|
||||||
if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) {
|
|
||||||
this->reset_read_position_and_dump_buffer_("RX invalid: payload too large");
|
|
||||||
}
|
|
||||||
continue;
|
|
||||||
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]);
|
|
||||||
if (this->read_pos_ <= len_without_checksum) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (checksum(this->read_buffer_, len_without_checksum) != value) {
|
|
||||||
this->reset_read_position_and_dump_buffer_("RX invalid: checksum mismatch");
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
bool 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) {
|
switch (type) {
|
||||||
case PACKET_TYPE_CONNECT_RESPONSE:
|
case PACKET_TYPE_CONNECT_RESPONSE:
|
||||||
@@ -251,11 +228,19 @@ bool MitsubishiCN105::process_status_packet_(const uint8_t *payload, size_t len)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg_type == STATUS_MSG_TYPES[this->status_msg_index_]) {
|
if (msg_type == this->current_status_msg_type_) {
|
||||||
this->set_state_(State::STATUS_UPDATED);
|
this->set_state_(State::STATUS_UPDATED);
|
||||||
}
|
}
|
||||||
|
|
||||||
return previous != this->status_ && this->is_status_initialized();
|
bool changed = previous.power_on != this->status_.power_on || previous.mode != this->status_.mode ||
|
||||||
|
previous.fan_mode != this->status_.fan_mode ||
|
||||||
|
previous.target_temperature != this->status_.target_temperature;
|
||||||
|
|
||||||
|
if (this->is_room_temperature_enabled()) {
|
||||||
|
changed |= previous.room_temperature != this->status_.room_temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
return changed && this->is_status_initialized();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) {
|
bool MitsubishiCN105::parse_status_payload_(uint8_t msg_type, const uint8_t *payload, size_t len) {
|
||||||
@@ -278,6 +263,9 @@ bool MitsubishiCN105::parse_status_settings_(const uint8_t *payload, size_t len)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const bool i_see = payload[3] > 0x08;
|
||||||
|
this->status_.mode = lookup(PROTOCOL_MODE_MAP, payload[3] - (i_see ? 0x08 : 0)).value_or(Mode::UNKNOWN);
|
||||||
|
this->status_.fan_mode = lookup(PROTOCOL_FAN_MODE_MAP, payload[5]).value_or(FanMode::UNKNOWN);
|
||||||
this->status_.power_on = payload[2] != 0;
|
this->status_.power_on = payload[2] != 0;
|
||||||
this->status_.target_temperature = decode_temperature(-payload[4], payload[10], 31);
|
this->status_.target_temperature = decode_temperature(-payload[4], payload[10], 31);
|
||||||
|
|
||||||
@@ -291,21 +279,11 @@ bool MitsubishiCN105::parse_status_room_temperature_(const uint8_t *payload, siz
|
|||||||
}
|
}
|
||||||
|
|
||||||
this->status_.room_temperature = decode_temperature(payload[2], payload[5], 10);
|
this->status_.room_temperature = decode_temperature(payload[2], payload[5], 10);
|
||||||
|
this->last_room_temperature_update_ms_ = get_loop_time_ms();
|
||||||
|
|
||||||
return true;
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
void MitsubishiCN105::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) {
|
|
||||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
|
||||||
char buf[format_hex_pretty_size(READ_BUFFER_SIZE)];
|
|
||||||
ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len));
|
|
||||||
#endif
|
|
||||||
}
|
|
||||||
|
|
||||||
const LogString *MitsubishiCN105::state_to_string(State state) {
|
const LogString *MitsubishiCN105::state_to_string(State state) {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case State::NOT_CONNECTED:
|
case State::NOT_CONNECTED:
|
||||||
@@ -328,4 +306,79 @@ const LogString *MitsubishiCN105::state_to_string(State state) {
|
|||||||
return LOG_STR("Unknown");
|
return LOG_STR("Unknown");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
template<typename Callback>
|
||||||
|
bool MitsubishiCN105::FrameParser::read_and_parse(uart::UARTDevice &device, Callback &&callback) {
|
||||||
|
uint8_t watchdog = 64;
|
||||||
|
while (device.available() > 0 && watchdog-- > 0) {
|
||||||
|
uint8_t &value = this->read_buffer_[this->read_pos_];
|
||||||
|
if (!device.read_byte(&value)) {
|
||||||
|
ESP_LOGW(TAG, "UART read failed while data available");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (++this->read_pos_) {
|
||||||
|
case 1:
|
||||||
|
if (value != PREAMBLE) {
|
||||||
|
this->reset_and_dump_buffer_("RX ignoring preamble");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case 3:
|
||||||
|
if (value != HEADER_BYTE_1) {
|
||||||
|
this->reset_and_dump_buffer_("RX invalid: header 1 mismatch");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case 4:
|
||||||
|
if (value != HEADER_BYTE_2) {
|
||||||
|
this->reset_and_dump_buffer_("RX invalid: header 2 mismatch");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
|
||||||
|
case HEADER_LEN:
|
||||||
|
static_assert(READ_BUFFER_SIZE > HEADER_LEN);
|
||||||
|
if (this->read_buffer_[HEADER_LEN - 1] >= READ_BUFFER_SIZE - HEADER_LEN) {
|
||||||
|
this->reset_and_dump_buffer_("RX invalid: payload too large");
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
const size_t len_without_checksum = HEADER_LEN + static_cast<size_t>(this->read_buffer_[HEADER_LEN - 1]);
|
||||||
|
if (this->read_pos_ <= len_without_checksum) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (checksum(this->read_buffer_, len_without_checksum) != value) {
|
||||||
|
this->reset_and_dump_buffer_("RX invalid: checksum mismatch");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
dump_buffer_vv("RX", this->read_buffer_, this->read_pos_);
|
||||||
|
const bool processed =
|
||||||
|
callback(this->read_buffer_[1], this->read_buffer_ + HEADER_LEN, len_without_checksum - HEADER_LEN);
|
||||||
|
this->read_pos_ = 0;
|
||||||
|
return processed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MitsubishiCN105::FrameParser::reset_and_dump_buffer_(const char *prefix) {
|
||||||
|
dump_buffer_vv(prefix, this->read_buffer_, this->read_pos_);
|
||||||
|
this->read_pos_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void MitsubishiCN105::FrameParser::dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len) {
|
||||||
|
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
|
||||||
|
char buf[format_hex_pretty_size(READ_BUFFER_SIZE)];
|
||||||
|
ESP_LOGVV(TAG, "%s (%zu): %s", prefix, len, format_hex_pretty_to(buf, data, len));
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace esphome::mitsubishi_cn105
|
} // namespace esphome::mitsubishi_cn105
|
||||||
|
|||||||
@@ -9,11 +9,30 @@ uint32_t get_loop_time_ms();
|
|||||||
|
|
||||||
class MitsubishiCN105 {
|
class MitsubishiCN105 {
|
||||||
public:
|
public:
|
||||||
struct Status {
|
enum class Mode : uint8_t {
|
||||||
bool operator==(const Status &) const = default;
|
HEAT,
|
||||||
|
DRY,
|
||||||
|
COOL,
|
||||||
|
FAN_ONLY,
|
||||||
|
AUTO,
|
||||||
|
UNKNOWN,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum class FanMode : uint8_t {
|
||||||
|
AUTO,
|
||||||
|
QUIET,
|
||||||
|
SPEED_1,
|
||||||
|
SPEED_2,
|
||||||
|
SPEED_3,
|
||||||
|
SPEED_4,
|
||||||
|
UNKNOWN,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Status {
|
||||||
bool power_on{false};
|
bool power_on{false};
|
||||||
float target_temperature{NAN};
|
float target_temperature{NAN};
|
||||||
|
Mode mode{Mode::UNKNOWN};
|
||||||
|
FanMode fan_mode{FanMode::UNKNOWN};
|
||||||
float room_temperature{NAN};
|
float room_temperature{NAN};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,8 +44,17 @@ class MitsubishiCN105 {
|
|||||||
uint32_t get_update_interval() const { return this->update_interval_ms_; }
|
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; }
|
void set_update_interval(uint32_t interval_ms) { this->update_interval_ms_ = interval_ms; }
|
||||||
|
|
||||||
|
uint32_t get_room_temperature_min_interval() const { return this->room_temperature_min_interval_ms_; }
|
||||||
|
bool is_room_temperature_enabled() const { return this->room_temperature_min_interval_ms_ != SCHEDULER_DONT_RUN; }
|
||||||
|
void set_room_temperature_min_interval(uint32_t interval_ms) {
|
||||||
|
this->room_temperature_min_interval_ms_ = interval_ms;
|
||||||
|
}
|
||||||
|
|
||||||
const Status &status() const { return this->status_; }
|
const Status &status() const { return this->status_; }
|
||||||
bool is_status_initialized() const { return !std::isnan(status_.room_temperature); }
|
bool is_status_initialized() const {
|
||||||
|
return this->is_room_temperature_enabled() ? !std::isnan(this->status_.room_temperature)
|
||||||
|
: !std::isnan(this->status_.target_temperature);
|
||||||
|
}
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
enum class State : uint8_t {
|
enum class State : uint8_t {
|
||||||
@@ -40,35 +68,46 @@ class MitsubishiCN105 {
|
|||||||
READ_TIMEOUT
|
READ_TIMEOUT
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class FrameParser {
|
||||||
|
public:
|
||||||
|
template<typename Callback> bool read_and_parse(uart::UARTDevice &device, Callback &&callback);
|
||||||
|
void reset() { read_pos_ = 0; }
|
||||||
|
static void dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len);
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void reset_and_dump_buffer_(const char *prefix);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static constexpr size_t READ_BUFFER_SIZE = 32;
|
||||||
|
uint8_t read_buffer_[READ_BUFFER_SIZE];
|
||||||
|
uint8_t read_pos_{0};
|
||||||
|
};
|
||||||
|
|
||||||
void set_state_(State new_state);
|
void set_state_(State new_state);
|
||||||
void did_transition_(State to);
|
void did_transition_(State to);
|
||||||
bool read_incoming_bytes_();
|
|
||||||
bool process_rx_packet_(uint8_t type, const uint8_t *payload, size_t len);
|
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 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_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_settings_(const uint8_t *payload, size_t len);
|
||||||
bool parse_status_room_temperature_(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 send_packet_(const uint8_t *packet, size_t len);
|
||||||
void update_status_();
|
void update_status_();
|
||||||
void cancel_waiting_and_transition_to_(State state);
|
void cancel_waiting_and_transition_to_(State state);
|
||||||
|
bool should_request_room_temperature_() const;
|
||||||
template<typename T> void send_packet_(const T &packet) { this->send_packet_(packet.data(), packet.size()); }
|
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 bool should_transition(State from, State to);
|
||||||
static const LogString *state_to_string(State state);
|
static const LogString *state_to_string(State state);
|
||||||
static void dump_buffer_vv(const char *prefix, const uint8_t *data, size_t len);
|
|
||||||
|
|
||||||
uart::UARTDevice &device_;
|
uart::UARTDevice &device_;
|
||||||
uint32_t update_interval_ms_{1000};
|
uint32_t update_interval_ms_{1000};
|
||||||
|
uint32_t room_temperature_min_interval_ms_{60000};
|
||||||
std::optional<uint32_t> write_timeout_start_ms_;
|
std::optional<uint32_t> write_timeout_start_ms_;
|
||||||
std::optional<uint32_t> status_update_start_ms_;
|
std::optional<uint32_t> status_update_start_ms_;
|
||||||
|
std::optional<uint32_t> last_room_temperature_update_ms_;
|
||||||
Status status_{};
|
Status status_{};
|
||||||
State state_{State::NOT_CONNECTED};
|
State state_{State::NOT_CONNECTED};
|
||||||
uint8_t status_msg_index_{0};
|
uint8_t current_status_msg_type_{0};
|
||||||
|
FrameParser frame_parser_;
|
||||||
private:
|
|
||||||
static constexpr size_t READ_BUFFER_SIZE = 32;
|
|
||||||
uint8_t read_buffer_[READ_BUFFER_SIZE];
|
|
||||||
uint8_t read_pos_{0};
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace esphome::mitsubishi_cn105
|
} // namespace esphome::mitsubishi_cn105
|
||||||
|
|||||||
@@ -6,8 +6,42 @@ namespace esphome::mitsubishi_cn105 {
|
|||||||
|
|
||||||
static const char *const TAG = "mitsubishi_cn105.climate";
|
static const char *const TAG = "mitsubishi_cn105.climate";
|
||||||
|
|
||||||
|
static constexpr std::array MODE_MAP{
|
||||||
|
std::pair{MitsubishiCN105::Mode::AUTO, climate::CLIMATE_MODE_AUTO},
|
||||||
|
std::pair{MitsubishiCN105::Mode::HEAT, climate::CLIMATE_MODE_HEAT},
|
||||||
|
std::pair{MitsubishiCN105::Mode::DRY, climate::CLIMATE_MODE_DRY},
|
||||||
|
std::pair{MitsubishiCN105::Mode::COOL, climate::CLIMATE_MODE_COOL},
|
||||||
|
std::pair{MitsubishiCN105::Mode::FAN_ONLY, climate::CLIMATE_MODE_FAN_ONLY},
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr std::array FAN_MODE_MAP{
|
||||||
|
std::pair{MitsubishiCN105::FanMode::AUTO, climate::CLIMATE_FAN_AUTO},
|
||||||
|
std::pair{MitsubishiCN105::FanMode::QUIET, climate::CLIMATE_FAN_QUIET},
|
||||||
|
std::pair{MitsubishiCN105::FanMode::SPEED_1, climate::CLIMATE_FAN_LOW},
|
||||||
|
std::pair{MitsubishiCN105::FanMode::SPEED_2, climate::CLIMATE_FAN_MEDIUM},
|
||||||
|
std::pair{MitsubishiCN105::FanMode::SPEED_3, climate::CLIMATE_FAN_MIDDLE},
|
||||||
|
std::pair{MitsubishiCN105::FanMode::SPEED_4, climate::CLIMATE_FAN_HIGH},
|
||||||
|
};
|
||||||
|
|
||||||
|
template<typename A, typename B, std::size_t N>
|
||||||
|
static bool map_lookup(const std::array<std::pair<A, B>, N> &map, A key, B &out) {
|
||||||
|
for (const auto &[from, to] : map) {
|
||||||
|
if (from == key) {
|
||||||
|
out = to;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
void MitsubishiCN105Climate::dump_config() {
|
void MitsubishiCN105Climate::dump_config() {
|
||||||
LOG_CLIMATE("", "Mitsubishi CN105 Climate", this);
|
LOG_CLIMATE("", "Mitsubishi CN105 Climate", this);
|
||||||
|
if (this->hp_.is_room_temperature_enabled()) {
|
||||||
|
ESP_LOGCONFIG(TAG, " Current temperature min interval: %" PRIu32 " ms",
|
||||||
|
this->hp_.get_room_temperature_min_interval());
|
||||||
|
} else {
|
||||||
|
ESP_LOGCONFIG(TAG, " Current temperature: disabled");
|
||||||
|
}
|
||||||
ESP_LOGCONFIG(TAG,
|
ESP_LOGCONFIG(TAG,
|
||||||
" Update interval: %" PRIu32 " ms\n"
|
" Update interval: %" PRIu32 " ms\n"
|
||||||
" UART: baud_rate=%" PRIu32 " data_bits=%u parity=%s stop_bits=%u",
|
" UART: baud_rate=%" PRIu32 " data_bits=%u parity=%s stop_bits=%u",
|
||||||
@@ -26,12 +60,32 @@ void MitsubishiCN105Climate::loop() {
|
|||||||
climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
climate::ClimateTraits MitsubishiCN105Climate::traits() {
|
||||||
climate::ClimateTraits traits;
|
climate::ClimateTraits traits;
|
||||||
|
|
||||||
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
traits.set_supported_modes({
|
||||||
|
climate::CLIMATE_MODE_OFF,
|
||||||
|
climate::CLIMATE_MODE_COOL,
|
||||||
|
climate::CLIMATE_MODE_HEAT,
|
||||||
|
climate::CLIMATE_MODE_DRY,
|
||||||
|
climate::CLIMATE_MODE_FAN_ONLY,
|
||||||
|
climate::CLIMATE_MODE_AUTO,
|
||||||
|
});
|
||||||
|
|
||||||
|
traits.set_supported_fan_modes({
|
||||||
|
climate::CLIMATE_FAN_AUTO,
|
||||||
|
climate::CLIMATE_FAN_QUIET,
|
||||||
|
climate::CLIMATE_FAN_LOW,
|
||||||
|
climate::CLIMATE_FAN_MEDIUM,
|
||||||
|
climate::CLIMATE_FAN_MIDDLE,
|
||||||
|
climate::CLIMATE_FAN_HIGH,
|
||||||
|
});
|
||||||
|
|
||||||
traits.set_visual_min_temperature(16.0f);
|
traits.set_visual_min_temperature(16.0f);
|
||||||
traits.set_visual_max_temperature(31.0f);
|
traits.set_visual_max_temperature(31.0f);
|
||||||
traits.set_visual_temperature_step(1.0f);
|
traits.set_visual_temperature_step(1.0f);
|
||||||
traits.set_visual_current_temperature_step(0.5f);
|
|
||||||
|
if (this->hp_.is_room_temperature_enabled()) {
|
||||||
|
traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
|
||||||
|
traits.set_visual_current_temperature_step(0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
return traits;
|
return traits;
|
||||||
}
|
}
|
||||||
@@ -42,7 +96,25 @@ void MitsubishiCN105Climate::apply_values_() {
|
|||||||
const auto &status = this->hp_.status();
|
const auto &status = this->hp_.status();
|
||||||
|
|
||||||
this->target_temperature = status.target_temperature;
|
this->target_temperature = status.target_temperature;
|
||||||
this->current_temperature = status.room_temperature;
|
|
||||||
|
if (this->hp_.is_room_temperature_enabled()) {
|
||||||
|
this->current_temperature = status.room_temperature;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (status.power_on) {
|
||||||
|
if (!map_lookup(MODE_MAP, status.mode, this->mode)) {
|
||||||
|
ESP_LOGD(TAG, "Unable to map mode");
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this->mode = climate::CLIMATE_MODE_OFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
climate::ClimateFanMode fan_mode;
|
||||||
|
if (map_lookup(FAN_MODE_MAP, status.fan_mode, fan_mode)) {
|
||||||
|
this->fan_mode = fan_mode;
|
||||||
|
} else {
|
||||||
|
ESP_LOGD(TAG, "Unable to map fan mode");
|
||||||
|
}
|
||||||
|
|
||||||
this->publish_state();
|
this->publish_state();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ class MitsubishiCN105Climate : public climate::Climate, public Component, public
|
|||||||
void control(const climate::ClimateCall &call) override;
|
void control(const climate::ClimateCall &call) override;
|
||||||
|
|
||||||
void set_update_interval(uint32_t ms) { hp_.set_update_interval(ms); }
|
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); }
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
void apply_values_();
|
void apply_values_();
|
||||||
|
|||||||
@@ -60,6 +60,8 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) {
|
|||||||
// Settings should still have initial values
|
// Settings should still have initial values
|
||||||
EXPECT_FALSE(ctx.sut.status().power_on);
|
EXPECT_FALSE(ctx.sut.status().power_on);
|
||||||
EXPECT_THAT(ctx.sut.status().target_temperature, ::testing::IsNan());
|
EXPECT_THAT(ctx.sut.status().target_temperature, ::testing::IsNan());
|
||||||
|
EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::UNKNOWN);
|
||||||
|
EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::UNKNOWN);
|
||||||
|
|
||||||
ctx.sut.set_current_time(300);
|
ctx.sut.set_current_time(300);
|
||||||
ASSERT_FALSE(ctx.sut.update());
|
ASSERT_FALSE(ctx.sut.update());
|
||||||
@@ -68,6 +70,8 @@ TEST(MitsubishiCN105Tests, ConnectAndUpdateStatus) {
|
|||||||
// Check settings that we just read from received package
|
// Check settings that we just read from received package
|
||||||
EXPECT_FALSE(ctx.sut.status().power_on);
|
EXPECT_FALSE(ctx.sut.status().power_on);
|
||||||
EXPECT_EQ(ctx.sut.status().target_temperature, 24.0f);
|
EXPECT_EQ(ctx.sut.status().target_temperature, 24.0f);
|
||||||
|
EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::AUTO);
|
||||||
|
EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::AUTO);
|
||||||
|
|
||||||
// Now fetch room temperature (0x03)
|
// Now fetch room temperature (0x03)
|
||||||
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS);
|
EXPECT_EQ(ctx.sut.state_, TestableMitsubishiCN105::State::UPDATING_STATUS);
|
||||||
@@ -260,24 +264,28 @@ TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedA) {
|
|||||||
auto ctx = TestContext{};
|
auto ctx = TestContext{};
|
||||||
|
|
||||||
ctx.uart.push_rx(
|
ctx.uart.push_rx(
|
||||||
{0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x01, 0x03, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x56});
|
{0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x01, 0x03, 0x05, 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x55});
|
||||||
|
|
||||||
ctx.sut.update();
|
ctx.sut.update();
|
||||||
|
|
||||||
EXPECT_TRUE(ctx.sut.status().power_on);
|
EXPECT_TRUE(ctx.sut.status().power_on);
|
||||||
EXPECT_EQ(ctx.sut.status().target_temperature, 26.0f);
|
EXPECT_EQ(ctx.sut.status().target_temperature, 26.0f);
|
||||||
|
EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::COOL);
|
||||||
|
EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::QUIET);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedB) {
|
TEST(MitsubishiCN105Tests, DecodeStatusSettingsPackageTempEncodedB) {
|
||||||
auto ctx = TestContext{};
|
auto ctx = TestContext{};
|
||||||
|
|
||||||
ctx.uart.push_rx(
|
ctx.uart.push_rx(
|
||||||
{0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xB7});
|
{0xFC, 0x62, 0x01, 0x30, 0x0C, 0x02, 0x00, 0x00, 0x00, 0x07, 0x00, 0x06, 0x00, 0x00, 0x00, 0x00, 0xA5, 0xAD});
|
||||||
|
|
||||||
ctx.sut.update();
|
ctx.sut.update();
|
||||||
|
|
||||||
EXPECT_FALSE(ctx.sut.status().power_on);
|
EXPECT_FALSE(ctx.sut.status().power_on);
|
||||||
EXPECT_EQ(ctx.sut.status().target_temperature, 18.5f);
|
EXPECT_EQ(ctx.sut.status().target_temperature, 18.5f);
|
||||||
|
EXPECT_EQ(ctx.sut.status().mode, TestableMitsubishiCN105::Mode::FAN_ONLY);
|
||||||
|
EXPECT_EQ(ctx.sut.status().fan_mode, TestableMitsubishiCN105::FanMode::SPEED_4);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedA) {
|
TEST(MitsubishiCN105Tests, DecodeStatusRoomTempPackageTempEncodedA) {
|
||||||
|
|||||||
Reference in New Issue
Block a user