Initial commit of core component

This commit is contained in:
Sammy1Am 2024-08-14 23:03:05 +00:00
parent 80a0f13722
commit 0bb5419851
23 changed files with 2860 additions and 0 deletions

View file

@ -253,6 +253,7 @@ esphome/components/mics_4514/* @jesserockz
esphome/components/midea/* @dudanov
esphome/components/midea_ir/* @dudanov
esphome/components/mitsubishi/* @RubyBailey
esphome/components/mitsubishi_itp/* @KazWolfe @Sammy1Am
esphome/components/mlx90393/* @functionpointer
esphome/components/mlx90614/* @jesserockz
esphome/components/mmc5603/* @benhoff

View file

@ -0,0 +1,39 @@
import esphome.codegen as cg
from esphome.components import climate
import esphome.config_validation as cv
from esphome.const import CONF_ID
CODEOWNERS = ["@Sammy1Am", "@KazWolfe"]
mitsubishi_itp_ns = cg.esphome_ns.namespace("mitsubishi_itp")
MitsubishiUART = mitsubishi_itp_ns.class_(
"MitsubishiUART", cg.PollingComponent, climate.Climate
)
CONF_MITSUBISHI_ITP_ID = "mitsubishi_itp_id"
def sensors_to_config_schema(sensors):
return cv.Schema(
{
cv.GenerateID(CONF_MITSUBISHI_ITP_ID): cv.use_id(MitsubishiUART),
}
).extend(
{
cv.Optional(sensor_designator): sensor_schema
for sensor_designator, sensor_schema in sensors.items()
}
)
async def sensors_to_code(config, sensors, registration_function):
mitp_component = await cg.get_variable(config[CONF_MITSUBISHI_ITP_ID])
# Sensors
for sensor_designator, _ in sensors.items():
if sensor_conf := config.get(sensor_designator):
sensor_component = cg.new_Pvariable(sensor_conf[CONF_ID])
await registration_function(sensor_component, sensor_conf)
cg.add(getattr(mitp_component, "register_listener")(sensor_component))

View file

@ -0,0 +1,126 @@
import esphome.codegen as cg
from esphome.components import climate, time, uart
import esphome.config_validation as cv
from esphome.const import (
CONF_CUSTOM_FAN_MODES,
CONF_ID,
CONF_SUPPORTED_FAN_MODES,
CONF_SUPPORTED_MODES,
CONF_TIME_ID,
)
from esphome.core import coroutine
from . import MitsubishiUART, mitsubishi_itp_ns
DEPENDENCIES = [
"uart",
]
CONF_UART_HEATPUMP = "uart_heatpump"
CONF_UART_THERMOSTAT = "uart_thermostat"
CONF_DISABLE_ACTIVE_MODE = "disable_active_mode"
CONF_ENHANCED_MHK_SUPPORT = (
"enhanced_mhk" # EXPERIMENTAL. Will be set to default eventually.
)
DEFAULT_POLLING_INTERVAL = "5s"
DEFAULT_CLIMATE_MODES = ["OFF", "HEAT", "DRY", "COOL", "FAN_ONLY", "HEAT_COOL"]
DEFAULT_FAN_MODES = ["AUTO", "QUIET", "LOW", "MEDIUM", "HIGH"]
CUSTOM_FAN_MODES = {"VERYHIGH": mitsubishi_itp_ns.FAN_MODE_VERYHIGH}
validate_custom_fan_modes = cv.enum(CUSTOM_FAN_MODES, upper=True)
CONFIG_SCHEMA = climate.CLIMATE_SCHEMA.extend(
{
cv.GenerateID(CONF_ID): cv.declare_id(MitsubishiUART),
cv.Required(CONF_UART_HEATPUMP): cv.use_id(uart.UARTComponent),
cv.Optional(CONF_UART_THERMOSTAT): cv.use_id(uart.UARTComponent),
cv.Optional(CONF_TIME_ID): cv.use_id(time.RealTimeClock),
cv.Optional(
CONF_SUPPORTED_MODES, default=DEFAULT_CLIMATE_MODES
): cv.ensure_list(climate.validate_climate_mode),
cv.Optional(
CONF_SUPPORTED_FAN_MODES, default=DEFAULT_FAN_MODES
): cv.ensure_list(climate.validate_climate_fan_mode),
cv.Optional(CONF_CUSTOM_FAN_MODES, default=["VERYHIGH"]): cv.ensure_list(
validate_custom_fan_modes
),
cv.Optional(CONF_DISABLE_ACTIVE_MODE, default=False): cv.boolean,
cv.Optional(CONF_ENHANCED_MHK_SUPPORT, default=False): cv.boolean,
}
).extend(cv.polling_component_schema(DEFAULT_POLLING_INTERVAL))
def final_validate(config):
schema = uart.final_validate_device_schema(
"mitsubishi_itp",
uart_bus=CONF_UART_HEATPUMP,
require_tx=True,
require_rx=True,
data_bits=8,
parity="EVEN",
stop_bits=1,
)
if CONF_UART_THERMOSTAT in config:
schema = schema.extend(
uart.final_validate_device_schema(
"mitsubishi_itp",
uart_bus=CONF_UART_THERMOSTAT,
require_tx=True,
require_rx=True,
data_bits=8,
parity="EVEN",
stop_bits=1,
)
)
schema(config)
FINAL_VALIDATE_SCHEMA = final_validate
@coroutine
async def to_code(config):
hp_uart_component = await cg.get_variable(config[CONF_UART_HEATPUMP])
mitp_component = cg.new_Pvariable(config[CONF_ID], hp_uart_component)
await cg.register_component(mitp_component, config)
await climate.register_climate(mitp_component, config)
# If thermostat defined
if CONF_UART_THERMOSTAT in config:
# Register thermostat with MITP
ts_uart_component = await cg.get_variable(config[CONF_UART_THERMOSTAT])
cg.add(getattr(mitp_component, "set_thermostat_uart")(ts_uart_component))
# If RTC defined
if CONF_TIME_ID in config:
rtc_component = await cg.get_variable(config[CONF_TIME_ID])
cg.add(getattr(mitp_component, "set_time_source")(rtc_component))
elif CONF_UART_THERMOSTAT in config and config.get(CONF_ENHANCED_MHK_SUPPORT):
raise cv.RequiredFieldInvalid(
f"{CONF_TIME_ID} is required if {CONF_ENHANCED_MHK_SUPPORT} is set."
)
# Traits
traits = mitp_component.config_traits()
if CONF_SUPPORTED_MODES in config:
cg.add(traits.set_supported_modes(config[CONF_SUPPORTED_MODES]))
if CONF_SUPPORTED_FAN_MODES in config:
cg.add(traits.set_supported_fan_modes(config[CONF_SUPPORTED_FAN_MODES]))
if CONF_CUSTOM_FAN_MODES in config:
cg.add(traits.set_supported_custom_fan_modes(config[CONF_CUSTOM_FAN_MODES]))
# Debug Settings
if dam_conf := config.get(CONF_DISABLE_ACTIVE_MODE):
cg.add(getattr(mitp_component, "set_active_mode")(not dam_conf))
if enhanced_mhk_protocol := config.get(CONF_ENHANCED_MHK_SUPPORT):
cg.add(
getattr(mitp_component, "set_enhanced_mhk_support")(enhanced_mhk_protocol)
)

View file

@ -0,0 +1,217 @@
#include "mitp_bridge.h"
namespace esphome {
namespace mitsubishi_itp {
MITPBridge::MITPBridge(uart::UARTComponent *uart_component, PacketProcessor *packet_processor)
: uart_comp_{*uart_component}, pkt_processor_{*packet_processor} {}
// The heatpump loop expects responses for most sent packets, so it tracks the last send packet and wait for a response
void HeatpumpBridge::loop() {
// Try to get a packet
if (optional<RawPacket> pkt = receive_raw_packet_(SourceBridge::HEATPUMP,
packet_awaiting_response_.has_value()
? packet_awaiting_response_.value().get_controller_association()
: ControllerAssociation::MITP)) {
ESP_LOGV(BRIDGE_TAG, "Parsing %x heatpump packet", pkt.value().get_packet_type());
// Check the packet's checksum and either process it, or log an error
if (pkt.value().is_checksum_valid()) {
// If we're waiting for a response, associate the incomming packet with the request packet
classify_and_process_raw_packet_(pkt.value());
} else {
ESP_LOGW(BRIDGE_TAG, "Invalid packet checksum!\n%s",
format_hex_pretty(&pkt.value().get_bytes()[0], pkt.value().get_length()).c_str());
}
// If there was a packet waiting for a response, remove it.
// TODO: This incoming packet wasn't *nessesarily* a response, but for now
// it's probably not worth checking to make sure it matches.
if (packet_awaiting_response_.has_value()) {
packet_awaiting_response_.reset();
}
} else if (!packet_awaiting_response_.has_value() && !pkt_queue_.empty()) {
// If we're not waiting for a response and there's a packet in the queue...
// If the packet expects a response, add it to the awaitingResponse variable
if (pkt_queue_.front().is_response_expected()) {
packet_awaiting_response_ = pkt_queue_.front();
}
ESP_LOGV(BRIDGE_TAG, "Sending to heatpump %s", pkt_queue_.front().to_string().c_str());
write_raw_packet_(pkt_queue_.front().raw_packet());
packet_sent_millis_ = millis();
// Remove packet from queue
pkt_queue_.pop();
} else if (packet_awaiting_response_.has_value() && (millis() - packet_sent_millis_ > RESPONSE_TIMEOUT_MS)) {
// We've been waiting too long for a response, give up
// TODO: We could potentially retry here, but that seems unnecessary
packet_awaiting_response_.reset();
ESP_LOGW(BRIDGE_TAG, "Timeout waiting for response to %x packet.",
packet_awaiting_response_.value().get_packet_type());
}
}
// The thermostat bridge loop doesn't expect any responses, so packets in queue are just sent without checking if they
// expect a response
void ThermostatBridge::loop() {
// Try to get a packet
if (optional<RawPacket> pkt = receive_raw_packet_(SourceBridge::THERMOSTAT, ControllerAssociation::THERMOSTAT)) {
ESP_LOGV(BRIDGE_TAG, "Parsing %x thermostat packet", pkt.value().get_packet_type());
// Check the packet's checksum and either process it, or log an error
if (pkt.value().is_checksum_valid()) {
classify_and_process_raw_packet_(pkt.value());
} else {
ESP_LOGW(BRIDGE_TAG, "Invalid packet checksum!\n%s",
format_hex_pretty(&pkt.value().get_bytes()[0], pkt.value().get_length()).c_str());
}
} else if (!pkt_queue_.empty()) {
// If there's a packet in the queue...
ESP_LOGV(BRIDGE_TAG, "Sending to thermostat %s", pkt_queue_.front().to_string().c_str());
write_raw_packet_(pkt_queue_.front().raw_packet());
packet_sent_millis_ = millis();
// Remove packet from queue
pkt_queue_.pop();
}
}
/* Queues a packet to be sent by the bridge. If the queue is full, the packet will not be
enqueued.*/
void MITPBridge::send_packet(const Packet &packet_to_send) {
if (pkt_queue_.size() <= MAX_QUEUE_SIZE) {
pkt_queue_.push(packet_to_send);
} else {
ESP_LOGW(BRIDGE_TAG, "Packet queue full! %x packet not sent.", packet_to_send.get_packet_type());
}
}
void MITPBridge::write_raw_packet_(const RawPacket &packet_to_send) const {
uart_comp_.write_array(packet_to_send.get_bytes(), packet_to_send.get_length());
}
/* Reads and deserializes a packet from UART.
Communication with heatpump is *slow*, so we need to check and make sure there are
enough packets available before we start reading. If there aren't enough packets,
no packet will be returned.
Even at 2400 baud, the 100ms readtimeout should be enough to read a whole payload
after the first byte has been received though, so currently we're assuming that once
the header is available, it's safe to call read_array without timing out and severing
the packet.
*/
optional<RawPacket> MITPBridge::receive_raw_packet_(const SourceBridge source_bridge,
const ControllerAssociation controller_association) const {
// TODO: Can we make the source_bridge and controller_association inherent to the class instead of passed as
// arguments?
uint8_t packet_bytes[PACKET_MAX_SIZE];
packet_bytes[0] = 0; // Reset control byte before starting
// Drain UART until we see a control byte (times out after 100ms in UARTComponent)
while (uart_comp_.available() >= PACKET_HEADER_SIZE && uart_comp_.read_byte(&packet_bytes[0])) {
if (packet_bytes[0] == BYTE_CONTROL)
break;
// TODO: If the serial is all garbage, this may never stop-- we should have our own timeout
}
// If we never found a control byte, we didn't receive a packet
if (packet_bytes[0] != BYTE_CONTROL) {
return nullopt;
}
// Read the header
uart_comp_.read_array(&packet_bytes[1], PACKET_HEADER_SIZE - 1);
// Read payload + checksum
uint8_t payload_size = packet_bytes[PACKET_HEADER_INDEX_PAYLOAD_LENGTH];
uart_comp_.read_array(&packet_bytes[PACKET_HEADER_SIZE], payload_size + 1);
return RawPacket(packet_bytes, PACKET_HEADER_SIZE + payload_size + 1, source_bridge, controller_association);
}
template<class P> void MITPBridge::process_raw_packet_(RawPacket &pkt, bool expect_response) const {
P packet = P(std::move(pkt));
packet.set_response_expected(expect_response);
pkt_processor_.process_packet(packet);
}
void MITPBridge::classify_and_process_raw_packet_(RawPacket &pkt) const {
// Figure out how to do this without a static_cast?
switch (static_cast<PacketType>(pkt.get_packet_type())) {
case PacketType::CONNECT_REQUEST:
process_raw_packet_<ConnectRequestPacket>(pkt, true);
break;
case PacketType::CONNECT_RESPONSE:
process_raw_packet_<ConnectResponsePacket>(pkt, false);
break;
case PacketType::IDENTIFY_REQUEST:
process_raw_packet_<CapabilitiesRequestPacket>(pkt, true);
break;
case PacketType::IDENTIFY_RESPONSE:
process_raw_packet_<CapabilitiesResponsePacket>(pkt, false);
break;
case PacketType::GET_REQUEST:
process_raw_packet_<GetRequestPacket>(pkt, true);
break;
case PacketType::GET_RESPONSE:
switch (static_cast<GetCommand>(pkt.get_command())) {
case GetCommand::SETTINGS:
process_raw_packet_<SettingsGetResponsePacket>(pkt, false);
break;
case GetCommand::CURRENT_TEMP:
process_raw_packet_<CurrentTempGetResponsePacket>(pkt, false);
break;
case GetCommand::ERROR_INFO:
process_raw_packet_<ErrorStateGetResponsePacket>(pkt, false);
break;
case GetCommand::RUN_STATE:
process_raw_packet_<RunStateGetResponsePacket>(pkt, false);
break;
case GetCommand::STATUS:
process_raw_packet_<StatusGetResponsePacket>(pkt, false);
break;
case GetCommand::THERMOSTAT_STATE_DOWNLOAD:
process_raw_packet_<ThermostatStateDownloadResponsePacket>(pkt, false);
break;
default:
process_raw_packet_<Packet>(pkt, false);
}
break;
case PacketType::SET_REQUEST:
switch (static_cast<SetCommand>(pkt.get_command())) {
case SetCommand::REMOTE_TEMPERATURE:
process_raw_packet_<RemoteTemperatureSetRequestPacket>(pkt, true);
break;
case SetCommand::SETTINGS:
process_raw_packet_<SettingsSetRequestPacket>(pkt, true);
break;
case SetCommand::THERMOSTAT_SENSOR_STATUS:
process_raw_packet_<ThermostatSensorStatusPacket>(pkt, true);
break;
case SetCommand::THERMOSTAT_HELLO:
process_raw_packet_<ThermostatHelloPacket>(pkt, false);
break;
case SetCommand::THERMOSTAT_STATE_UPLOAD:
process_raw_packet_<ThermostatStateUploadPacket>(pkt, true);
break;
case SetCommand::THERMOSTAT_SET_AA:
process_raw_packet_<ThermostatAASetRequestPacket>(pkt, true);
break;
default:
process_raw_packet_<Packet>(pkt, true);
}
break;
case PacketType::SET_RESPONSE:
process_raw_packet_<SetResponsePacket>(pkt, false);
break;
default:
process_raw_packet_<Packet>(pkt, true); // If we get an unknown packet from the thermostat, expect a response
}
}
} // namespace mitsubishi_itp
} // namespace esphome

View file

@ -0,0 +1,57 @@
#pragma once
#include "esphome/components/uart/uart.h"
#include "mitp_packet.h"
#include "queue"
namespace esphome {
namespace mitsubishi_itp {
static constexpr char BRIDGE_TAG[] = "mitp_bridge";
static const uint32_t RESPONSE_TIMEOUT_MS = 3000; // Maximum amount of time to wait for an expected response packet
/* Maximum number of packets allowed to be queued for sending. In some circumstances the equipment response
time can be very slow and packets would queue up faster than they were being received. TODO: Not sure what size this
should be, 4ish should be enough for almost all situations, so 8 seems plenty.*/
static const size_t MAX_QUEUE_SIZE = 8;
// A UARTComponent wrapper to send and receieve packets
class MITPBridge {
public:
MITPBridge(uart::UARTComponent *uart_component, PacketProcessor *packet_processor);
// Enqueues a packet to be sent
void send_packet(const Packet &packet_to_send);
// Checks for incoming packets, processes them, sends queued packets
virtual void loop() = 0;
protected:
optional<RawPacket> receive_raw_packet_(SourceBridge source_bridge,
ControllerAssociation controller_association) const;
void write_raw_packet_(const RawPacket &packet_to_send) const;
template<class P> void process_raw_packet_(RawPacket &pkt, bool expect_response = true) const;
void classify_and_process_raw_packet_(RawPacket &pkt) const;
uart::UARTComponent &uart_comp_;
PacketProcessor &pkt_processor_;
std::queue<Packet> pkt_queue_;
optional<Packet> packet_awaiting_response_ = nullopt;
uint32_t packet_sent_millis_;
};
class HeatpumpBridge : public MITPBridge {
public:
using MITPBridge::MITPBridge;
void loop() override;
};
class ThermostatBridge : public MITPBridge {
public:
using MITPBridge::MITPBridge;
// ThermostatBridge(uart::UARTComponent &uart_component, PacketProcessor &packet_processor) :
// MITPBridge(uart_component, packet_processor){};
void loop() override;
};
} // namespace mitsubishi_itp
} // namespace esphome

View file

@ -0,0 +1,21 @@
#pragma once
#include "mitp_packet.h"
namespace esphome {
namespace mitsubishi_itp {
static constexpr char LISTENER_TAG[] = "mitsubishi_itp.listener";
class MITPListener : public PacketProcessor {
public:
virtual void publish() = 0; // Publish only if the underlying state has changed
// TODO: These trhee are only used by the TemperatureSourceSelect, so might need to be broken out (putting them here
// now to get things working)
virtual void setup(bool thermostat_is_present){}; // Called during hub-component setup();
virtual void temperature_source_change(const std::string &temp_source){};
};
} // namespace mitsubishi_itp
} // namespace esphome

View file

@ -0,0 +1,15 @@
#pragma once
#include <cmath>
namespace esphome {
namespace mitsubishi_itp {
/// A struct that represents the connected MHK's state for management and synchronization purposes.
struct MHKState {
float cool_setpoint_ = NAN;
float heat_setpoint_ = NAN;
};
} // namespace mitsubishi_itp
} // namespace esphome

View file

@ -0,0 +1,463 @@
#include "mitp_packet.h"
#include "mitp_utils.h"
#include "mitsubishi_itp.h"
#include "esphome/core/datatypes.h"
namespace esphome {
namespace mitsubishi_itp {
// Packet to_strings()
std::string ConnectRequestPacket::to_string() const { return ("Connect Request: " + Packet::to_string()); }
std::string ConnectResponsePacket::to_string() const { return ("Connect Response: " + Packet::to_string()); }
std::string CapabilitiesResponsePacket::to_string() const {
return (
"Identify Base Capabilities Response: " + Packet::to_string() + CONSOLE_COLOR_PURPLE +
"\n HeatDisabled:" + (is_heat_disabled() ? "Yes" : "No") + " SupportsVane:" + (supports_vane() ? "Yes" : "No") +
" SupportsVaneSwing:" + (supports_vane_swing() ? "Yes" : "No")
+ " DryDisabled:" + (is_dry_disabled() ? "Yes" : "No") + " FanDisabled:" + (is_fan_disabled() ? "Yes" : "No") +
" ExtTempRange:" + (has_extended_temperature_range() ? "Yes" : "No") +
" AutoFanDisabled:" + (auto_fan_speed_disabled() ? "Yes" : "No") +
" InstallerSettings:" + (supports_installer_settings() ? "Yes" : "No") +
" TestMode:" + (supports_test_mode() ? "Yes" : "No") + " DryTemp:" + (supports_dry_temperature() ? "Yes" : "No")
+ " StatusDisplay:" + (has_status_display() ? "Yes" : "No")
+ "\n CoolDrySetpoint:" + std::to_string(get_min_cool_dry_setpoint()) + "/" +
std::to_string(get_max_cool_dry_setpoint()) + " HeatSetpoint:" + std::to_string(get_min_heating_setpoint()) +
"/" + std::to_string(get_max_heating_setpoint()) + " AutoSetpoint:" + std::to_string(get_min_auto_setpoint()) +
"/" + std::to_string(get_max_auto_setpoint()) + " FanSpeeds:" + std::to_string(get_supported_fan_speeds()));
}
std::string IdentifyCDResponsePacket::to_string() const { return "Identify CD Response: " + Packet::to_string(); }
std::string CurrentTempGetResponsePacket::to_string() const {
return ("Current Temp Response: " + Packet::to_string() + CONSOLE_COLOR_PURPLE +
"\n Temp:" + std::to_string(get_current_temp()) +
" Outdoor:" + (std::isnan(get_outdoor_temp()) ? "Unsupported" : std::to_string(get_outdoor_temp())));
}
std::string SettingsGetResponsePacket::to_string() const {
return ("Settings Response: " + Packet::to_string() + CONSOLE_COLOR_PURPLE + "\n Fan:" + format_hex(get_fan()) +
" Mode:" + format_hex(get_mode()) + " Power:" +
(get_power() == 3 ? "Test"
: get_power() > 0 ? "On"
: "Off") +
" TargetTemp:" + std::to_string(get_target_temp()) + " Vane:" + format_hex(get_vane()) +
" HVane:" + format_hex(get_horizontal_vane()) + (get_horizontal_vane_msb() ? " (MSB Set)" : "") +
"\n PowerLock:" + (locked_power() ? "Yes" : "No") + " ModeLock:" + (locked_mode() ? "Yes" : "No") +
" TempLock:" + (locked_temp() ? "Yes" : "No"));
}
std::string RunStateGetResponsePacket::to_string() const {
return ("RunState Response: " + Packet::to_string() + CONSOLE_COLOR_PURPLE +
"\n ServiceFilter:" + (service_filter() ? "Yes" : "No") + " Defrost:" + (in_defrost() ? "Yes" : "No") +
" Preheat:" + (in_preheat() ? "Yes" : "No") + " Standby:" + (in_standby() ? "Yes" : "No") +
" ActualFan:" + ACTUAL_FAN_SPEED_NAMES[get_actual_fan_speed()] + " (" +
std::to_string(get_actual_fan_speed()) + ")" + " AutoMode:" + format_hex(get_auto_mode()));
}
std::string StatusGetResponsePacket::to_string() const {
return ("Status Response: " + Packet::to_string() + CONSOLE_COLOR_PURPLE + "\n CompressorFrequency: " +
std::to_string(get_compressor_frequency()) + " Operating: " + (get_operating() ? "Yes" : "No"));
}
std::string ErrorStateGetResponsePacket::to_string() const {
return ("Error State Response: " + Packet::to_string() + CONSOLE_COLOR_PURPLE +
"\n Error State: " + (error_present() ? "Yes" : "No") + " ErrorCode: " + format_hex(get_error_code()) +
" ShortCode: " + get_short_code() + "(" + format_hex(get_raw_short_code()) + ")");
}
std::string RemoteTemperatureSetRequestPacket::to_string() const {
return ("Remote Temp Set Request: " + Packet::to_string() + CONSOLE_COLOR_PURPLE +
"\n Temp:" + std::to_string(get_remote_temperature()));
}
std::string ThermostatSensorStatusPacket::to_string() const {
return ("Thermostat Sensor Status: " + Packet::to_string() + CONSOLE_COLOR_PURPLE +
"\n Indoor RH: " + std::to_string(get_indoor_humidity_percent()) + "%" +
" MHK Battery: " + THERMOSTAT_BATTERY_STATE_NAMES[get_thermostat_battery_state()] + "(" +
std::to_string(get_thermostat_battery_state()) + ")" +
" Sensor Flags: " + std::to_string(get_sensor_flags()));
}
std::string ThermostatHelloPacket::to_string() const {
return ("Thermostat Hello: " + Packet::to_string() + CONSOLE_COLOR_PURPLE + "\n Model: " + get_thermostat_model() +
" Serial: " + get_thermostat_serial() + " Version: " + get_thermostat_version_string());
}
std::string ThermostatStateUploadPacket::to_string() const {
uint8_t flags = get_flags();
std::string result =
"Thermostat Sync " + Packet::to_string() + CONSOLE_COLOR_PURPLE + "\n Flags: " + format_hex(flags) + " =>";
if (flags & TSSF_TIMESTAMP) {
ESPTime timestamp{};
get_thermostat_timestamp(&timestamp);
result += " TS Time: " + timestamp.strftime("%Y-%m-%d %H:%M:%S");
}
if (flags & TSSF_AUTO_MODE)
result += " AutoMode: " + std::to_string(get_auto_mode());
if (flags & TSSF_HEAT_SETPOINT)
result += " HeatSetpoint: " + std::to_string(get_heat_setpoint());
if (flags & TSSF_COOL_SETPOINT)
result += " CoolSetpoint: " + std::to_string(get_cool_setpoint());
return result;
}
std::string GetRequestPacket::to_string() const {
return ("Get Request: " + Packet::to_string() + CONSOLE_COLOR_PURPLE +
"\n CommandID: " + format_hex((uint8_t) get_requested_command()));
}
std::string SettingsSetRequestPacket::to_string() const {
uint8_t flags = get_flags();
uint8_t flags2 = get_flags_2();
std::string result = "Settings Set Request: " + Packet::to_string() + CONSOLE_COLOR_PURPLE +
"\n Flags: " + format_hex(flags2) + format_hex(flags) + " =>";
if (flags & SettingFlag::SF_POWER)
result += " Power: " + std::to_string(get_power());
if (flags & SettingFlag::SF_MODE)
result += " Mode: " + std::to_string(get_mode());
if (flags & SettingFlag::SF_TARGET_TEMPERATURE)
result += " TargetTemp: " + std::to_string(get_target_temp());
if (flags & SettingFlag::SF_FAN)
result += " Fan: " + std::to_string(get_fan());
if (flags & SettingFlag::SF_VANE)
result += " Vane: " + std::to_string(get_vane());
if (flags2 & SettingFlag2::SF2_HORIZONTAL_VANE)
result += " HVane: " + std::to_string(get_horizontal_vane()) + (get_horizontal_vane_msb() ? " (MSB Set)" : "");
return result;
}
// TODO: Are there function implementations for packets in the .h file? (Yes) Should they be here?
// SettingsSetRequestPacket functions
void SettingsSetRequestPacket::add_settings_flag_(const SettingFlag flag_to_add) { add_flag(flag_to_add); }
void SettingsSetRequestPacket::add_settings_flag2_(const SettingFlag2 flag2_to_add) { add_flag2(flag2_to_add); }
SettingsSetRequestPacket &SettingsSetRequestPacket::set_power(const bool is_on) {
pkt_.set_payload_byte(PLINDEX_POWER, is_on ? 0x01 : 0x00);
add_settings_flag_(SF_POWER);
return *this;
}
SettingsSetRequestPacket &SettingsSetRequestPacket::set_mode(const ModeByte mode) {
pkt_.set_payload_byte(PLINDEX_MODE, mode);
add_settings_flag_(SF_MODE);
return *this;
}
SettingsSetRequestPacket &SettingsSetRequestPacket::set_target_temperature(const float temperature_degrees_c) {
if (temperature_degrees_c < 63.5 && temperature_degrees_c > -64.0) {
pkt_.set_payload_byte(PLINDEX_TARGET_TEMPERATURE, MITPUtils::deg_c_to_temp_scale_a(temperature_degrees_c));
pkt_.set_payload_byte(PLINDEX_TARGET_TEMPERATURE_CODE,
MITPUtils::deg_c_to_legacy_target_temp(temperature_degrees_c));
// TODO: while spawning a warning here is fine, we should (a) only actually send that warning if the system can't
// support this setpoint, and (b) clamp the setpoint to the known-acceptable values.
// The utility class will already clamp this for us, so we only need to worry about the warning.
if (temperature_degrees_c < 16 || temperature_degrees_c > 31.5) {
ESP_LOGW(PTAG, "Target temp %f is out of range for the legacy temp scale. This may be a problem on older units.",
temperature_degrees_c);
}
add_settings_flag_(SF_TARGET_TEMPERATURE);
} else {
ESP_LOGW(PTAG, "Target temp %f is outside valid range - target temperature not set!", temperature_degrees_c);
}
return *this;
}
SettingsSetRequestPacket &SettingsSetRequestPacket::set_fan(const FanByte fan) {
pkt_.set_payload_byte(PLINDEX_FAN, fan);
add_settings_flag_(SF_FAN);
return *this;
}
SettingsSetRequestPacket &SettingsSetRequestPacket::set_vane(const VaneByte vane) {
pkt_.set_payload_byte(PLINDEX_VANE, vane);
add_settings_flag_(SF_VANE);
return *this;
}
SettingsSetRequestPacket &SettingsSetRequestPacket::set_horizontal_vane(const HorizontalVaneByte horizontal_vane) {
pkt_.set_payload_byte(PLINDEX_HORIZONTAL_VANE, horizontal_vane);
add_settings_flag2_(SF2_HORIZONTAL_VANE);
return *this;
}
float SettingsSetRequestPacket::get_target_temp() const {
uint8_t enhanced_raw_temp = pkt_.get_payload_byte(PLINDEX_TARGET_TEMPERATURE);
if (enhanced_raw_temp == 0x00) {
uint8_t legacy_raw_temp = pkt_.get_payload_byte(PLINDEX_TARGET_TEMPERATURE_CODE);
return MITPUtils::legacy_target_temp_to_deg_c(legacy_raw_temp);
}
return MITPUtils::temp_scale_a_to_deg_c(enhanced_raw_temp);
}
// SettingsGetResponsePacket functions
float SettingsGetResponsePacket::get_target_temp() const {
uint8_t enhanced_raw_temp = pkt_.get_payload_byte(PLINDEX_TARGETTEMP);
if (enhanced_raw_temp == 0x00) {
uint8_t legacy_raw_temp = pkt_.get_payload_byte(PLINDEX_TARGETTEMP_LEGACY);
return MITPUtils::legacy_target_temp_to_deg_c(legacy_raw_temp);
}
return MITPUtils::temp_scale_a_to_deg_c(enhanced_raw_temp);
}
bool SettingsGetResponsePacket::is_i_see_enabled() const {
uint8_t mode = pkt_.get_payload_byte(PLINDEX_MODE);
// so far only modes 0x09 to 0x11 are known to be i-see.
// Mode 0x08 technically *can* be, but it's not a guarantee by itself.
return (mode >= 0x09 && mode <= 0x11);
}
// RemoteTemperatureSetRequestPacket functions
float RemoteTemperatureSetRequestPacket::get_remote_temperature() const {
uint8_t raw_temp_a = pkt_.get_payload_byte(PLINDEX_REMOTE_TEMPERATURE);
if (raw_temp_a == 0) {
uint8_t raw_temp_legacy = pkt_.get_payload_byte(PLINDEX_LEGACY_REMOTE_TEMPERATURE);
return MITPUtils::legacy_room_temp_to_deg_c(raw_temp_legacy);
}
return MITPUtils::temp_scale_a_to_deg_c(raw_temp_a);
}
RemoteTemperatureSetRequestPacket &RemoteTemperatureSetRequestPacket::set_remote_temperature(
float temperature_degrees_c) {
if (temperature_degrees_c < 63.5 && temperature_degrees_c > -64.0) {
pkt_.set_payload_byte(PLINDEX_REMOTE_TEMPERATURE, MITPUtils::deg_c_to_temp_scale_a(temperature_degrees_c));
pkt_.set_payload_byte(PLINDEX_LEGACY_REMOTE_TEMPERATURE,
MITPUtils::deg_c_to_legacy_room_temp(temperature_degrees_c));
set_flags(0x01); // Set flags to say we're providing the temperature
} else {
ESP_LOGW(PTAG, "Remote temp %f is outside valid range.", temperature_degrees_c);
}
return *this;
}
RemoteTemperatureSetRequestPacket &RemoteTemperatureSetRequestPacket::use_internal_temperature() {
set_flags(0x00); // Set flags to say to use internal temperature
return *this;
}
// SettingsSetRunStatusPacket functions
SetRunStatePacket &SetRunStatePacket::set_filter_reset(bool do_reset) {
pkt_.set_payload_byte(PLINDEX_FILTER_RESET, do_reset ? 1 : 0);
set_flags(0x01);
return *this;
}
// CurrentTempGetResponsePacket functions
float CurrentTempGetResponsePacket::get_current_temp() const {
uint8_t enhanced_raw_temp = pkt_.get_payload_byte(PLINDEX_CURRENTTEMP);
// TODO: Figure out how to handle "out of range" issues here.
if (enhanced_raw_temp == 0) {
uint8_t legacy_raw_temp = pkt_.get_payload_byte(PLINDEX_CURRENTTEMP_LEGACY);
return MITPUtils::legacy_room_temp_to_deg_c(legacy_raw_temp);
}
return MITPUtils::temp_scale_a_to_deg_c(enhanced_raw_temp);
}
float CurrentTempGetResponsePacket::get_outdoor_temp() const {
uint8_t enhanced_raw_temp = pkt_.get_payload_byte(PLINDEX_OUTDOORTEMP);
// Return NAN if unsupported
return enhanced_raw_temp == 0 ? NAN : MITPUtils::temp_scale_a_to_deg_c(enhanced_raw_temp);
}
// ThermostatHelloPacket functions
std::string ThermostatHelloPacket::get_thermostat_model() const {
return MITPUtils::decode_n_bit_string((pkt_.get_payload_bytes(1)), 4, 6);
}
std::string ThermostatHelloPacket::get_thermostat_serial() const {
return MITPUtils::decode_n_bit_string((pkt_.get_payload_bytes(4)), 12, 6);
}
std::string ThermostatHelloPacket::get_thermostat_version_string() const {
char buf[16];
sprintf(buf, "%02d.%02d.%02d", pkt_.get_payload_byte(13), pkt_.get_payload_byte(14), pkt_.get_payload_byte(15));
return buf;
}
// ThermostatStateUploadPacket functions
time_t ThermostatStateUploadPacket::get_thermostat_timestamp(esphome::ESPTime *out_timestamp) const {
int32_be_t magic;
std::memcpy(&magic, pkt_.get_payload_bytes(PLINDEX_THERMOSTAT_TIMESTAMP), 4);
out_timestamp->second = magic & 63;
out_timestamp->minute = (magic >> 6) & 63;
out_timestamp->hour = (magic >> 12) & 31;
out_timestamp->day_of_month = (magic >> 17) & 31;
out_timestamp->month = (magic >> 22) & 15;
out_timestamp->year = (magic >> 26) + 2017;
out_timestamp->recalc_timestamp_local();
return out_timestamp->timestamp;
}
uint8_t ThermostatStateUploadPacket::get_auto_mode() const { return pkt_.get_payload_byte(PLINDEX_AUTO_MODE); }
float ThermostatStateUploadPacket::get_heat_setpoint() const {
uint8_t enhanced_raw_temp = pkt_.get_payload_byte(PLINDEX_HEAT_SETPOINT);
return MITPUtils::temp_scale_a_to_deg_c(enhanced_raw_temp);
}
float ThermostatStateUploadPacket::get_cool_setpoint() const {
uint8_t enhanced_raw_temp = pkt_.get_payload_byte(PLINDEX_COOL_SETPOINT);
return MITPUtils::temp_scale_a_to_deg_c(enhanced_raw_temp);
}
// ThermostatStateDownloadResponsePacket functions
ThermostatStateDownloadResponsePacket &ThermostatStateDownloadResponsePacket::set_timestamp(esphome::ESPTime ts) {
int32_t encoded_timestamp = ((ts.year - 2017) << 26) | (ts.month << 22) | (ts.day_of_month << 17) | (ts.hour << 12) |
(ts.minute << 6) | (ts.second);
int32_t swapped_timestamp = byteswap(encoded_timestamp);
pkt_.set_payload_bytes(PLINDEX_ADAPTER_TIMESTAMP, &swapped_timestamp, 4);
pkt_.set_payload_byte(10, 0x07); // ???
return *this;
}
ThermostatStateDownloadResponsePacket &ThermostatStateDownloadResponsePacket::set_auto_mode(bool is_auto) {
pkt_.set_payload_byte(PLINDEX_AUTO_MODE, is_auto ? 0x01 : 0x00);
return *this;
}
ThermostatStateDownloadResponsePacket &ThermostatStateDownloadResponsePacket::set_heat_setpoint(float high_temp) {
uint8_t temp_a = high_temp != NAN ? MITPUtils::deg_c_to_temp_scale_a(high_temp) : 0x00;
pkt_.set_payload_byte(PLINDEX_HEAT_SETPOINT, temp_a);
return *this;
}
ThermostatStateDownloadResponsePacket &ThermostatStateDownloadResponsePacket::set_cool_setpoint(float low_temp) {
uint8_t temp_a = low_temp != NAN ? MITPUtils::deg_c_to_temp_scale_a(low_temp) : 0x00;
pkt_.set_payload_byte(PLINDEX_COOL_SETPOINT, temp_a);
return *this;
}
// ErrorStateGetResponsePacket functions
std::string ErrorStateGetResponsePacket::get_short_code() const {
const char *upper_alphabet = "AbEFJLPU";
const char *lower_alphabet = "0123456789ABCDEFOHJLPU";
const uint8_t error_code = this->get_raw_short_code();
uint8_t low_bits = error_code & 0x1F;
if (low_bits > 0x15) {
char buf[7];
sprintf(buf, "ERR_%x", error_code);
return buf;
}
return {upper_alphabet[(error_code & 0xE0) >> 5], lower_alphabet[low_bits]};
}
// CapabilitiesResponsePacket functions
uint8_t CapabilitiesResponsePacket::get_supported_fan_speeds() const {
uint8_t raw_value = ((pkt_.get_payload_byte(7) & 0x10) >> 2) + ((pkt_.get_payload_byte(8) & 0x08) >> 2) +
((pkt_.get_payload_byte(9) & 0x02) >> 1);
switch (raw_value) {
case 1:
case 2:
case 4:
return raw_value;
case 0:
return 3;
case 6:
return 5;
default:
ESP_LOGW(PACKETS_TAG, "Unexpected supported fan speeds: %i", raw_value);
return 0; // TODO: Depending on how this is used, it might be more useful to just return 3 and hope for the best
}
}
climate::ClimateTraits CapabilitiesResponsePacket::as_traits() const {
auto ct = climate::ClimateTraits();
// always enabled
ct.add_supported_mode(climate::CLIMATE_MODE_COOL);
ct.add_supported_mode(climate::CLIMATE_MODE_OFF);
if (!this->is_heat_disabled())
ct.add_supported_mode(climate::CLIMATE_MODE_HEAT);
if (!this->is_dry_disabled())
ct.add_supported_mode(climate::CLIMATE_MODE_DRY);
if (!this->is_fan_disabled())
ct.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY);
if (this->supports_vane_swing()) {
ct.add_supported_swing_mode(climate::CLIMATE_SWING_OFF);
if (this->supports_vane() && this->supports_h_vane())
ct.add_supported_swing_mode(climate::CLIMATE_SWING_BOTH);
if (this->supports_vane())
ct.add_supported_swing_mode(climate::CLIMATE_SWING_VERTICAL);
if (this->supports_h_vane())
ct.add_supported_swing_mode(climate::CLIMATE_SWING_HORIZONTAL);
}
ct.set_visual_min_temperature(std::min(this->get_min_cool_dry_setpoint(), this->get_min_heating_setpoint()));
ct.set_visual_max_temperature(std::max(this->get_max_cool_dry_setpoint(), this->get_max_heating_setpoint()));
// TODO: Figure out what these states *actually* map to so we aren't sending bad data.
// This is probably a dynamic map, so the setter will need to be aware of things.
switch (this->get_supported_fan_speeds()) {
case 1:
ct.set_supported_fan_modes({climate::CLIMATE_FAN_HIGH});
break;
case 2:
ct.set_supported_fan_modes({climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_HIGH});
break;
case 3:
ct.set_supported_fan_modes({climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
break;
case 4:
ct.set_supported_fan_modes({
climate::CLIMATE_FAN_QUIET,
climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH,
});
break;
case 5:
ct.set_supported_fan_modes({
climate::CLIMATE_FAN_QUIET,
climate::CLIMATE_FAN_LOW,
climate::CLIMATE_FAN_MEDIUM,
climate::CLIMATE_FAN_HIGH,
});
ct.add_supported_custom_fan_mode("Very High");
break;
default:
// no-op, don't set a fan mode.
break;
}
if (!this->auto_fan_speed_disabled())
ct.add_supported_fan_mode(climate::CLIMATE_FAN_AUTO);
return ct;
}
} // namespace mitsubishi_itp
} // namespace esphome

View file

@ -0,0 +1,77 @@
#include "mitp_packet.h"
namespace esphome {
namespace mitsubishi_itp {
// Creates an empty packet
Packet::Packet() {
// TODO: Is this okay?
}
// std::string Packet::to_string() const {
// return format_hex_pretty(&pkt_.getBytes()[0], pkt_.getLength());
// }
static char format_hex_pretty_char(uint8_t v) { return v >= 10 ? 'A' + (v - 10) : '0' + v; }
std::string Packet::to_string() const {
// Based on `format_hex_pretty` from ESPHome
if (pkt_.get_length() < PACKET_HEADER_SIZE)
return "";
std::stringstream stream;
stream << CONSOLE_COLOR_CYAN; // Cyan
stream << '[';
for (size_t i = 0; i < PACKET_HEADER_SIZE; i++) {
if (i == 1) {
stream << CONSOLE_COLOR_CYAN_BOLD;
}
stream << format_hex_pretty_char((pkt_.get_bytes()[i] & 0xF0) >> 4);
stream << format_hex_pretty_char(pkt_.get_bytes()[i] & 0x0F);
if (i < PACKET_HEADER_SIZE - 1) {
stream << '.';
}
if (i == 1) {
stream << CONSOLE_COLOR_CYAN;
}
}
// Header close-bracket
stream << ']';
stream << CONSOLE_COLOR_WHITE; // White
// Payload
for (size_t i = PACKET_HEADER_SIZE; i < pkt_.get_length() - 1; i++) {
stream << format_hex_pretty_char((pkt_.get_bytes()[i] & 0xF0) >> 4);
stream << format_hex_pretty_char(pkt_.get_bytes()[i] & 0x0F);
if (i < pkt_.get_length() - 2) {
stream << '.';
}
}
// Space
stream << ' ';
stream << CONSOLE_COLOR_GREEN; // Green
// Checksum
stream << format_hex_pretty_char((pkt_.get_bytes()[pkt_.get_length() - 1] & 0xF0) >> 4);
stream << format_hex_pretty_char(pkt_.get_bytes()[pkt_.get_length() - 1] & 0x0F);
stream << CONSOLE_COLOR_NONE; // Reset
return stream.str();
}
void Packet::set_flags(const uint8_t flag_value) { pkt_.set_payload_byte(PLINDEX_FLAGS, flag_value); }
// Adds a flag (ONLY APPLICABLE FOR SOME COMMANDS)
void Packet::add_flag(const uint8_t flag_to_add) {
pkt_.set_payload_byte(PLINDEX_FLAGS, pkt_.get_payload_byte(PLINDEX_FLAGS) | flag_to_add);
}
// Adds a flag2 (ONLY APPLICABLE FOR SOME COMMANDS)
void Packet::add_flag2(const uint8_t flag2_to_add) {
pkt_.set_payload_byte(PLINDEX_FLAGS2, pkt_.get_payload_byte(PLINDEX_FLAGS2) | flag2_to_add);
}
} // namespace mitsubishi_itp
} // namespace esphome

View file

@ -0,0 +1,573 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
#include "mitp_rawpacket.h"
#include "mitp_utils.h"
#include "esphome/core/time.h"
#include <sstream>
namespace esphome {
namespace mitsubishi_itp {
static constexpr char PACKETS_TAG[] = "mitsubishi_itp.packets";
#define CONSOLE_COLOR_NONE "\033[0m"
#define CONSOLE_COLOR_GREEN "\033[0;32m"
#define CONSOLE_COLOR_PURPLE "\033[0;35m"
#define CONSOLE_COLOR_CYAN "\033[0;36m"
#define CONSOLE_COLOR_CYAN_BOLD "\033[1;36m"
#define CONSOLE_COLOR_WHITE "\033[0;37m"
// Defined as constant for use as a Custom Fan Mode
const std::string FAN_MODE_VERYHIGH = "Very High";
// These are named to match with set fan speeds where possible. "Very Low" is a special speed
// for e.g. preheating or thermal off.
const std::array<std::string, 7> ACTUAL_FAN_SPEED_NAMES = {"Off", "Very Low", "Low", "Medium",
"High", FAN_MODE_VERYHIGH, "Quiet"};
const std::array<std::string, 5> THERMOSTAT_BATTERY_STATE_NAMES = {"OK", "Low", "Critical", "Replace", "Unknown"};
class PacketProcessor;
// Generic Base Packet wrapper over RawPacket
class Packet {
public:
Packet(RawPacket &&pkt) : pkt_(pkt){}; // TODO: Confirm this needs std::move if call to constructor ALSO has move
Packet(); // For optional<> construction
// Returns a (more) human-readable string of the packet
virtual std::string to_string() const;
// Is a response packet expected when this packet is sent. Defaults to true since
// most requests receive a response.
bool is_response_expected() const { return response_expected_; };
void set_response_expected(bool expect_response) { response_expected_ = expect_response; };
// Passthrough methods to RawPacket
RawPacket &raw_packet() { return pkt_; };
uint8_t get_packet_type() const { return pkt_.get_packet_type(); }
bool is_checksum_valid() const { return pkt_.is_checksum_valid(); };
// Returns flags (ONLY APPLICABLE FOR SOME COMMANDS)
// TODO: Probably combine these a bit?
uint8_t get_flags() const { return pkt_.get_payload_byte(PLINDEX_FLAGS); }
uint8_t get_flags_2() const { return pkt_.get_payload_byte(PLINDEX_FLAGS2); }
// Sets flags (ONLY APPLICABLE FOR SOME COMMANDS)
void set_flags(uint8_t flag_value);
// Adds a flag (ONLY APPLICABLE FOR SOME COMMANDS)
void add_flag(uint8_t flag_to_add);
// Adds a flag2 (ONLY APPLICABLE FOR SOME COMMANDS)
void add_flag2(uint8_t flag2_to_add);
SourceBridge get_source_bridge() const { return pkt_.get_source_bridge(); }
ControllerAssociation get_controller_association() const { return pkt_.get_controller_association(); }
protected:
static const int PLINDEX_FLAGS = 1;
static const int PLINDEX_FLAGS2 = 2;
RawPacket pkt_;
private:
bool response_expected_ = true;
};
////
// Connect
////
class ConnectRequestPacket : public Packet {
public:
using Packet::Packet;
static ConnectRequestPacket &instance() {
static ConnectRequestPacket instance;
return instance;
}
std::string to_string() const override;
private:
ConnectRequestPacket() : Packet(RawPacket(PacketType::CONNECT_REQUEST, 2)) {
pkt_.set_payload_byte(0, 0xca);
pkt_.set_payload_byte(1, 0x01);
}
};
class ConnectResponsePacket : public Packet {
public:
using Packet::Packet;
std::string to_string() const override;
};
////
// Identify packets
////
class CapabilitiesRequestPacket : public Packet {
public:
static CapabilitiesRequestPacket &instance() {
static CapabilitiesRequestPacket instance;
return instance;
}
using Packet::Packet;
private:
CapabilitiesRequestPacket() : Packet(RawPacket(PacketType::IDENTIFY_REQUEST, 1)) { pkt_.set_payload_byte(0, 0xc9); }
};
class CapabilitiesResponsePacket : public Packet {
using Packet::Packet;
public:
// Byte 7
bool is_heat_disabled() const { return pkt_.get_payload_byte(7) & 0x02; }
bool supports_vane() const { return pkt_.get_payload_byte(7) & 0x20; }
bool supports_vane_swing() const { return pkt_.get_payload_byte(7) & 0x40; }
// Byte 8
bool is_dry_disabled() const { return pkt_.get_payload_byte(8) & 0x01; }
bool is_fan_disabled() const { return pkt_.get_payload_byte(8) & 0x02; }
bool has_extended_temperature_range() const { return pkt_.get_payload_byte(8) & 0x04; }
bool auto_fan_speed_disabled() const { return pkt_.get_payload_byte(8) & 0x10; }
bool supports_installer_settings() const { return pkt_.get_payload_byte(8) & 0x20; }
bool supports_test_mode() const { return pkt_.get_payload_byte(8) & 0x40; }
bool supports_dry_temperature() const { return pkt_.get_payload_byte(8) & 0x80; }
// Byte 9
bool has_status_display() const { return pkt_.get_payload_byte(9) & 0x01; }
// Bytes 10-15
float get_min_cool_dry_setpoint() const { return MITPUtils::temp_scale_a_to_deg_c(pkt_.get_payload_byte(10)); }
float get_max_cool_dry_setpoint() const { return MITPUtils::temp_scale_a_to_deg_c(pkt_.get_payload_byte(11)); }
float get_min_heating_setpoint() const { return MITPUtils::temp_scale_a_to_deg_c(pkt_.get_payload_byte(12)); }
float get_max_heating_setpoint() const { return MITPUtils::temp_scale_a_to_deg_c(pkt_.get_payload_byte(13)); }
float get_min_auto_setpoint() const { return MITPUtils::temp_scale_a_to_deg_c(pkt_.get_payload_byte(14)); }
float get_max_auto_setpoint() const { return MITPUtils::temp_scale_a_to_deg_c(pkt_.get_payload_byte(15)); }
// Things that have to exist, but we don't know where yet.
bool supports_h_vane() const { return true; }
// Fan Speeds TODO: Probably move this to .cpp?
uint8_t get_supported_fan_speeds() const;
// Convert a temperature response into ClimateTraits. This will *not* include library-provided features.
// This will also not handle things like MHK2 humidity detection.
climate::ClimateTraits as_traits() const;
std::string to_string() const override;
};
class IdentifyCDRequestPacket : public Packet {
public:
static IdentifyCDRequestPacket &instance() {
static IdentifyCDRequestPacket instance;
return instance;
}
using Packet::Packet;
private:
IdentifyCDRequestPacket() : Packet(RawPacket(PacketType::IDENTIFY_REQUEST, 1)) { pkt_.set_payload_byte(0, 0xCD); }
};
class IdentifyCDResponsePacket : public Packet {
using Packet::Packet;
public:
std::string to_string() const override;
};
////
// Get
////
class GetRequestPacket : public Packet {
public:
static GetRequestPacket &get_settings_instance() {
static GetRequestPacket instance = GetRequestPacket(GetCommand::SETTINGS);
return instance;
}
static GetRequestPacket &get_current_temp_instance() {
static GetRequestPacket instance = GetRequestPacket(GetCommand::CURRENT_TEMP);
return instance;
}
static GetRequestPacket &get_status_instance() {
static GetRequestPacket instance = GetRequestPacket(GetCommand::STATUS);
return instance;
}
static GetRequestPacket &get_runstate_instance() {
static GetRequestPacket instance = GetRequestPacket(GetCommand::RUN_STATE);
return instance;
}
static GetRequestPacket &get_error_info_instance() {
static GetRequestPacket instance = GetRequestPacket(GetCommand::ERROR_INFO);
return instance;
}
using Packet::Packet;
GetCommand get_requested_command() const { return (GetCommand) pkt_.get_payload_byte(0); }
std::string to_string() const override;
private:
GetRequestPacket(GetCommand get_command) : Packet(RawPacket(PacketType::GET_REQUEST, 1)) {
pkt_.set_payload_byte(0, static_cast<uint8_t>(get_command));
}
};
class SettingsGetResponsePacket : public Packet {
static const int PLINDEX_POWER = 3;
static const int PLINDEX_MODE = 4;
static const int PLINDEX_TARGETTEMP_LEGACY = 5;
static const int PLINDEX_FAN = 6;
static const int PLINDEX_VANE = 7;
static const int PLINDEX_PROHIBITFLAGS = 8;
static const int PLINDEX_HVANE = 10;
static const int PLINDEX_TARGETTEMP = 11;
using Packet::Packet;
public:
uint8_t get_power() const { return pkt_.get_payload_byte(PLINDEX_POWER); }
uint8_t get_mode() const { return pkt_.get_payload_byte(PLINDEX_MODE); }
uint8_t get_fan() const { return pkt_.get_payload_byte(PLINDEX_FAN); }
uint8_t get_vane() const { return pkt_.get_payload_byte(PLINDEX_VANE); }
bool locked_power() const { return pkt_.get_payload_byte(PLINDEX_PROHIBITFLAGS) & 0x01; }
bool locked_mode() const { return pkt_.get_payload_byte(PLINDEX_PROHIBITFLAGS) & 0x02; }
bool locked_temp() const { return pkt_.get_payload_byte(PLINDEX_PROHIBITFLAGS) & 0x04; }
uint8_t get_horizontal_vane() const { return pkt_.get_payload_byte(PLINDEX_HVANE) & 0x7F; }
bool get_horizontal_vane_msb() const { return pkt_.get_payload_byte(PLINDEX_HVANE) & 0x80; }
float get_target_temp() const;
bool is_i_see_enabled() const;
std::string to_string() const override;
};
class CurrentTempGetResponsePacket : public Packet {
static const int PLINDEX_CURRENTTEMP_LEGACY = 3;
static const int PLINDEX_OUTDOORTEMP = 5;
static const int PLINDEX_CURRENTTEMP = 6;
using Packet::Packet;
public:
float get_current_temp() const;
// Returns outdoor temperature or NAN if unsupported
float get_outdoor_temp() const;
std::string to_string() const override;
};
class StatusGetResponsePacket : public Packet {
static const int PLINDEX_COMPRESSOR_FREQUENCY = 3;
static const int PLINDEX_OPERATING = 4;
using Packet::Packet;
public:
uint8_t get_compressor_frequency() const { return pkt_.get_payload_byte(PLINDEX_COMPRESSOR_FREQUENCY); }
bool get_operating() const { return pkt_.get_payload_byte(PLINDEX_OPERATING); }
std::string to_string() const override;
};
class RunStateGetResponsePacket : public Packet {
static const int PLINDEX_STATUSFLAGS = 3;
static const int PLINDEX_ACTUALFAN = 4;
static const int PLINDEX_AUTOMODE = 5;
using Packet::Packet;
public:
bool service_filter() const { return pkt_.get_payload_byte(PLINDEX_STATUSFLAGS) & 0x01; }
bool in_defrost() const { return pkt_.get_payload_byte(PLINDEX_STATUSFLAGS) & 0x02; }
bool in_preheat() const { return pkt_.get_payload_byte(PLINDEX_STATUSFLAGS) & 0x04; }
bool in_standby() const { return pkt_.get_payload_byte(PLINDEX_STATUSFLAGS) & 0x08; }
uint8_t get_actual_fan_speed() const { return pkt_.get_payload_byte(PLINDEX_ACTUALFAN); }
uint8_t get_auto_mode() const { return pkt_.get_payload_byte(PLINDEX_AUTOMODE); }
std::string to_string() const override;
};
class ErrorStateGetResponsePacket : public Packet {
using Packet::Packet;
public:
uint16_t get_error_code() const { return pkt_.get_payload_byte(4) << 8 | pkt_.get_payload_byte(5); }
uint8_t get_raw_short_code() const { return pkt_.get_payload_byte(6); }
std::string get_short_code() const;
bool error_present() const { return get_error_code() != 0x8000 || get_raw_short_code() != 0x00; }
std::string to_string() const override;
};
////
// Set
////
class SettingsSetRequestPacket : public Packet {
static const int PLINDEX_POWER = 3;
static const int PLINDEX_MODE = 4;
static const int PLINDEX_TARGET_TEMPERATURE_CODE = 5;
static const int PLINDEX_FAN = 6;
static const int PLINDEX_VANE = 7;
static const int PLINDEX_HORIZONTAL_VANE = 13;
static const int PLINDEX_TARGET_TEMPERATURE = 14;
enum SettingFlag : uint8_t {
SF_POWER = 0x01,
SF_MODE = 0x02,
SF_TARGET_TEMPERATURE = 0x04,
SF_FAN = 0x08,
SF_VANE = 0x10
};
enum SettingFlag2 : uint8_t {
SF2_HORIZONTAL_VANE = 0x01,
};
public:
enum ModeByte : uint8_t {
MODE_BYTE_HEAT = 0x01,
MODE_BYTE_DRY = 0x02,
MODE_BYTE_COOL = 0x03,
MODE_BYTE_FAN = 0x07,
MODE_BYTE_AUTO = 0x08,
};
enum FanByte : uint8_t {
FAN_AUTO = 0x00,
FAN_QUIET = 0x01,
FAN_1 = 0x02,
FAN_2 = 0x03,
FAN_3 = 0x05,
FAN_4 = 0x06,
};
enum VaneByte : uint8_t {
VANE_AUTO = 0x00,
VANE_1 = 0x01,
VANE_2 = 0x02,
VANE_3 = 0x03,
VANE_4 = 0x04,
VANE_5 = 0x05,
VANE_SWING = 0x07,
};
enum HorizontalVaneByte : uint8_t {
HV_AUTO = 0x00,
HV_LEFT_FULL = 0x01,
HV_LEFT = 0x02,
HV_CENTER = 0x03,
HV_RIGHT = 0x04,
HV_RIGHT_FULL = 0x05,
HV_SPLIT = 0x08,
HV_SWING = 0x0c,
};
SettingsSetRequestPacket() : Packet(RawPacket(PacketType::SET_REQUEST, 16)) {
pkt_.set_payload_byte(0, static_cast<uint8_t>(SetCommand::SETTINGS));
}
using Packet::Packet;
uint8_t get_power() const { return pkt_.get_payload_byte(PLINDEX_POWER); }
ModeByte get_mode() const { return (ModeByte) pkt_.get_payload_byte(PLINDEX_MODE); }
FanByte get_fan() const { return (FanByte) pkt_.get_payload_byte(PLINDEX_FAN); }
VaneByte get_vane() const { return (VaneByte) pkt_.get_payload_byte(PLINDEX_VANE); }
HorizontalVaneByte get_horizontal_vane() const {
return (HorizontalVaneByte) (pkt_.get_payload_byte(PLINDEX_HORIZONTAL_VANE) & 0x7F);
}
bool get_horizontal_vane_msb() const { return pkt_.get_payload_byte(PLINDEX_HORIZONTAL_VANE) & 0x80; }
float get_target_temp() const;
SettingsSetRequestPacket &set_power(bool is_on);
SettingsSetRequestPacket &set_mode(ModeByte mode);
SettingsSetRequestPacket &set_target_temperature(float temperature_degrees_c);
SettingsSetRequestPacket &set_fan(FanByte fan);
SettingsSetRequestPacket &set_vane(VaneByte vane);
SettingsSetRequestPacket &set_horizontal_vane(HorizontalVaneByte horizontal_vane);
std::string to_string() const override;
private:
void add_settings_flag_(SettingFlag flag_to_add);
void add_settings_flag2_(SettingFlag2 flag2_to_add);
};
class RemoteTemperatureSetRequestPacket : public Packet {
static const uint8_t PLINDEX_LEGACY_REMOTE_TEMPERATURE = 2;
static const uint8_t PLINDEX_REMOTE_TEMPERATURE = 3;
public:
RemoteTemperatureSetRequestPacket() : Packet(RawPacket(PacketType::SET_REQUEST, 4)) {
pkt_.set_payload_byte(0, static_cast<uint8_t>(SetCommand::REMOTE_TEMPERATURE));
}
using Packet::Packet;
float get_remote_temperature() const;
RemoteTemperatureSetRequestPacket &set_remote_temperature(float temperature_degrees_c);
RemoteTemperatureSetRequestPacket &use_internal_temperature();
std::string to_string() const override;
};
class SetResponsePacket : public Packet {
using Packet::Packet;
public:
SetResponsePacket() : Packet(RawPacket(PacketType::SET_RESPONSE, 16)) {}
uint8_t get_result_code() const { return pkt_.get_payload_byte(0); }
bool is_successful() const { return get_result_code() == 0; }
};
class SetRunStatePacket : public Packet {
// bytes 1 and 2 are update flags
static const uint8_t PLINDEX_FILTER_RESET = 3;
using Packet::Packet;
public:
SetRunStatePacket() : Packet(RawPacket(PacketType::SET_REQUEST, 10)) {
pkt_.set_payload_byte(0, static_cast<uint8_t>(SetCommand::RUN_STATE));
}
bool get_filter_reset() { return pkt_.get_payload_byte(PLINDEX_FILTER_RESET) != 0; };
SetRunStatePacket &set_filter_reset(bool do_reset);
};
class ThermostatSensorStatusPacket : public Packet {
using Packet::Packet;
public:
enum ThermostatBatteryState : uint8_t {
THERMOSTAT_BATTERY_OK = 0x00,
THERMOSTAT_BATTERY_LOW = 0x01,
THERMOSTAT_BATTERY_CRITICAL = 0x02,
THERMOSTAT_BATTERY_REPLACE = 0x03,
THERMOSTAT_BATTERY_UNKNOWN = 0x04,
};
ThermostatSensorStatusPacket() : Packet(RawPacket(PacketType::SET_REQUEST, 16)) {
pkt_.set_payload_byte(0, static_cast<uint8_t>(SetCommand::THERMOSTAT_SENSOR_STATUS));
}
uint8_t get_indoor_humidity_percent() const { return pkt_.get_payload_byte(5); }
ThermostatBatteryState get_thermostat_battery_state() const {
return (ThermostatBatteryState) pkt_.get_payload_byte(6);
}
uint8_t get_sensor_flags() const { return pkt_.get_payload_byte(7); }
std::string to_string() const override;
};
// Sent by MHK2 but with no response; defined to allow setResponseExpected(false)
class ThermostatHelloPacket : public Packet {
using Packet::Packet;
public:
ThermostatHelloPacket() : Packet(RawPacket(PacketType::SET_REQUEST, 16)) {
pkt_.set_payload_byte(0, static_cast<uint8_t>(SetCommand::THERMOSTAT_HELLO));
}
std::string get_thermostat_model() const;
std::string get_thermostat_serial() const;
std::string get_thermostat_version_string() const;
std::string to_string() const override;
};
class ThermostatStateUploadPacket : public Packet {
// Packet 0x41 - AG 0xA8
static const uint8_t PLINDEX_THERMOSTAT_TIMESTAMP = 2;
static const uint8_t PLINDEX_AUTO_MODE = 7;
static const uint8_t PLINDEX_HEAT_SETPOINT = 8;
static const uint8_t PLINDEX_COOL_SETPOINT = 9;
enum TSStateSyncFlags : uint8_t {
TSSF_TIMESTAMP = 0x01,
TSSF_AUTO_MODE = 0x04,
TSSF_HEAT_SETPOINT = 0x08,
TSSF_COOL_SETPOINT = 0x10,
};
using Packet::Packet;
public:
ThermostatStateUploadPacket() : Packet(RawPacket(PacketType::SET_REQUEST, 16)) {
pkt_.set_payload_byte(0, static_cast<uint8_t>(SetCommand::THERMOSTAT_STATE_UPLOAD));
}
time_t get_thermostat_timestamp(esphome::ESPTime *out_timestamp) const;
uint8_t get_auto_mode() const;
float get_heat_setpoint() const;
float get_cool_setpoint() const;
std::string to_string() const override;
};
class ThermostatStateDownloadResponsePacket : public Packet {
static const uint8_t PLINDEX_ADAPTER_TIMESTAMP = 1;
static const uint8_t PLINDEX_AUTO_MODE = 6;
static const uint8_t PLINDEX_HEAT_SETPOINT = 7;
static const uint8_t PLINDEX_COOL_SETPOINT = 8;
using Packet::Packet;
public:
ThermostatStateDownloadResponsePacket() : Packet(RawPacket(PacketType::GET_RESPONSE, 16)) {
pkt_.set_payload_byte(0, static_cast<uint8_t>(GetCommand::THERMOSTAT_STATE_DOWNLOAD));
}
ThermostatStateDownloadResponsePacket &set_timestamp(ESPTime ts);
ThermostatStateDownloadResponsePacket &set_auto_mode(bool is_auto);
ThermostatStateDownloadResponsePacket &set_heat_setpoint(float high_temp);
ThermostatStateDownloadResponsePacket &set_cool_setpoint(float low_temp);
};
class ThermostatAASetRequestPacket : public Packet {
using Packet::Packet;
public:
ThermostatAASetRequestPacket() : Packet(RawPacket(PacketType::SET_REQUEST, 16)) {
pkt_.set_payload_byte(0, static_cast<uint8_t>(SetCommand::THERMOSTAT_SET_AA));
}
};
class ThermostatABGetResponsePacket : public Packet {
using Packet::Packet;
public:
ThermostatABGetResponsePacket() : Packet(RawPacket(PacketType::GET_RESPONSE, 16)) {
pkt_.set_payload_byte(0, static_cast<uint8_t>(GetCommand::THERMOSTAT_GET_AB));
pkt_.set_payload_byte(1, 1);
}
};
class PacketProcessor {
public:
virtual void process_packet(const Packet &packet){};
virtual void process_packet(const ConnectRequestPacket &packet){};
virtual void process_packet(const ConnectResponsePacket &packet){};
virtual void process_packet(const CapabilitiesRequestPacket &packet){};
virtual void process_packet(const CapabilitiesResponsePacket &packet){};
virtual void process_packet(const GetRequestPacket &packet){};
virtual void process_packet(const SettingsGetResponsePacket &packet){};
virtual void process_packet(const CurrentTempGetResponsePacket &packet){};
virtual void process_packet(const StatusGetResponsePacket &packet){};
virtual void process_packet(const RunStateGetResponsePacket &packet){};
virtual void process_packet(const ErrorStateGetResponsePacket &packet){};
virtual void process_packet(const SettingsSetRequestPacket &packet){};
virtual void process_packet(const RemoteTemperatureSetRequestPacket &packet){};
virtual void process_packet(const ThermostatSensorStatusPacket &packet){};
virtual void process_packet(const ThermostatHelloPacket &packet){};
virtual void process_packet(const ThermostatStateUploadPacket &packet){};
virtual void process_packet(const ThermostatStateDownloadResponsePacket &packet){};
virtual void process_packet(const ThermostatAASetRequestPacket &packet){};
virtual void process_packet(const ThermostatABGetResponsePacket &packet){};
virtual void process_packet(const SetResponsePacket &packet){};
virtual void handle_thermostat_state_download_request(const GetRequestPacket &packet){};
virtual void handle_thermostat_ab_get_request(const GetRequestPacket &packet){};
};
} // namespace mitsubishi_itp
} // namespace esphome

View file

@ -0,0 +1,70 @@
#include "mitp_rawpacket.h"
namespace esphome {
namespace mitsubishi_itp {
// Creates an empty packet
RawPacket::RawPacket(PacketType packet_type, uint8_t payload_size, SourceBridge source_bridge,
ControllerAssociation controller_association)
: length_{(uint8_t) (payload_size + PACKET_HEADER_SIZE + 1)},
checksum_index_{(uint8_t) (length_ - 1)},
source_bridge_{source_bridge},
controller_association_{controller_association} {
memcpy(packet_bytes_, EMPTY_PACKET, length_);
packet_bytes_[PACKET_HEADER_INDEX_PACKET_TYPE] = static_cast<uint8_t>(packet_type);
packet_bytes_[PACKET_HEADER_INDEX_PAYLOAD_LENGTH] = payload_size;
update_checksum_();
}
// Creates a packet with the provided bytes
RawPacket::RawPacket(const uint8_t packet_bytes[], const uint8_t packet_length, SourceBridge source_bridge,
ControllerAssociation controller_association)
: length_{(uint8_t) packet_length},
checksum_index_{(uint8_t) (packet_length - 1)},
source_bridge_{source_bridge},
controller_association_{controller_association} {
memcpy(packet_bytes_, packet_bytes, packet_length);
if (!this->is_checksum_valid()) {
// For now, just log this as information (we can decide if we want to process it elsewhere)
ESP_LOGI(PTAG, "Packet of type %x has invalid checksum!", this->get_packet_type());
}
}
// Creates an empty RawPacket
RawPacket::RawPacket() {
// TODO: Is this okay?
}
uint8_t RawPacket::calculate_checksum_() const { // NOLINT(readability-identifier-naming)
uint8_t sum = 0;
for (int i = 0; i < checksum_index_; i++) {
sum += packet_bytes_[i];
}
return (0xfc - sum) & 0xff;
}
RawPacket &RawPacket::update_checksum_() {
packet_bytes_[checksum_index_] = calculate_checksum_();
return *this;
}
bool RawPacket::is_checksum_valid() const { return packet_bytes_[checksum_index_] == calculate_checksum_(); }
// Sets a payload byte and automatically updates the packet checksum
RawPacket &RawPacket::set_payload_byte(const uint8_t payload_byte_index, const uint8_t value) {
packet_bytes_[PACKET_HEADER_SIZE + payload_byte_index] = value;
update_checksum_();
return *this;
}
RawPacket &RawPacket::set_payload_bytes(const uint8_t begin_index, const void *value, const size_t size) {
memcpy(&packet_bytes_[PACKET_HEADER_SIZE + begin_index], value, size);
update_checksum_();
return *this;
}
} // namespace mitsubishi_itp
} // namespace esphome

View file

@ -0,0 +1,122 @@
#pragma once
#include "esphome/core/component.h"
#include "esphome/components/climate/climate.h"
#include "esphome/components/uart/uart.h"
#include <type_traits>
namespace esphome {
namespace mitsubishi_itp {
static constexpr char PTAG[] = "mitsubishi_itp.packets";
const uint8_t BYTE_CONTROL = 0xfc;
const uint8_t PACKET_MAX_SIZE = 22; // Used to intialize empty packet
const uint8_t PACKET_HEADER_SIZE = 5;
const uint8_t PACKET_HEADER_INDEX_PACKET_TYPE = 1;
const uint8_t PACKET_HEADER_INDEX_PAYLOAD_LENGTH = 4;
// TODO: Figure out something here so we don't have to static_cast<uint8_t> as much
enum class PacketType : uint8_t {
CONNECT_REQUEST = 0x5a,
CONNECT_RESPONSE = 0x7a,
GET_REQUEST = 0x42,
GET_RESPONSE = 0x62,
SET_REQUEST = 0x41,
SET_RESPONSE = 0x61,
IDENTIFY_REQUEST = 0x5b,
IDENTIFY_RESPONSE = 0x7b
};
// Used to specify certain packet subtypes
enum class GetCommand : uint8_t {
SETTINGS = 0x02,
CURRENT_TEMP = 0x03,
ERROR_INFO = 0x04,
STATUS = 0x06,
RUN_STATE = 0x09,
THERMOSTAT_STATE_DOWNLOAD = 0xa9,
THERMOSTAT_GET_AB = 0xab,
};
// Used to specify certain packet subtypes
enum class SetCommand : uint8_t {
SETTINGS = 0x01,
REMOTE_TEMPERATURE = 0x07,
RUN_STATE = 0x08,
THERMOSTAT_SENSOR_STATUS = 0xa6,
THERMOSTAT_HELLO = 0xa7,
THERMOSTAT_STATE_UPLOAD = 0xa8,
THERMOSTAT_SET_AA = 0xaa,
};
// Which MITPBridge was the packet read from (used to determine flow direction of the packet)
enum class SourceBridge { NONE, HEATPUMP, THERMOSTAT };
// Specifies which controller the packet "belongs" to (i.e. which controler created it either directly or via a request
// packet)
enum class ControllerAssociation { MITP, THERMOSTAT };
static const uint8_t EMPTY_PACKET[PACKET_MAX_SIZE] = {BYTE_CONTROL, // Sync
0x00, // Packet type
0x01, 0x30, // Unknown
0x00, // Payload Size
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // Payload
0x00};
/* A class representing the raw packet sent to or from the Mitsubishi equipment with definitions
for header indexes, checksum calculations, utility methods, etc. These generally shouldn't be accessed
directly outside the MITPBridge, and the Packet class (or its subclasses) should be used instead.
*/
class RawPacket {
public:
RawPacket(
const uint8_t packet_bytes[], uint8_t packet_length, SourceBridge source_bridge = SourceBridge::NONE,
ControllerAssociation controller_association = ControllerAssociation::MITP); // For reading or copying packets
// TODO: Can I hide this constructor except from optional?
RawPacket(); // For optional<RawPacket> construction
RawPacket(PacketType packet_type, uint8_t payload_size, SourceBridge source_bridge = SourceBridge::NONE,
ControllerAssociation controller_association = ControllerAssociation::MITP); // For building packets
virtual ~RawPacket() {}
virtual std::string to_string() const { return format_hex_pretty(&get_bytes()[0], get_length()); };
uint8_t get_length() const { return length_; };
const uint8_t *get_bytes() const { return packet_bytes_; }; // Primarily for sending packets
bool is_checksum_valid() const;
// Returns the packet type byte
uint8_t get_packet_type() const { return packet_bytes_[PACKET_HEADER_INDEX_PACKET_TYPE]; };
// Returns the first byte of the payload, often used as a command
uint8_t get_command() const { return get_payload_byte(PLINDEX_COMMAND); };
SourceBridge get_source_bridge() const { return source_bridge_; };
ControllerAssociation get_controller_association() const { return controller_association_; };
RawPacket &set_payload_byte(uint8_t payload_byte_index, uint8_t value);
RawPacket &set_payload_bytes(uint8_t begin_index, const void *value, size_t size);
uint8_t get_payload_byte(const uint8_t payload_byte_index) const {
return packet_bytes_[PACKET_HEADER_SIZE + payload_byte_index];
};
const uint8_t *get_payload_bytes(size_t start_index = 0) const {
return &packet_bytes_[PACKET_HEADER_SIZE + start_index];
}
private:
static const int PLINDEX_COMMAND = 0;
uint8_t packet_bytes_[PACKET_MAX_SIZE]{};
uint8_t length_;
uint8_t checksum_index_;
SourceBridge source_bridge_;
ControllerAssociation controller_association_;
uint8_t calculate_checksum_() const;
RawPacket &update_checksum_();
};
} // namespace mitsubishi_itp
} // namespace esphome

View file

@ -0,0 +1,87 @@
#pragma once
namespace esphome {
namespace mitsubishi_itp {
class MITPUtils {
public:
/// Read a string out of data, wordSize bits at a time.
/// Used to decode serial numbers and other information from a thermostat.
static std::string decode_n_bit_string(const uint8_t data[], size_t data_length, size_t word_size = 6) {
auto result = std::string();
for (int i = 0; i < data_length; i++) {
auto bits = bit_slice(data, i * word_size, ((i + 1) * word_size) - 1);
if (bits <= 0x1F)
bits += 0x40;
result += (char) bits;
}
return result;
}
static float temp_scale_a_to_deg_c(const uint8_t value) { return (float) (value - 128) / 2.0f; }
static uint8_t deg_c_to_temp_scale_a(const float value) {
// Special cases
if (value < -64)
return 0;
if (value > 63.5f)
return 0xFF;
return (uint8_t) round(value * 2) + 128;
}
static float legacy_target_temp_to_deg_c(const uint8_t value) {
return ((float) (31 - (value & 0x0F)) + (((value & 0xF0) > 0) ? 0.5f : 0));
}
static uint8_t deg_c_to_legacy_target_temp(const float value) {
// Special cases per docs
if (value < 16)
return 0x0F;
if (value > 31.5)
return 0x10;
return ((31 - (uint8_t) value) & 0xF) + (((int) (value * 2) % 2) << 4);
}
static float legacy_room_temp_to_deg_c(const uint8_t value) { return (float) value + 10; }
static uint8_t deg_c_to_legacy_room_temp(const float value) {
if (value < 10)
return 0x00;
if (value > 41)
return 0x1F;
return (uint8_t) value - 10;
}
private:
/// Extract the specified bits (inclusive) from an arbitrarily-sized byte array. Does not perform bounds checks.
/// Max extraction is 64 bits. Preserves endianness of incoming data stream.
static uint64_t bit_slice(const uint8_t ds[], size_t start, size_t end) {
if ((end - start) >= 64)
return 0;
uint64_t result = 0;
size_t start_byte = (start) / 8;
size_t end_byte = ((end) / 8) + 1; // exclusive, used for length calc
// raw copy the relevant bytes into our int64, preserving endian-ness
std::memcpy(&result, &ds[start_byte], end_byte - start_byte);
result = byteswap(result);
// shift out the bits we don't want from the end (64 + credit any pre-sliced bits)
result >>= (sizeof(uint64_t) * 8) + (start_byte * 8) - end - 1;
// mask out the number of bits we want
result &= (1 << (end - start + 1)) - 1;
return result;
}
};
} // namespace mitsubishi_itp
} // namespace esphome

View file

@ -0,0 +1,120 @@
#include "mitsubishi_itp.h"
namespace esphome {
namespace mitsubishi_itp {
// Called to instruct a change of the climate controls
void MitsubishiUART::control(const climate::ClimateCall &call) {
if (!active_mode_)
return; // If we're not in active mode, ignore control requests
SettingsSetRequestPacket set_request_packet = SettingsSetRequestPacket();
// Apply fan settings
// Prioritize a custom fan mode if it's set.
if (call.get_custom_fan_mode().has_value()) {
if (call.get_custom_fan_mode().value() == FAN_MODE_VERYHIGH) {
set_custom_fan_mode_(FAN_MODE_VERYHIGH);
set_request_packet.set_fan(SettingsSetRequestPacket::FAN_4);
}
} else if (call.get_fan_mode().has_value()) {
switch (call.get_fan_mode().value()) {
case climate::CLIMATE_FAN_QUIET:
set_fan_mode_(climate::CLIMATE_FAN_QUIET);
set_request_packet.set_fan(SettingsSetRequestPacket::FAN_QUIET);
break;
case climate::CLIMATE_FAN_LOW:
set_fan_mode_(climate::CLIMATE_FAN_LOW);
set_request_packet.set_fan(SettingsSetRequestPacket::FAN_1);
break;
case climate::CLIMATE_FAN_MEDIUM:
set_fan_mode_(climate::CLIMATE_FAN_MEDIUM);
set_request_packet.set_fan(SettingsSetRequestPacket::FAN_2);
break;
case climate::CLIMATE_FAN_HIGH:
set_fan_mode_(climate::CLIMATE_FAN_HIGH);
set_request_packet.set_fan(SettingsSetRequestPacket::FAN_3);
break;
case climate::CLIMATE_FAN_AUTO:
set_fan_mode_(climate::CLIMATE_FAN_AUTO);
set_request_packet.set_fan(SettingsSetRequestPacket::FAN_AUTO);
break;
default:
ESP_LOGW(TAG, "Unhandled fan mode %i!", call.get_fan_mode().value());
break;
}
}
// Mode
if (call.get_mode().has_value()) {
mode = call.get_mode().value();
switch (call.get_mode().value()) {
case climate::CLIMATE_MODE_HEAT_COOL:
set_request_packet.set_power(true).set_mode(SettingsSetRequestPacket::MODE_BYTE_AUTO);
break;
case climate::CLIMATE_MODE_COOL:
set_request_packet.set_power(true).set_mode(SettingsSetRequestPacket::MODE_BYTE_COOL);
break;
case climate::CLIMATE_MODE_HEAT:
set_request_packet.set_power(true).set_mode(SettingsSetRequestPacket::MODE_BYTE_HEAT);
break;
case climate::CLIMATE_MODE_FAN_ONLY:
set_request_packet.set_power(true).set_mode(SettingsSetRequestPacket::MODE_BYTE_FAN);
break;
case climate::CLIMATE_MODE_DRY:
set_request_packet.set_power(true).set_mode(SettingsSetRequestPacket::MODE_BYTE_DRY);
break;
case climate::CLIMATE_MODE_OFF:
default:
set_request_packet.set_power(false);
break;
}
}
// Target Temperature
if (call.get_target_temperature().has_value()) {
target_temperature = call.get_target_temperature().value();
set_request_packet.set_target_temperature(call.get_target_temperature().value());
// update our MHK tracking setpoints accordingly
switch (mode) {
case climate::CLIMATE_MODE_COOL:
case climate::CLIMATE_MODE_DRY:
this->mhk_state_.cool_setpoint_ = target_temperature;
break;
case climate::CLIMATE_MODE_HEAT:
this->mhk_state_.heat_setpoint_ = target_temperature;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
if (this->get_traits().get_supports_two_point_target_temperature()) {
this->mhk_state_.cool_setpoint_ = target_temperature_low;
this->mhk_state_.heat_setpoint_ = target_temperature_high;
} else {
// HACK: This is not accurate, but it's good enough for testing.
this->mhk_state_.cool_setpoint_ = target_temperature + 2;
this->mhk_state_.heat_setpoint_ = target_temperature - 2;
}
default:
break;
}
}
// TODO:
// Vane
// HVane?
// Swing?
// We're assuming that every climate call *does* make some change worth sending to the heat pump
// Queue the packet to be sent first (so any subsequent update packets come *after* our changes)
hp_bridge_.send_packet(set_request_packet);
// Publish state and any sensor changes (shouldn't be any a result of this function, but
// since they lazy-publish, no harm in trying)
do_publish_();
}
} // namespace mitsubishi_itp
} // namespace esphome

View file

@ -0,0 +1,363 @@
#include "mitsubishi_itp.h"
namespace esphome {
namespace mitsubishi_itp {
void MitsubishiUART::route_packet_(const Packet &packet) {
// If the packet is associated with the thermostat and just came from the thermostat, send it to the heatpump
// If it came from the heatpump, send it back to the thermostat
if (packet.get_controller_association() == ControllerAssociation::THERMOSTAT) {
if (packet.get_source_bridge() == SourceBridge::THERMOSTAT) {
hp_bridge_.send_packet(packet);
} else if (packet.get_source_bridge() == SourceBridge::HEATPUMP) {
ts_bridge_->send_packet(packet);
}
}
}
// Packet Handlers
void MitsubishiUART::process_packet(const Packet &packet) {
ESP_LOGI(TAG, "Generic unhandled packet type %x received.", packet.get_packet_type());
ESP_LOGD(TAG, "%s", packet.to_string().c_str());
route_packet_(packet);
}
void MitsubishiUART::process_packet(const ConnectRequestPacket &packet) {
// Nothing to be done for these except forward them along from thermostat to heat pump.
// This method defined so that these packets are not "unhandled"
ESP_LOGV(TAG, "Passing through inbound %s", packet.to_string().c_str());
route_packet_(packet);
}
void MitsubishiUART::process_packet(const ConnectResponsePacket &packet) {
ESP_LOGV(TAG, "Processing %s", packet.to_string().c_str());
route_packet_(packet);
// Not sure if there's any needed content in this response, so assume we're connected.
hp_connected_ = true;
ESP_LOGI(TAG, "Heatpump connected.");
}
void MitsubishiUART::process_packet(const CapabilitiesRequestPacket &packet) {
// Nothing to be done for these except forward them along from thermostat to heat pump.
// This method defined so that these packets are not "unhandled"
ESP_LOGV(TAG, "Passing through inbound %s", packet.to_string().c_str());
route_packet_(packet);
}
void MitsubishiUART::process_packet(const CapabilitiesResponsePacket &packet) {
ESP_LOGV(TAG, "Processing %s", packet.to_string().c_str());
route_packet_(packet);
// Not sure if there's any needed content in this response, so assume we're connected.
// TODO: Is there more useful info in these?
hp_connected_ = true;
capabilities_cache_ = packet;
ESP_LOGI(TAG, "Received heat pump identification packet.");
}
void MitsubishiUART::process_packet(const GetRequestPacket &packet) {
ESP_LOGV(TAG, "Processing %s", packet.to_string().c_str());
switch (packet.get_requested_command()) {
case GetCommand::THERMOSTAT_STATE_DOWNLOAD:
this->handle_thermostat_state_download_request(packet);
break;
case GetCommand::THERMOSTAT_GET_AB:
this->handle_thermostat_ab_get_request(packet);
break;
default:
route_packet_(packet);
}
}
void MitsubishiUART::process_packet(const SettingsGetResponsePacket &packet) {
ESP_LOGV(TAG, "Processing %s", packet.to_string().c_str());
route_packet_(packet);
alert_listeners_(packet);
// Mode
const climate::ClimateMode old_mode = mode;
if (packet.get_power()) {
switch (packet.get_mode()) {
case 0x01:
case 0x09: // i-see
mode = climate::CLIMATE_MODE_HEAT;
break;
case 0x02:
case 0x0A: // i-see
mode = climate::CLIMATE_MODE_DRY;
break;
case 0x03:
case 0x0B: // i-see
mode = climate::CLIMATE_MODE_COOL;
break;
case 0x07:
mode = climate::CLIMATE_MODE_FAN_ONLY;
break;
case 0x08:
// unsure when 0x21 or 0x23 would ever be sent, as they seem to be Kumo exclusive, but let's handle them anyways.
case 0x21:
case 0x23:
mode = climate::CLIMATE_MODE_HEAT_COOL;
break;
default:
mode = climate::CLIMATE_MODE_OFF;
}
} else {
mode = climate::CLIMATE_MODE_OFF;
}
publish_on_update_ |= (old_mode != mode);
// Temperature
const float old_target_temperature = target_temperature;
target_temperature = packet.get_target_temp();
publish_on_update_ |= (old_target_temperature != target_temperature);
switch (mode) {
case climate::CLIMATE_MODE_COOL:
case climate::CLIMATE_MODE_DRY:
this->mhk_state_.cool_setpoint_ = target_temperature;
break;
case climate::CLIMATE_MODE_HEAT:
this->mhk_state_.heat_setpoint_ = target_temperature;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
this->mhk_state_.cool_setpoint_ = target_temperature + 2;
this->mhk_state_.heat_setpoint_ = target_temperature - 2;
default:
break;
}
// Fan
static bool fan_changed = false;
switch (packet.get_fan()) {
case 0x00:
fan_changed = set_fan_mode_(climate::CLIMATE_FAN_AUTO);
break;
case 0x01:
fan_changed = set_fan_mode_(climate::CLIMATE_FAN_QUIET);
break;
case 0x02:
fan_changed = set_fan_mode_(climate::CLIMATE_FAN_LOW);
break;
case 0x03:
fan_changed = set_fan_mode_(climate::CLIMATE_FAN_MEDIUM);
break;
case 0x05:
fan_changed = set_fan_mode_(climate::CLIMATE_FAN_HIGH);
break;
case 0x06:
fan_changed = set_custom_fan_mode_(FAN_MODE_VERYHIGH);
break;
}
publish_on_update_ |= fan_changed;
}
void MitsubishiUART::process_packet(const CurrentTempGetResponsePacket &packet) {
ESP_LOGV(TAG, "Processing %s", packet.to_string().c_str());
route_packet_(packet);
alert_listeners_(packet);
// This will be the same as the remote temperature if we're using a remote sensor, otherwise the internal temp
const float old_current_temperature = current_temperature;
current_temperature = packet.get_current_temp();
publish_on_update_ |= (old_current_temperature != current_temperature);
}
void MitsubishiUART::process_packet(const StatusGetResponsePacket &packet) {
ESP_LOGV(TAG, "Processing %s", packet.to_string().c_str());
route_packet_(packet);
alert_listeners_(packet);
const climate::ClimateAction old_action = action;
// If mode is off, action is off
if (mode == climate::CLIMATE_MODE_OFF) {
action = climate::CLIMATE_ACTION_OFF;
}
// If mode is fan only, packet.getOperating() may be false, but the fan is running
else if (mode == climate::CLIMATE_MODE_FAN_ONLY) {
action = climate::CLIMATE_ACTION_FAN;
}
// If mode is anything other than off or fan, and the unit is operating, determine the action
else if (packet.get_operating()) {
switch (mode) {
case climate::CLIMATE_MODE_HEAT:
action = climate::CLIMATE_ACTION_HEATING;
break;
case climate::CLIMATE_MODE_COOL:
action = climate::CLIMATE_ACTION_COOLING;
break;
case climate::CLIMATE_MODE_DRY:
action = climate::CLIMATE_ACTION_DRYING;
break;
// TODO: This only works if we get an update while the temps are in this configuration
// Surely there's some info from the heat pump about which of these modes it's in?
case climate::CLIMATE_MODE_HEAT_COOL:
if (current_temperature > target_temperature) {
action = climate::CLIMATE_ACTION_COOLING;
} else if (current_temperature < target_temperature) {
action = climate::CLIMATE_ACTION_HEATING;
}
// When the heat pump *changes* to a new action, these temperature comparisons should be accurate.
// If the mode hasn't changed, but the temps are equal, we can assume the same action and make no change.
// If the unit overshoots, this still doesn't work.
break;
default:
ESP_LOGW(TAG, "Unhandled mode %i.", mode);
break;
}
}
// If we're not operating (but not off or in fan mode), we're idle
// Should be relatively safe to fall through any unknown modes into showing IDLE
else {
action = climate::CLIMATE_ACTION_IDLE;
}
publish_on_update_ |= (old_action != action);
}
void MitsubishiUART::process_packet(const RunStateGetResponsePacket &packet) {
ESP_LOGV(TAG, "Processing %s", packet.to_string().c_str());
route_packet_(packet);
alert_listeners_(packet);
run_state_received_ = true; // Set this since we received one
// TODO: Not sure what AutoMode does yet
}
void MitsubishiUART::process_packet(const ErrorStateGetResponsePacket &packet) {
ESP_LOGV(TAG, "Processing %s", packet.to_string().c_str());
route_packet_(packet);
alert_listeners_(packet);
}
void MitsubishiUART::process_packet(const SettingsSetRequestPacket &packet) {
ESP_LOGV(TAG, "Passing through inbound %s", packet.to_string().c_str());
// forward this packet as-is; we're just intercepting to log.
alert_listeners_(packet);
}
void MitsubishiUART::process_packet(const RemoteTemperatureSetRequestPacket &packet) {
ESP_LOGV(TAG, "Processing %s", packet.to_string().c_str());
// Only send this temperature packet to the heatpump if Thermostat is the selected source,
// or we're in passive mode (since in passive mode we're not generating any packets to
// set the temperature) otherwise just respond to the thermostat to keep it happy.
if (current_temperature_source_ == TEMPERATURE_SOURCE_THERMOSTAT || !active_mode_) {
route_packet_(packet);
} else {
ts_bridge_->send_packet(SetResponsePacket());
}
alert_listeners_(packet);
float t = packet.get_remote_temperature();
temperature_source_report(TEMPERATURE_SOURCE_THERMOSTAT, t);
}
void MitsubishiUART::process_packet(const ThermostatSensorStatusPacket &packet) {
if (!enhanced_mhk_support_) {
ESP_LOGV(TAG, "Passing through inbound %s", packet.to_string().c_str());
route_packet_(packet);
return;
}
ESP_LOGV(TAG, "Processing inbound %s", packet.to_string().c_str());
alert_listeners_(packet);
ts_bridge_->send_packet(SetResponsePacket());
}
void MitsubishiUART::process_packet(const ThermostatHelloPacket &packet) {
if (!enhanced_mhk_support_) {
ESP_LOGV(TAG, "Passing through inbound %s", packet.to_string().c_str());
route_packet_(packet);
return;
}
ESP_LOGV(TAG, "Processing inbound %s", packet.to_string().c_str());
ts_bridge_->send_packet(SetResponsePacket());
}
void MitsubishiUART::process_packet(const ThermostatStateUploadPacket &packet) {
if (!enhanced_mhk_support_) {
ESP_LOGV(TAG, "Passing through inbound %s", packet.to_string().c_str());
route_packet_(packet);
return;
}
ESP_LOGV(TAG, "Processing inbound %s", packet.to_string().c_str());
if (packet.get_flags() & 0x08)
this->mhk_state_.heat_setpoint_ = packet.get_heat_setpoint();
if (packet.get_flags() & 0x10)
this->mhk_state_.cool_setpoint_ = packet.get_cool_setpoint();
ts_bridge_->send_packet(SetResponsePacket());
}
void MitsubishiUART::process_packet(const ThermostatAASetRequestPacket &packet) {
if (!enhanced_mhk_support_) {
ESP_LOGV(TAG, "Passing through inbound %s", packet.to_string().c_str());
route_packet_(packet);
return;
}
ESP_LOGV(TAG, "Processing inbound %s", packet.to_string().c_str());
ts_bridge_->send_packet(SetResponsePacket());
}
void MitsubishiUART::process_packet(const SetResponsePacket &packet) {
ESP_LOGV(TAG, "Got Set Response packet, success = %s (code = %x)", packet.is_successful() ? "true" : "false",
packet.get_result_code());
route_packet_(packet);
}
// Process incoming data requests from an MHK probing for/running in enhanced mode
void MitsubishiUART::handle_thermostat_state_download_request(const GetRequestPacket &packet) {
if (!enhanced_mhk_support_) {
route_packet_(packet);
return;
}
auto response = ThermostatStateDownloadResponsePacket();
response.set_auto_mode((mode == climate::CLIMATE_MODE_HEAT_COOL || mode == climate::CLIMATE_MODE_AUTO));
response.set_heat_setpoint(this->mhk_state_.heat_setpoint_);
response.set_cool_setpoint(this->mhk_state_.cool_setpoint_);
#ifdef USE_TIME
if (this->time_source_ != nullptr) {
response.set_timestamp(this->time_source_->now());
} else {
ESP_LOGW(TAG, "No time source specified. Cannot provide accurate time!");
response.set_timestamp(ESPTime::from_epoch_utc(1704067200)); // 2024-01-01 00:00:00Z
}
#else
ESP_LOGW(TAG, "No time source specified. Cannot provide accurate time!");
response.set_timestamp(ESPTime::from_epoch_utc(1704067200)); // 2024-01-01 00:00:00Z
#endif
ts_bridge_->send_packet(response);
}
void MitsubishiUART::handle_thermostat_ab_get_request(const GetRequestPacket &packet) {
if (!enhanced_mhk_support_) {
route_packet_(packet);
return;
}
auto response = ThermostatABGetResponsePacket();
ts_bridge_->send_packet(response);
}
} // namespace mitsubishi_itp
} // namespace esphome

View file

@ -0,0 +1,273 @@
#include "mitsubishi_itp.h"
namespace esphome {
namespace mitsubishi_itp {
////
// MitsubishiUART
////
MitsubishiUART::MitsubishiUART(uart::UARTComponent *hp_uart_comp)
: hp_uart_{*hp_uart_comp}, hp_bridge_{HeatpumpBridge(hp_uart_comp, this)} {
/**
* Climate pushes all its data to Home Assistant immediately when the API connects, this causes
* the default 0 to be sent as temperatures, but since this is a valid value (0 deg C), it
* can cause confusion and mess with graphs when looking at the state in HA. Setting this to
* NAN gets HA to treat this value as "unavailable" until we have a real value to publish.
*/
target_temperature = NAN;
current_temperature = NAN;
}
// Used to restore state of previous MITP-specific settings (like temperature source or pass-thru mode)
// Most other climate-state is preserved by the heatpump itself and will be retrieved after connection
void MitsubishiUART::setup() {
for (auto *listener : listeners_) {
listener->setup(bool(ts_uart_));
}
}
void MitsubishiUART::send_if_active_(const Packet &packet) {
if (active_mode_)
hp_bridge_.send_packet(packet);
}
#define IFACTIVE(dothis) \
if (active_mode_) { \
dothis \
}
#define IFNOTACTIVE(dothis) \
if (!active_mode_) { \
dothis \
}
/* Used for receiving and acting on incoming packets as soon as they're available.
Because packet processing happens as part of the receiving process, packet processing
should not block for very long (e.g. no publishing inside the packet processing)
*/
void MitsubishiUART::loop() {
// Loop bridge to handle sending and receiving packets
hp_bridge_.loop();
if (ts_bridge_)
ts_bridge_->loop();
// If it's been too long since we received a temperature update (and we're not set to Internal)
if (((millis() - last_received_temperature_) > TEMPERATURE_SOURCE_TIMEOUT_MS) &&
current_temperature_source_ != TEMPERATURE_SOURCE_INTERNAL && !temperature_source_timeout_) {
ESP_LOGW(TAG, "No temperature received from %s for %lu milliseconds, reverting to Internal source",
current_temperature_source_.c_str(), (unsigned long) TEMPERATURE_SOURCE_TIMEOUT_MS);
// Let listeners know we've changed to the Internal temperature source (but do not change
// currentTemperatureSource)
for (auto *listener : listeners_) {
listener->temperature_source_change(TEMPERATURE_SOURCE_INTERNAL);
}
temperature_source_timeout_ = true;
// Send a packet to the heat pump to tell it to switch to internal temperature sensing
IFACTIVE(hp_bridge_.send_packet(RemoteTemperatureSetRequestPacket().use_internal_temperature());)
}
}
void MitsubishiUART::dump_config() {
if (capabilities_cache_.has_value()) {
ESP_LOGCONFIG(TAG, "Discovered Capabilities: %s", capabilities_cache_.value().to_string().c_str());
}
if (enhanced_mhk_support_) {
ESP_LOGCONFIG(TAG, "MHK Enhanced Protocol Mode is ENABLED! This is currently *experimental* and things may break!");
}
}
// Set thermostat UART component
void MitsubishiUART::set_thermostat_uart(uart::UARTComponent *uart) {
ESP_LOGCONFIG(TAG, "Thermostat uart was set.");
ts_uart_ = uart;
ts_bridge_ = make_unique<ThermostatBridge>(ts_uart_, static_cast<PacketProcessor *>(this));
}
/* Called periodically as PollingComponent; used to send packets to connect or request updates.
Possible TODO: If we only publish during updates, since data is received during loop, updates will always
be about `update_interval` late from their actual time. Generally the update interval should be low enough
(default is 5seconds) this won't pose a practical problem.
*/
void MitsubishiUART::update() {
// TODO: Temporarily wait 5 seconds on startup to help with viewing logs
if (millis() < 5000) {
return;
}
// If we're not yet connected, send off a connection request (we'll check again next update)
if (!hp_connected_) {
IFACTIVE(hp_bridge_.send_packet(ConnectRequestPacket::instance());)
return;
}
// Attempt to read capabilities on the next loop after connect.
// TODO: This should likely be done immediately after connect, and will likely need to block setup for proper
// autoconf.
// For now, just requesting it as part of our "init loops" is a good first step.
if (!this->capabilities_requested_) {
IFACTIVE(hp_bridge_.send_packet(CapabilitiesRequestPacket::instance()); this->capabilities_requested_ = true;)
}
// Before requesting additional updates, publish any changes waiting from packets received
// Notify all listeners a publish is happening, they will decide if actual publish is needed.
for (auto *listener : listeners_) {
listener->publish();
}
if (publish_on_update_) {
do_publish_();
publish_on_update_ = false;
}
IFACTIVE(
// Request an update from the heatpump
// TODO: This isn't a problem *yet*, but sending all these packets every loop might start to cause some issues
// in
// certain configurations or setups. We may want to consider only asking for certain packets on a rarer
// cadence, depending on their utility (e.g. we dont need to check for errors every loop).
hp_bridge_.send_packet(
GetRequestPacket::get_settings_instance()); // Needs to be done before status packet for mode logic to work
if (in_discovery_ || run_state_received_) { hp_bridge_.send_packet(GetRequestPacket::get_runstate_instance()); }
hp_bridge_.send_packet(GetRequestPacket::get_status_instance());
hp_bridge_.send_packet(GetRequestPacket::get_current_temp_instance());
hp_bridge_.send_packet(GetRequestPacket::get_error_info_instance());)
if (in_discovery_) {
// After criteria met, exit discovery mode
// Currently this is either 5 updates or a successful RunState response.
if (discovery_updates_++ > 5 || run_state_received_) {
ESP_LOGD(TAG, "Discovery complete.");
in_discovery_ = false;
if (!run_state_received_) {
ESP_LOGI(TAG, "RunState packets not supported.");
}
}
}
}
void MitsubishiUART::do_publish_() { publish_state(); }
bool MitsubishiUART::select_temperature_source(const std::string &state) {
// TODO: Possibly check to see if state is available from the select options? (Might be a bit redundant)
current_temperature_source_ = state;
// Reset the timeout for received temperature (without this, the menu dropdown will switch back to Internal
// temporarily)
last_received_temperature_ = millis();
// If we've switched to internal, let the HP know right away
if (TEMPERATURE_SOURCE_INTERNAL == state) {
IFACTIVE(hp_bridge_.send_packet(RemoteTemperatureSetRequestPacket().use_internal_temperature());)
}
return true;
}
bool MitsubishiUART::select_vane_position(const std::string &state) {
IFNOTACTIVE(return false;) // Skip this if we're not in active mode
SettingsSetRequestPacket::VaneByte position_byte = SettingsSetRequestPacket::VANE_AUTO;
// NOTE: Annoyed that C++ doesn't have switches for strings, but since this is going to be called
// infrequently, this is probably a better solution than over-optimizing via maps or something
if (state == "Auto") {
position_byte = SettingsSetRequestPacket::VANE_AUTO;
} else if (state == "1") {
position_byte = SettingsSetRequestPacket::VANE_1;
} else if (state == "2") {
position_byte = SettingsSetRequestPacket::VANE_2;
} else if (state == "3") {
position_byte = SettingsSetRequestPacket::VANE_3;
} else if (state == "4") {
position_byte = SettingsSetRequestPacket::VANE_4;
} else if (state == "5") {
position_byte = SettingsSetRequestPacket::VANE_5;
} else if (state == "Swing") {
position_byte = SettingsSetRequestPacket::VANE_SWING;
} else {
ESP_LOGW(TAG, "Unknown vane position %s", state.c_str());
return false;
}
hp_bridge_.send_packet(SettingsSetRequestPacket().set_vane(position_byte));
return true;
}
bool MitsubishiUART::select_horizontal_vane_position(const std::string &state) {
IFNOTACTIVE(return false;) // Skip this if we're not in active mode
SettingsSetRequestPacket::HorizontalVaneByte position_byte = SettingsSetRequestPacket::HV_CENTER;
// NOTE: Annoyed that C++ doesn't have switches for strings, but since this is going to be called
// infrequently, this is probably a better solution than over-optimizing via maps or something
if (state == "Auto") {
position_byte = SettingsSetRequestPacket::HV_AUTO;
} else if (state == "<<") {
position_byte = SettingsSetRequestPacket::HV_LEFT_FULL;
} else if (state == "<") {
position_byte = SettingsSetRequestPacket::HV_LEFT;
} else if (state == "|") {
position_byte = SettingsSetRequestPacket::HV_CENTER;
} else if (state == ">") {
position_byte = SettingsSetRequestPacket::HV_RIGHT;
} else if (state == ">>") {
position_byte = SettingsSetRequestPacket::HV_RIGHT_FULL;
} else if (state == "<>") {
position_byte = SettingsSetRequestPacket::HV_SPLIT;
} else if (state == "Swing") {
position_byte = SettingsSetRequestPacket::HV_SWING;
} else {
ESP_LOGW(TAG, "Unknown horizontal vane position %s", state.c_str());
return false;
}
hp_bridge_.send_packet(SettingsSetRequestPacket().set_horizontal_vane(position_byte));
return true;
}
// Called by temperature_source sensors to report values. Will only take action if the currentTemperatureSource
// matches the incoming source. Specifically this means that we are not storing any values
// for sensors other than the current source, and selecting a different source won't have any
// effect until that source reports a temperature.
// TODO: ? Maybe store all temperatures (and report on them using internal sensors??) so that selecting a new
// source takes effect immediately? Only really needed if source sensors are configured with very slow update times.
void MitsubishiUART::temperature_source_report(const std::string &temperature_source, const float &v) {
ESP_LOGI(TAG, "Received temperature from %s of %f. (Current source: %s)", temperature_source.c_str(), v,
current_temperature_source_.c_str());
// Only proceed if the incomming source matches our chosen source.
if (current_temperature_source_ == temperature_source) {
// Reset the timeout for received temperature
last_received_temperature_ = millis();
temperature_source_timeout_ = false;
// Tell the heat pump about the temperature asap, but don't worry about setting it locally, the next update() will
// get it
IFACTIVE(RemoteTemperatureSetRequestPacket pkt = RemoteTemperatureSetRequestPacket(); pkt.set_remote_temperature(v);
hp_bridge_.send_packet(pkt);)
// If we've changed the select to reflect a temporary reversion to a different source, change it back.
for (auto *listener : listeners_) {
listener->temperature_source_change(current_temperature_source_);
}
}
}
void MitsubishiUART::reset_filter_status() {
ESP_LOGI(TAG, "Received a request to reset the filter status.");
IFNOTACTIVE(return;)
SetRunStatePacket pkt = SetRunStatePacket();
pkt.set_filter_reset(true);
hp_bridge_.send_packet(pkt);
}
} // namespace mitsubishi_itp
} // namespace esphome

View file

@ -0,0 +1,182 @@
#pragma once
#include "esphome/core/application.h"
#include "esphome/core/component.h"
#include "esphome/components/uart/uart.h"
#ifdef USE_TIME
#include "esphome/components/time/real_time_clock.h"
#endif
#include "esphome/components/climate/climate.h"
#include "mitp_listener.h"
#include "mitp_packet.h"
#include "mitp_bridge.h"
#include "mitp_mhk.h"
#include <map>
namespace esphome {
namespace mitsubishi_itp {
static constexpr char TAG[] = "mitsubishi_itp";
const uint8_t MITP_MIN_TEMP = 16; // Degrees C
const uint8_t MITP_MAX_TEMP = 31; // Degrees C
const float MITP_TEMPERATURE_STEP = 0.5;
const std::string TEMPERATURE_SOURCE_INTERNAL = "Internal";
const std::string TEMPERATURE_SOURCE_THERMOSTAT = "Thermostat";
const uint32_t TEMPERATURE_SOURCE_TIMEOUT_MS = 420000; // (7min) The heatpump will revert on its own in ~10min
class MitsubishiUART : public PollingComponent, public climate::Climate, public PacketProcessor {
public:
/**
* Create a new MitsubishiUART with the specified esphome::uart::UARTComponent.
*/
MitsubishiUART(uart::UARTComponent *hp_uart_comp);
// Used to restore state of previous MITP-specific settings (like temperature source or pass-thru mode)
// Most other climate-state is preserved by the heatpump itself and will be retrieved after connection
void setup() override;
// Called repeatedly (used for UART receiving/forwarding)
void loop() override;
// Called periodically as PollingComponent (used for UART sending periodically)
void update() override;
// Returns default traits for MITP
climate::ClimateTraits traits() override { return climate_traits_; }
// Returns a reference to traits for MITP to be used during configuration
// TODO: Maybe replace this with specific functions for the traits needed in configuration (a la the override
// fuctions)
climate::ClimateTraits &config_traits() { return climate_traits_; }
// Dumps some configuration data that we may have missed in the real-time logs
void dump_config() override;
// Called to instruct a change of the climate controls
void control(const climate::ClimateCall &call) override;
// Set thermostat UART component
void set_thermostat_uart(uart::UARTComponent *uart);
// Listener-sensors
void register_listener(MITPListener *listener) { this->listeners_.push_back(listener); }
// Returns true if select was valid (even if not yet successful) to indicate select component
// should optimistically publish
bool select_temperature_source(const std::string &state);
bool select_vane_position(const std::string &state);
bool select_horizontal_vane_position(const std::string &state);
// Used by external sources to report a temperature
void temperature_source_report(const std::string &temperature_source, const float &v);
// Button triggers
void reset_filter_status();
// Turns on or off actively sending packets
void set_active_mode(const bool active) { active_mode_ = active; };
// Turns on or off Kumo emulation mode
void set_enhanced_mhk_support(const bool supports) { enhanced_mhk_support_ = supports; }
#ifdef USE_TIME
void set_time_source(time::RealTimeClock *rtc) { time_source_ = rtc; }
#endif
protected:
void route_packet_(const Packet &packet);
void process_packet(const Packet &packet) override;
void process_packet(const ConnectRequestPacket &packet) override;
void process_packet(const ConnectResponsePacket &packet) override;
void process_packet(const CapabilitiesRequestPacket &packet) override;
void process_packet(const CapabilitiesResponsePacket &packet) override;
void process_packet(const GetRequestPacket &packet) override;
void process_packet(const SettingsGetResponsePacket &packet) override;
void process_packet(const CurrentTempGetResponsePacket &packet) override;
void process_packet(const StatusGetResponsePacket &packet) override;
void process_packet(const RunStateGetResponsePacket &packet) override;
void process_packet(const ErrorStateGetResponsePacket &packet) override;
void process_packet(const SettingsSetRequestPacket &packet) override;
void process_packet(const RemoteTemperatureSetRequestPacket &packet) override;
void process_packet(const ThermostatSensorStatusPacket &packet) override;
void process_packet(const ThermostatHelloPacket &packet) override;
void process_packet(const ThermostatStateUploadPacket &packet) override;
void process_packet(const ThermostatAASetRequestPacket &packet) override;
void process_packet(const SetResponsePacket &packet) override;
void handle_thermostat_state_download_request(const GetRequestPacket &packet) override;
void handle_thermostat_ab_get_request(const GetRequestPacket &packet) override;
void do_publish_();
private:
// Default climate_traits for MITP
climate::ClimateTraits climate_traits_ = []() -> climate::ClimateTraits {
climate::ClimateTraits ct = climate::ClimateTraits();
ct.set_supports_action(true);
ct.set_supports_current_temperature(true);
ct.set_supports_two_point_target_temperature(false);
ct.set_visual_min_temperature(MITP_MIN_TEMP);
ct.set_visual_max_temperature(MITP_MAX_TEMP);
ct.set_visual_temperature_step(MITP_TEMPERATURE_STEP);
return ct;
}();
// UARTComponent connected to heatpump
const uart::UARTComponent &hp_uart_;
// UART packet wrapper for heatpump
HeatpumpBridge hp_bridge_;
// UARTComponent connected to thermostat
uart::UARTComponent *ts_uart_ = nullptr;
// UART packet wrapper for heatpump
std::unique_ptr<ThermostatBridge> ts_bridge_ = nullptr;
// Are we connected to the heatpump?
bool hp_connected_ = false;
// Should we call publish on the next update?
bool publish_on_update_ = false;
// Are we still discovering information about the device?
bool in_discovery_ = true;
// Number of times update() has been called in discovery mode
size_t discovery_updates_ = 0;
optional<CapabilitiesResponsePacket> capabilities_cache_;
bool capabilities_requested_ = false;
// Have we received at least one RunState response?
bool run_state_received_ = false;
// Time Source
#ifdef USE_TIME
time::RealTimeClock *time_source_ = nullptr;
#endif
// Listener-sensors
std::vector<MITPListener *> listeners_{};
template<typename T> void alert_listeners_(const T &packet) const {
for (auto *listener : this->listeners_) {
listener->process_packet(packet);
}
}
// Temperature select extras
std::string current_temperature_source_ = TEMPERATURE_SOURCE_INTERNAL;
uint32_t last_received_temperature_ = millis();
bool temperature_source_timeout_ = false; // Has the current source timed out?
void send_if_active_(const Packet &packet);
bool active_mode_ = true;
// used to track whether to support/handle the enhanced MHK protocol packets
bool enhanced_mhk_support_ = false;
MHKState mhk_state_;
};
} // namespace mitsubishi_itp
} // namespace esphome

View file

@ -0,0 +1,49 @@
wifi:
ssid: MySSID
password: password1
api:
time:
- platform: homeassistant
id: homeassistant_time
timezone: America/Los_Angeles
# Temporarily commented out until this component can be merged
# select:
# - platform: mitsubishi_itp
# temperature_source:
# name: "Temperature Source"
# sources:
# - fake_temp
sensor:
- platform: template
id: fake_temp
name: "Fake Temperature"
lambda: |-
return 20.5;
uart:
- id: hp_uart
baud_rate: 2400
parity: EVEN
rx_pin:
number: GPIO0
tx_pin:
number: GPIO1
- id: tstat_uart
baud_rate: 2400
parity: EVEN
rx_pin:
number: GPIO3
tx_pin:
number: GPIO4
climate:
- platform: mitsubishi_itp
name: "Climate"
uart_heatpump: hp_uart
uart_thermostat: tstat_uart
time_id: homeassistant_time
update_interval: 12s

View file

@ -0,0 +1 @@
<<: !include common.yaml

View file

@ -0,0 +1 @@
<<: !include common.yaml

View file

@ -0,0 +1 @@
<<: !include common.yaml

View file

@ -0,0 +1 @@
<<: !include common.yaml

View file

@ -0,0 +1 @@
<<: !include common.yaml