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
58 changes: 50 additions & 8 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 @@ -36,21 +39,20 @@
"binary_sensor",
"button",
"text_sensor",
"time",
]
DEPENDENCIES = [
"uart",
"climate",
"sensor",
"binary_sensor",
"button",
"text_sensor",
"select",
]

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 +69,9 @@
)

CONF_DISABLE_ACTIVE_MODE = "disable_active_mode"
CONF_ENHANCED_MHK_SUPPORT = (
"enhanced_mhk" # EXPERIMENTAL. Will be set to default eventually.
)

DEFAULT_POLLING_INTERVAL = "5s"

Expand Down Expand Up @@ -104,6 +109,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 +133,7 @@
cv.use_id(sensor.Sensor)
),
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))

Expand Down Expand Up @@ -154,6 +161,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 +332,16 @@ 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 config.get(CONF_ENHANCED_MHK_SUPPORT):
raise cv.RequiredFieldInvalid(
f"{CONF_TIME_SOURCE} is required if {CONF_ENHANCED_MHK_SUPPORT} is set."
)

# Traits
traits = muart_component.config_traits()

if CONF_SUPPORTED_MODES in config:
Expand All @@ -329,8 +361,13 @@ 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

Expand Down Expand Up @@ -390,3 +427,8 @@ 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 enhanced_mhk_protocol := config.get(CONF_ENHANCED_MHK_SUPPORT):
cg.add(
getattr(muart_component, "set_enhanced_mhk_support")(enhanced_mhk_protocol)
)
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,28 @@ 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->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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ void MitsubishiUART::process_packet(const 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, "Processing %s", packet.to_string().c_str());
ESP_LOGV(TAG, "Passing through inbound %s", packet.to_string().c_str());
route_packet_(packet);
};
void MitsubishiUART::process_packet(const ConnectResponsePacket &packet) {
Expand All @@ -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 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, "Processing %s", packet.to_string().c_str());
ESP_LOGV(TAG, "Passing through inbound %s", packet.to_string().c_str());
route_packet_(packet);
};
void MitsubishiUART::process_packet(const ExtendedConnectResponsePacket &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.
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::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) {
Expand Down Expand Up @@ -111,6 +118,21 @@ 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->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()) {
Expand Down Expand Up @@ -333,6 +355,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, "Passing through inbound %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 +384,113 @@ void MitsubishiUART::process_packet(const RemoteTemperatureSetRequestPacket &pac

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

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());
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 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_);

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
}

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
Loading
Loading