Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Kumo Emulation Mode #30

Merged
merged 15 commits into from
Jun 25, 2024
43 changes: 39 additions & 4 deletions esphome/components/mitsubishi_itp/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from esphome.components import (
climate,
uart,
time,
sensor,
binary_sensor,
button,
Expand All @@ -19,11 +20,13 @@
CONF_SUPPORTED_MODES,
DEVICE_CLASS_TEMPERATURE,
DEVICE_CLASS_FREQUENCY,
DEVICE_CLASS_HUMIDITY,
ENTITY_CATEGORY_CONFIG,
ENTITY_CATEGORY_NONE,
STATE_CLASS_MEASUREMENT,
UNIT_CELSIUS,
UNIT_HERTZ,
UNIT_PERCENT,
)
from esphome.core import coroutine

Expand All @@ -39,6 +42,7 @@
]
DEPENDENCIES = [
"uart",
"time",
"climate",
"sensor",
"binary_sensor",
Expand All @@ -49,8 +53,11 @@

CONF_UART_HEATPUMP = "uart_heatpump"
CONF_UART_THERMOSTAT = "uart_thermostat"
CONF_TIME_SOURCE = "time_source"

CONF_THERMOSTAT_TEMPERATURE = "thermostat_temperature"
CONF_THERMOSTAT_HUMIDITY = "thermostat_humidity"
CONF_THERMOSTAT_BATTERY = "thermostat_battery"
CONF_ERROR_CODE = "error_code"
CONF_ISEE_STATUS = "isee_status"

Expand All @@ -67,6 +74,7 @@
)

CONF_DISABLE_ACTIVE_MODE = "disable_active_mode"
CONF_ENABLE_KUMO_EMULATION = "kumo_emulation" # EXPERIMENTAL FEATURE - Enables Kumo packet handling.
KazWolfe marked this conversation as resolved.
Show resolved Hide resolved

DEFAULT_POLLING_INTERVAL = "5s"

Expand Down Expand Up @@ -104,6 +112,7 @@
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_SOURCE): cv.use_id(time.RealTimeClock),
# Overwrite name from ENTITY_BASE_SCHEMA with "Climate" as default
cv.Optional(CONF_NAME, default="Climate"): cv.Any(
cv.All(
Expand All @@ -127,6 +136,7 @@
cv.use_id(sensor.Sensor)
),
cv.Optional(CONF_DISABLE_ACTIVE_MODE, default=False): cv.boolean,
cv.Optional(CONF_ENABLE_KUMO_EMULATION, default=False): cv.boolean,
}
).extend(cv.polling_component_schema(DEFAULT_POLLING_INTERVAL))

Expand Down Expand Up @@ -154,6 +164,23 @@
),
sensor.register_sensor,
),
CONF_THERMOSTAT_HUMIDITY: (
"Thermostat Humidity",
sensor.sensor_schema(
unit_of_measurement=UNIT_PERCENT,
device_class=DEVICE_CLASS_HUMIDITY,
state_class=STATE_CLASS_MEASUREMENT,
accuracy_decimals=0,
),
sensor.register_sensor
),
CONF_THERMOSTAT_BATTERY: (
"Thermostat Battery",
text_sensor.text_sensor_schema(
icon="mdi:battery",
),
text_sensor.register_text_sensor
),
"compressor_frequency": (
"Compressor Frequency",
sensor.sensor_schema(
Expand Down Expand Up @@ -308,8 +335,14 @@ async def to_code(config):
# Add sensor as source
SELECTS[CONF_TEMPERATURE_SOURCE_SELECT][2].append("Thermostat")

# Traits
# If RTC defined
if CONF_TIME_SOURCE in config:
rtc_component = await cg.get_variable(config[CONF_TIME_SOURCE])
cg.add(getattr(muart_component, "set_time_source")(rtc_component))
elif CONF_UART_THERMOSTAT in config and not config.get(CONF_ENABLE_KUMO_EMULATION):
KazWolfe marked this conversation as resolved.
Show resolved Hide resolved
raise cv.RequiredFieldInvalid(f"{CONF_TIME_SOURCE} is required if {CONF_ENABLE_KUMO_EMULATION} is set.")

# Traits
traits = muart_component.config_traits()

if CONF_SUPPORTED_MODES in config:
Expand All @@ -329,9 +362,8 @@ async def to_code(config):
registration_function,
) in SENSORS.items():
# Only add the thermostat temp if we have a TS_UART
if (sensor_designator == CONF_THERMOSTAT_TEMPERATURE) and (
CONF_UART_THERMOSTAT not in config
):
if ((CONF_UART_THERMOSTAT not in config) and
(sensor_designator in [CONF_THERMOSTAT_TEMPERATURE, CONF_THERMOSTAT_HUMIDITY, CONF_THERMOSTAT_BATTERY])):
continue

sensor_conf = config[CONF_SENSORS][sensor_designator]
Expand Down Expand Up @@ -390,3 +422,6 @@ async def to_code(config):
# Debug Settings
if dam_conf := config.get(CONF_DISABLE_ACTIVE_MODE):
cg.add(getattr(muart_component, "set_active_mode")(not dam_conf))

if kumo_emulation := config.get(CONF_ENABLE_KUMO_EMULATION):
cg.add(getattr(muart_component, "set_kumo_emulation_mode")(kumo_emulation))
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,27 @@ void MitsubishiUART::control(const climate::ClimateCall &call) {
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->last_cool_setpoint_ = target_temperature;
break;
case climate::CLIMATE_MODE_HEAT:
this->last_heat_setpoint_ = target_temperature;
break;
case climate::CLIMATE_MODE_HEAT_COOL:
if (this->get_traits().get_supports_two_point_target_temperature()) {
this->last_heat_setpoint_ = target_temperature_low;
this->last_cool_setpoint_ = target_temperature_high;
} else {
// this->last_heat_setpoint_ = target_temperature;
// this->last_cool_setpoint_ = target_temperature;
}
KazWolfe marked this conversation as resolved.
Show resolved Hide resolved
default:
break;
}
}

// TODO:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,13 @@ void MitsubishiUART::process_packet(const ConnectResponsePacket &packet) {
ESP_LOGI(TAG, "Heatpump connected.");
};

void MitsubishiUART::process_packet(const ExtendedConnectRequestPacket &packet) {
void MitsubishiUART::process_packet(const BaseCapabilitiesRequestPacket &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, "Processing %s", packet.to_string().c_str());
route_packet_(packet);
};
void MitsubishiUART::process_packet(const ExtendedConnectResponsePacket &packet) {
void MitsubishiUART::process_packet(const BaseCapabilitiesResponsePacket &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.
Expand All @@ -54,10 +54,17 @@ void MitsubishiUART::process_packet(const ExtendedConnectResponsePacket &packet)

void MitsubishiUART::process_packet(const GetRequestPacket &packet) {
ESP_LOGV(TAG, "Processing %s", packet.to_string().c_str());
route_packet_(packet);
// These are just requests for information from the thermostat. For now, nothing to be done
// except route them. In the future, we could use this to inject information for the thermostat
// or use a cached value.

switch (packet.get_requested_command()) {
case GetCommand::KUMO_GET_ADAPTER_STATE:
this->handle_kumo_adapter_state_get_request(packet);
break;
case GetCommand::KUMO_AB:
this->handle_kumo_aa_get_request(packet);
break;
default:
route_packet_(packet);
}
}

void MitsubishiUART::process_packet(const SettingsGetResponsePacket &packet) {
Expand Down Expand Up @@ -111,6 +118,18 @@ void MitsubishiUART::process_packet(const SettingsGetResponsePacket &packet) {
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->last_cool_setpoint_ = target_temperature;
break;
case climate::CLIMATE_MODE_HEAT:
this->last_heat_setpoint_ = target_temperature;
break;
default:
break;
}

KazWolfe marked this conversation as resolved.
Show resolved Hide resolved
// Fan
static bool fan_changed = false;
switch (packet.get_fan()) {
Expand Down Expand Up @@ -333,6 +352,13 @@ void MitsubishiUART::process_packet(const ErrorStateGetResponsePacket &packet) {
publish_on_update_ |= (old_error_code != error_code_sensor_->raw_state);
}

void MitsubishiUART::process_packet(const SettingsSetRequestPacket &packet) {
ESP_LOGV(TAG, "Processing %s", packet.to_string().c_str());

// forward this packet as-is; we're just intercepting to log.
route_packet_(packet);
}

void MitsubishiUART::process_packet(const RemoteTemperatureSetRequestPacket &packet) {
ESP_LOGV(TAG, "Processing %s", packet.to_string().c_str());

Expand All @@ -355,12 +381,87 @@ void MitsubishiUART::process_packet(const RemoteTemperatureSetRequestPacket &pac

publish_on_update_ |= (old_thermostat_temp != thermostat_temperature_sensor_->raw_state);
}
};
}

void MitsubishiUART::process_packet(const KumoThermostatSensorStatusPacket &packet) {
if (!kumo_emulation_mode_) return;

ESP_LOGV(TAG, "Processing inbound %s", packet.to_string().c_str());
KazWolfe marked this conversation as resolved.
Show resolved Hide resolved

if (thermostat_humidity_sensor_ && packet.get_flags() & 0x04) {
const float old_humidity = thermostat_humidity_sensor_->raw_state;
thermostat_humidity_sensor_->raw_state = packet.get_indoor_humidity_percent();
publish_on_update_ |= (old_humidity != thermostat_humidity_sensor_->raw_state);
}

if (thermostat_battery_sensor_ && packet.get_flags() & 0x08) {
const auto old_battery = thermostat_battery_sensor_->raw_state;
thermostat_battery_sensor_->raw_state = THERMOSTAT_BATTERY_STATE_NAMES[packet.get_thermostat_battery_state()];
publish_on_update_ |= (old_battery != thermostat_battery_sensor_->raw_state);
}

ts_bridge_->send_packet(SetResponsePacket());
}

void MitsubishiUART::process_packet(const KumoThermostatHelloPacket &packet) {
if (!kumo_emulation_mode_) return;

ESP_LOGV(TAG, "Processing inbound %s", packet.to_string().c_str());

ts_bridge_->send_packet(SetResponsePacket());
}

void MitsubishiUART::process_packet(const KumoThermostatStateSyncPacket &packet) {
if (!kumo_emulation_mode_) return;

ESP_LOGV(TAG, "Processing inbound %s", packet.to_string().c_str());

if (packet.get_flags() & 0x08) this->last_heat_setpoint_ = packet.get_heat_setpoint();
if (packet.get_flags() & 0x10) this->last_cool_setpoint_ = packet.get_cool_setpoint();

ts_bridge_->send_packet(SetResponsePacket());
}

void MitsubishiUART::process_packet(const KumoAASetRequestPacket &packet) {
if (!kumo_emulation_mode_) return;

ESP_LOGV(TAG, "Processing inbound KumoAASetRequestPacket: %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 (Kumo)
void MitsubishiUART::handle_kumo_adapter_state_get_request(const GetRequestPacket &packet) {
if (!kumo_emulation_mode_) return;

auto response = KumoCloudStateSyncPacket();

response.set_heat_setpoint(this->last_heat_setpoint_);
response.set_cool_setpoint(this->last_cool_setpoint_);
KazWolfe marked this conversation as resolved.
Show resolved Hide resolved

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:00 Z
}

ts_bridge_->send_packet(response);
}

void MitsubishiUART::handle_kumo_aa_get_request(const GetRequestPacket &packet) {
if (!kumo_emulation_mode_) return;

auto response = KumoABGetRequestPacket();
KazWolfe marked this conversation as resolved.
Show resolved Hide resolved

ts_bridge_->send_packet(response);
}

} // namespace mitsubishi_itp
} // namespace esphome
15 changes: 14 additions & 1 deletion esphome/components/mitsubishi_itp/mitsubishi_uart.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,10 @@ void MitsubishiUART::dump_config() {
if (capabilities_cache_.has_value()) {
ESP_LOGCONFIG(TAG, "Discovered Capabilities: %s", capabilities_cache_.value().to_string().c_str());
}

if (kumo_emulation_mode_) {
ESP_LOGCONFIG(TAG, "Kumo Emulation Mode is ENABLED! This is an *experimental mode* and things may break.");
}
}

// Set thermostat UART component
Expand Down Expand Up @@ -146,7 +150,7 @@ void MitsubishiUART::update() {
// 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(ExtendedConnectRequestPacket::instance()); this->capabilities_requested_ = true;)
IFACTIVE(hp_bridge_.send_packet(BaseCapabilitiesRequestPacket::instance()); this->capabilities_requested_ = true;)
}

// Before requesting additional updates, publish any changes waiting from packets received
Expand Down Expand Up @@ -203,6 +207,11 @@ void MitsubishiUART::do_publish_() {
ESP_LOGI(TAG, "Outdoor temp differs, do publish");
outdoor_temperature_sensor_->publish_state(outdoor_temperature_sensor_->raw_state);
}
if (thermostat_humidity_sensor_ &&
(thermostat_humidity_sensor_->raw_state != thermostat_humidity_sensor_->state)) {
ESP_LOGI(TAG, "Thermostat humidity differs, do publish");
thermostat_humidity_sensor_->publish_state(thermostat_humidity_sensor_->raw_state);
}
if (compressor_frequency_sensor_ &&
(compressor_frequency_sensor_->raw_state != compressor_frequency_sensor_->state)) {
ESP_LOGI(TAG, "Compressor frequency differs, do publish");
Expand All @@ -216,6 +225,10 @@ void MitsubishiUART::do_publish_() {
ESP_LOGI(TAG, "Error code state differs, do publish");
error_code_sensor_->publish_state(error_code_sensor_->raw_state);
}
if (thermostat_battery_sensor_ && (thermostat_battery_sensor_->raw_state != thermostat_battery_sensor_->state)) {
ESP_LOGI(TAG, "Thermostat battery state differs, do publish");
thermostat_battery_sensor_->publish_state(thermostat_battery_sensor_->raw_state);
}

// Binary sensors automatically dedup publishes (I think) and so will only actually publish on change
filter_status_sensor_->publish_state(filter_status_sensor_->state);
Expand Down
Loading