From 86e7de8ca8d9053c818c43f1fe1dee1b8fca8d35 Mon Sep 17 00:00:00 2001 From: Jeroen Nijssen Date: Sun, 31 May 2026 20:15:29 +0200 Subject: [PATCH] Add climate action and status telemetry --- esphome/base.yaml | 13 +++ esphome/components/lg_controller/climate.py | 62 +++++++++++- .../components/lg_controller/lg-controller.h | 98 ++++++++++++++++++- 3 files changed, 169 insertions(+), 4 deletions(-) diff --git a/esphome/base.yaml b/esphome/base.yaml index 2ac8bb9..0561d23 100644 --- a/esphome/base.yaml +++ b/esphome/base.yaml @@ -105,6 +105,7 @@ climate: name: Error Code id: error_code icon: mdi:alert-circle-outline + accuracy_decimals: 0 pipe_temp_in: name: Pipe Temperature In id: pipe_temperature_in @@ -120,6 +121,18 @@ climate: id: pipe_temperature_out icon: mdi:thermometer unit_of_measurement: "°C" + humidity: + name: Humidity + id: humidity + fan_operation_time: + name: Fan Operation Time + id: fan_operation_time + indoor_unit_operation_time: + name: Indoor Unit Operation Time + id: indoor_unit_operation_time + precise_room_temperature: + name: Precise Room Temperature + id: precise_room_temperature defrost: name: Defrost id: defrost diff --git a/esphome/components/lg_controller/climate.py b/esphome/components/lg_controller/climate.py index 5677d1f..23d4065 100644 --- a/esphome/components/lg_controller/climate.py +++ b/esphome/components/lg_controller/climate.py @@ -2,7 +2,19 @@ import esphome.config_validation as cv from esphome import pins from esphome.components import binary_sensor, climate, number, select, sensor, switch, uart -from esphome.const import CONF_ID, CONF_RX_PIN, CONF_INTERNAL +from esphome.const import ( + CONF_ID, + CONF_RX_PIN, + CONF_INTERNAL, + DEVICE_CLASS_DURATION, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_CELSIUS, + UNIT_HOUR, + UNIT_PERCENT, +) CODEOWNERS = ["JanM321"] DEPENDENCIES = ["uart"] @@ -37,6 +49,10 @@ CONF_PIPE_TEMP_IN = "pipe_temp_in" CONF_PIPE_TEMP_MID = "pipe_temp_mid" CONF_PIPE_TEMP_OUT = "pipe_temp_out" +CONF_HUMIDITY = "humidity" +CONF_FAN_OPERATION_TIME = "fan_operation_time" +CONF_INDOOR_UNIT_OPERATION_TIME = "indoor_unit_operation_time" +CONF_PRECISE_ROOM_TEMPERATURE = "precise_room_temperature" CONF_DEFROST = "defrost" CONF_PREHEAT = "preheat" @@ -71,10 +87,34 @@ cv.Required(CONF_FAN_SPEED_HIGH): number.number_schema(LgNumber), cv.Required(CONF_SLEEP_TIMER): number.number_schema(LgNumber), - cv.Required(CONF_ERROR_CODE): sensor.sensor_schema(), + cv.Required(CONF_ERROR_CODE): sensor.sensor_schema(accuracy_decimals=0), cv.Required(CONF_PIPE_TEMP_IN): sensor.sensor_schema(), cv.Required(CONF_PIPE_TEMP_MID): sensor.sensor_schema(), cv.Required(CONF_PIPE_TEMP_OUT): sensor.sensor_schema(), + cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( + unit_of_measurement=UNIT_PERCENT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, + ), + cv.Optional(CONF_FAN_OPERATION_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_INDOOR_UNIT_OPERATION_TIME): sensor.sensor_schema( + unit_of_measurement=UNIT_HOUR, + accuracy_decimals=0, + device_class=DEVICE_CLASS_DURATION, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_PRECISE_ROOM_TEMPERATURE): sensor.sensor_schema( + unit_of_measurement=UNIT_CELSIUS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, + ), cv.Required(CONF_DEFROST): binary_sensor.binary_sensor_schema(), cv.Required(CONF_PREHEAT): binary_sensor.binary_sensor_schema(), @@ -120,6 +160,22 @@ async def to_code(config): pipe_temp_in = await sensor.new_sensor(config[CONF_PIPE_TEMP_IN]) pipe_temp_mid = await sensor.new_sensor(config[CONF_PIPE_TEMP_MID]) pipe_temp_out = await sensor.new_sensor(config[CONF_PIPE_TEMP_OUT]) + if CONF_HUMIDITY in config: + humidity = await sensor.new_sensor(config[CONF_HUMIDITY]) + else: + humidity = cg.nullptr + if CONF_FAN_OPERATION_TIME in config: + fan_operation_time = await sensor.new_sensor(config[CONF_FAN_OPERATION_TIME]) + else: + fan_operation_time = cg.nullptr + if CONF_INDOOR_UNIT_OPERATION_TIME in config: + indoor_unit_operation_time = await sensor.new_sensor(config[CONF_INDOOR_UNIT_OPERATION_TIME]) + else: + indoor_unit_operation_time = cg.nullptr + if CONF_PRECISE_ROOM_TEMPERATURE in config: + precise_room_temperature = await sensor.new_sensor(config[CONF_PRECISE_ROOM_TEMPERATURE]) + else: + precise_room_temperature = cg.nullptr defrost = await binary_sensor.new_binary_sensor(config[CONF_DEFROST]) preheat = await binary_sensor.new_binary_sensor(config[CONF_PREHEAT]) @@ -135,6 +191,8 @@ async def to_code(config): fan_speed_slow, fan_speed_low, fan_speed_medium, fan_speed_high, sleep_timer, error_code, pipe_temp_in, pipe_temp_mid, pipe_temp_out, + humidity, fan_operation_time, indoor_unit_operation_time, + precise_room_temperature, defrost, preheat, outdoor, auto_dry_active, purifier, internal_thermistor, auto_dry, config[CONF_FAHRENHEIT], config[CONF_IS_SLAVE_CONTROLLER]) diff --git a/esphome/components/lg_controller/lg-controller.h b/esphome/components/lg_controller/lg-controller.h index 6493f27..94c54c8 100644 --- a/esphome/components/lg_controller/lg-controller.h +++ b/esphome/components/lg_controller/lg-controller.h @@ -153,6 +153,10 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub esphome::sensor::Sensor& pipe_temp_in_; esphome::sensor::Sensor& pipe_temp_mid_; esphome::sensor::Sensor& pipe_temp_out_; + esphome::sensor::Sensor* humidity_; + esphome::sensor::Sensor* fan_operation_time_; + esphome::sensor::Sensor* indoor_unit_operation_time_; + esphome::sensor::Sensor* precise_room_temperature_; esphome::binary_sensor::BinarySensor& defrost_; esphome::binary_sensor::BinarySensor& preheat_; esphome::binary_sensor::BinarySensor& outdoor_; @@ -283,7 +287,7 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub supported_traits_.set_supported_modes(device_modes); supported_traits_.set_supported_fan_modes(fan_modes); supported_traits_.set_supported_swing_modes(swing_modes); - supported_traits_.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE); + supported_traits_.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION); supported_traits_.set_visual_min_temperature(MIN_TEMP_SETPOINT); supported_traits_.set_visual_max_temperature(MAX_TEMP_SETPOINT); supported_traits_.set_visual_current_temperature_step(fahrenheit_ ? 1 : 0.5); @@ -347,6 +351,10 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub sensor::Sensor* pipe_temp_in, sensor::Sensor* pipe_temp_mid, sensor::Sensor* pipe_temp_out, + sensor::Sensor* humidity, + sensor::Sensor* fan_operation_time, + sensor::Sensor* indoor_unit_operation_time, + sensor::Sensor* precise_room_temperature, binary_sensor::BinarySensor* defrost, binary_sensor::BinarySensor* preheat, binary_sensor::BinarySensor* outdoor, @@ -371,6 +379,10 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub pipe_temp_in_(*pipe_temp_in), pipe_temp_mid_(*pipe_temp_mid), pipe_temp_out_(*pipe_temp_out), + humidity_(humidity), + fan_operation_time_(fan_operation_time), + indoor_unit_operation_time_(indoor_unit_operation_time), + precise_room_temperature_(precise_room_temperature), defrost_(*defrost), preheat_(*preheat), outdoor_(*outdoor), @@ -467,6 +479,13 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub void control(const climate::ClimateCall &call) override { if (call.get_mode().has_value()) { this->mode = *call.get_mode(); + if (this->mode == climate::CLIMATE_MODE_OFF) { + this->action = climate::CLIMATE_ACTION_OFF; + } else if (this->mode == climate::CLIMATE_MODE_FAN_ONLY) { + this->action = climate::CLIMATE_ACTION_FAN; + } else if (this->action == climate::CLIMATE_ACTION_OFF) { + this->action = climate::CLIMATE_ACTION_IDLE; + } } if (call.get_target_temperature().has_value()) { this->target_temperature = *call.get_target_temperature(); @@ -486,6 +505,46 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub } private: + bool set_action_from_status_(bool outdoor_on, bool preheating, bool defrosting, uint8_t temperature_flags) { + climate::ClimateAction action = climate::CLIMATE_ACTION_IDLE; + if (this->mode == climate::CLIMATE_MODE_OFF) { + action = climate::CLIMATE_ACTION_OFF; + } else if (defrosting) { + action = climate::CLIMATE_ACTION_DEFROSTING; + } else if (this->mode == climate::CLIMATE_MODE_FAN_ONLY) { + action = climate::CLIMATE_ACTION_FAN; + } else if (!outdoor_on || preheating) { + action = climate::CLIMATE_ACTION_IDLE; + } else { + switch (this->mode) { + case climate::CLIMATE_MODE_COOL: + action = climate::CLIMATE_ACTION_COOLING; + break; + case climate::CLIMATE_MODE_DRY: + action = climate::CLIMATE_ACTION_DRYING; + break; + case climate::CLIMATE_MODE_HEAT: + action = climate::CLIMATE_ACTION_HEATING; + break; + case climate::CLIMATE_MODE_HEAT_COOL: + if (temperature_flags & 0x40) { + action = climate::CLIMATE_ACTION_COOLING; + } else if (temperature_flags & 0x80) { + action = climate::CLIMATE_ACTION_HEATING; + } + break; + default: + break; + } + } + + if (this->action == action) { + return false; + } + this->action = action; + return true; + } + // Sets position of vane index (1-4) to position (0-6). void set_vane_position(int index, int position) { if (index < 1 || index > 4) { @@ -935,6 +994,9 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub case 3: // 0xCB/AB/2B process_type_b_settings_message(*sender, buffer); break; + case 6: // 0xCE/AE/2E + process_type_e_status_message(*sender, buffer); + break; default: return; } @@ -958,7 +1020,8 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub // change. defrost_.publish_state(buffer[3] & 0x4); - preheat_.publish_state(buffer[3] & 0x8); + bool preheating = buffer[3] & 0x8; + preheat_.publish_state(preheating); if (sender == MessageSender::Unit) { error_code_.publish_state(buffer[11]); @@ -1009,10 +1072,16 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub // changes we still have to send (or are sending) to the AC. if (pending_status_change_) { ESP_LOGD(TAG, "ignoring because pending change"); + if (set_action_from_status_(outdoor_.state, preheating, defrost_.state, buffer[7])) { + publish_state(); + } return; } if (pending_send_ == PendingSendKind::Status) { ESP_LOGD(TAG, "ignoring because pending send"); + if (set_action_from_status_(outdoor_.state, preheating, defrost_.state, buffer[7])) { + publish_state(); + } return; } @@ -1104,6 +1173,7 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub sleep_timer_.publish_state(minutes); } + set_action_from_status_(outdoor_.state, preheating, defrost_.state, buffer[7]); publish_state(); } @@ -1273,6 +1343,29 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub } } + static uint16_t decode_uint16_(const uint8_t* buffer, size_t offset) { + return (uint16_t(buffer[offset]) << 8) | buffer[offset + 1]; + } + + void process_type_e_status_message(MessageSender sender, const uint8_t* buffer) { + if (sender == MessageSender::Slave || buffer[1] != 0x80) { + return; + } + + if (humidity_ != nullptr) { + humidity_->publish_state(buffer[2]); + } + if (fan_operation_time_ != nullptr) { + fan_operation_time_->publish_state(decode_uint16_(buffer, 3)); + } + if (indoor_unit_operation_time_ != nullptr) { + indoor_unit_operation_time_->publish_state(decode_uint16_(buffer, 6)); + } + if (precise_room_temperature_ != nullptr) { + precise_room_temperature_->publish_state(float(buffer[10]) + float(buffer[11]) / 10.0f); + } + } + void update() { ESP_LOGD(TAG, "update"); @@ -1338,6 +1431,7 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub sleep_timer_.publish_state(0); ignore_sleep_timer_callback_ = false; this->mode = climate::CLIMATE_MODE_OFF; + this->action = climate::CLIMATE_ACTION_OFF; pending_status_change_ = true; publish_state(); } else if (optional minutes = get_sleep_timer_minutes()) {