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_ir/* @dudanov
|
||||
esphome/components/mitsubishi/* @RubyBailey
|
||||
esphome/components/mitsubishi_itp/* @KazWolfe @Sammy1Am
|
||||
esphome/components/mlx90393/* @functionpointer
|
||||
esphome/components/mlx90614/* @jesserockz
|
||||
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