mirror of
https://github.com/esphome/esphome.git
synced 2024-11-30 10:44:13 +01:00
Initial commit of core component
This commit is contained in:
parent
80a0f13722
commit
0bb5419851
23 changed files with 2860 additions and 0 deletions
|
@ -253,6 +253,7 @@ esphome/components/mics_4514/* @jesserockz
|
||||||
esphome/components/midea/* @dudanov
|
esphome/components/midea/* @dudanov
|
||||||
esphome/components/midea_ir/* @dudanov
|
esphome/components/midea_ir/* @dudanov
|
||||||
esphome/components/mitsubishi/* @RubyBailey
|
esphome/components/mitsubishi/* @RubyBailey
|
||||||
|
esphome/components/mitsubishi_itp/* @KazWolfe @Sammy1Am
|
||||||
esphome/components/mlx90393/* @functionpointer
|
esphome/components/mlx90393/* @functionpointer
|
||||||
esphome/components/mlx90614/* @jesserockz
|
esphome/components/mlx90614/* @jesserockz
|
||||||
esphome/components/mmc5603/* @benhoff
|
esphome/components/mmc5603/* @benhoff
|
||||||
|
|
39
esphome/components/mitsubishi_itp/__init__.py
Normal file
39
esphome/components/mitsubishi_itp/__init__.py
Normal 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))
|
126
esphome/components/mitsubishi_itp/climate.py
Normal file
126
esphome/components/mitsubishi_itp/climate.py
Normal 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)
|
||||||
|
)
|
217
esphome/components/mitsubishi_itp/mitp_bridge.cpp
Normal file
217
esphome/components/mitsubishi_itp/mitp_bridge.cpp
Normal 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
|
57
esphome/components/mitsubishi_itp/mitp_bridge.h
Normal file
57
esphome/components/mitsubishi_itp/mitp_bridge.h
Normal 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
|
21
esphome/components/mitsubishi_itp/mitp_listener.h
Normal file
21
esphome/components/mitsubishi_itp/mitp_listener.h
Normal 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
|
15
esphome/components/mitsubishi_itp/mitp_mhk.h
Normal file
15
esphome/components/mitsubishi_itp/mitp_mhk.h
Normal 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
|
463
esphome/components/mitsubishi_itp/mitp_packet-derived.cpp
Normal file
463
esphome/components/mitsubishi_itp/mitp_packet-derived.cpp
Normal 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(×tamp);
|
||||||
|
|
||||||
|
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
|
77
esphome/components/mitsubishi_itp/mitp_packet.cpp
Normal file
77
esphome/components/mitsubishi_itp/mitp_packet.cpp
Normal 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
|
573
esphome/components/mitsubishi_itp/mitp_packet.h
Normal file
573
esphome/components/mitsubishi_itp/mitp_packet.h
Normal 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
|
70
esphome/components/mitsubishi_itp/mitp_rawpacket.cpp
Normal file
70
esphome/components/mitsubishi_itp/mitp_rawpacket.cpp
Normal 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
|
122
esphome/components/mitsubishi_itp/mitp_rawpacket.h
Normal file
122
esphome/components/mitsubishi_itp/mitp_rawpacket.h
Normal 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
|
87
esphome/components/mitsubishi_itp/mitp_utils.h
Normal file
87
esphome/components/mitsubishi_itp/mitp_utils.h
Normal 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
|
120
esphome/components/mitsubishi_itp/mitsubishi_itp-climatecall.cpp
Normal file
120
esphome/components/mitsubishi_itp/mitsubishi_itp-climatecall.cpp
Normal 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
|
|
@ -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
|
273
esphome/components/mitsubishi_itp/mitsubishi_itp.cpp
Normal file
273
esphome/components/mitsubishi_itp/mitsubishi_itp.cpp
Normal 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
|
182
esphome/components/mitsubishi_itp/mitsubishi_itp.h
Normal file
182
esphome/components/mitsubishi_itp/mitsubishi_itp.h
Normal 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
|
49
tests/components/mitsubishi_itp/common.yaml
Normal file
49
tests/components/mitsubishi_itp/common.yaml
Normal 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
|
1
tests/components/mitsubishi_itp/test.esp32-ard.yaml
Normal file
1
tests/components/mitsubishi_itp/test.esp32-ard.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<<: !include common.yaml
|
1
tests/components/mitsubishi_itp/test.esp32-c3-ard.yaml
Normal file
1
tests/components/mitsubishi_itp/test.esp32-c3-ard.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<<: !include common.yaml
|
1
tests/components/mitsubishi_itp/test.esp32-c3-idf.yaml
Normal file
1
tests/components/mitsubishi_itp/test.esp32-c3-idf.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<<: !include common.yaml
|
1
tests/components/mitsubishi_itp/test.esp32-idf.yaml
Normal file
1
tests/components/mitsubishi_itp/test.esp32-idf.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<<: !include common.yaml
|
1
tests/components/mitsubishi_itp/test.esp8266-ard.yaml
Normal file
1
tests/components/mitsubishi_itp/test.esp8266-ard.yaml
Normal file
|
@ -0,0 +1 @@
|
||||||
|
<<: !include common.yaml
|
Loading…
Reference in a new issue