From c89b77ac20e6825a7ef3651050677baf948864be Mon Sep 17 00:00:00 2001 From: Jeroen Nijssen Date: Sun, 31 May 2026 19:03:59 +0200 Subject: [PATCH 1/2] Add LG power consumption sensors --- esphome/base.yaml | 6 +++ esphome/components/lg_controller/climate.py | 35 ++++++++++++++++- .../components/lg_controller/lg-controller.h | 39 +++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/esphome/base.yaml b/esphome/base.yaml index 2ac8bb9..875b3ec 100644 --- a/esphome/base.yaml +++ b/esphome/base.yaml @@ -120,6 +120,12 @@ climate: id: pipe_temperature_out icon: mdi:thermometer unit_of_measurement: "°C" + power_consumption: + name: Power Consumption + id: power_consumption + current_power: + name: Current Power + id: current_power defrost: name: Defrost id: defrost diff --git a/esphome/components/lg_controller/climate.py b/esphome/components/lg_controller/climate.py index 5677d1f..bd8dc26 100644 --- a/esphome/components/lg_controller/climate.py +++ b/esphome/components/lg_controller/climate.py @@ -2,7 +2,17 @@ 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_ENERGY, + DEVICE_CLASS_POWER, + STATE_CLASS_MEASUREMENT, + STATE_CLASS_TOTAL_INCREASING, + UNIT_KILOWATT_HOURS, + UNIT_WATT, +) CODEOWNERS = ["JanM321"] DEPENDENCIES = ["uart"] @@ -37,6 +47,8 @@ CONF_PIPE_TEMP_IN = "pipe_temp_in" CONF_PIPE_TEMP_MID = "pipe_temp_mid" CONF_PIPE_TEMP_OUT = "pipe_temp_out" +CONF_POWER_CONSUMPTION = "power_consumption" +CONF_CURRENT_POWER = "current_power" CONF_DEFROST = "defrost" CONF_PREHEAT = "preheat" @@ -75,6 +87,18 @@ 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_POWER_CONSUMPTION): sensor.sensor_schema( + unit_of_measurement=UNIT_KILOWATT_HOURS, + accuracy_decimals=1, + device_class=DEVICE_CLASS_ENERGY, + state_class=STATE_CLASS_TOTAL_INCREASING, + ), + cv.Optional(CONF_CURRENT_POWER): sensor.sensor_schema( + unit_of_measurement=UNIT_WATT, + accuracy_decimals=0, + device_class=DEVICE_CLASS_POWER, + 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 +144,14 @@ 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_POWER_CONSUMPTION in config: + power_consumption = await sensor.new_sensor(config[CONF_POWER_CONSUMPTION]) + else: + power_consumption = cg.nullptr + if CONF_CURRENT_POWER in config: + current_power = await sensor.new_sensor(config[CONF_CURRENT_POWER]) + else: + current_power = cg.nullptr defrost = await binary_sensor.new_binary_sensor(config[CONF_DEFROST]) preheat = await binary_sensor.new_binary_sensor(config[CONF_PREHEAT]) @@ -135,6 +167,7 @@ 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, + power_consumption, current_power, 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..200f341 100644 --- a/esphome/components/lg_controller/lg-controller.h +++ b/esphome/components/lg_controller/lg-controller.h @@ -153,6 +153,8 @@ 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* power_consumption_; + esphome::sensor::Sensor* current_power_; esphome::binary_sensor::BinarySensor& defrost_; esphome::binary_sensor::BinarySensor& preheat_; esphome::binary_sensor::BinarySensor& outdoor_; @@ -347,6 +349,8 @@ 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* power_consumption, + sensor::Sensor* current_power, binary_sensor::BinarySensor* defrost, binary_sensor::BinarySensor* preheat, binary_sensor::BinarySensor* outdoor, @@ -371,6 +375,8 @@ 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), + power_consumption_(power_consumption), + current_power_(current_power), defrost_(*defrost), preheat_(*preheat), outdoor_(*outdoor), @@ -935,6 +941,12 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub case 3: // 0xCB/AB/2B process_type_b_settings_message(*sender, buffer); break; + case 4: // 0xCC/AC/2C + process_type_c_status_message(*sender, buffer); + break; + case 7: // 0xCF/AF/2F + process_type_f_status_message(*sender, buffer); + break; default: return; } @@ -1273,6 +1285,33 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub } } + static float decode_decimal_nibbles_(const uint8_t* buffer, size_t offset, size_t length, float divisor) { + // Energy/power payloads use decimal nibbles rather than binary integers. + uint32_t result = 0; + for (size_t i = 0; i < length; i++) { + result = result * 100 + ((buffer[offset + i] >> 4) & 0xF) * 10 + (buffer[offset + i] & 0xF); + } + return result / divisor; + } + + void process_type_c_status_message(MessageSender sender, const uint8_t* buffer) { + if (sender != MessageSender::Unit) { + return; + } + if (power_consumption_ != nullptr) { + power_consumption_->publish_state(decode_decimal_nibbles_(buffer, 3, 3, 10.0f)); + } + } + + void process_type_f_status_message(MessageSender sender, const uint8_t* buffer) { + if (sender != MessageSender::Unit) { + return; + } + if (current_power_ != nullptr) { + current_power_->publish_state(decode_decimal_nibbles_(buffer, 2, 3, 1.0f)); + } + } + void update() { ESP_LOGD(TAG, "update"); From 9dcce323cc1104422ed062f7dd042845042e020b Mon Sep 17 00:00:00 2001 From: Jeroen Nijssen Date: Tue, 16 Jun 2026 22:06:58 +0200 Subject: [PATCH 2/2] Clarify optional LG power sensors --- README.md | 6 ++++++ esphome/base.yaml | 6 ------ esphome/components/lg_controller/climate.py | 6 +++--- esphome/components/lg_controller/lg-controller.h | 9 +++++---- 4 files changed, 14 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 6b69df1..ade3ace 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,8 @@ Features currently available in Home Assistant: * Switch and binary sensor for Auto Dry (also known as Auto Clean) feature. Used to dry indoor unit when it's turned off after cooling/dehumidifying. * Sensors for reporting outdoor unit on/off, defrost, preheat, error code. * Sensors for reporting in/mid/out pipe temperatures (if supported by unit). +* Optional passive sensors for total energy and current power, if the unit emits `0xCC` + or `0xCF` frames on the wired-controller bus. * Input field for sleep timer from 0 to 420 minutes (0 turns off the sleep timer). * Input fields for fan speed installer setting (to fine-tune fan speeds, 0-255 with 0 being factory default). This is installer setting 3 (ESP Setting) on LG controllers. * Select option for over heating installer setting from 0-4 (to change over heating behavior in heating mode). This is installer setting 15 (Over Heating) on LG controllers. @@ -118,6 +120,10 @@ The LG ThinQ app and wireless remote can still be used to change these settings Unfortunately not all settings are exposed to the wired controller, but if you're interested in a feature and it's supported by the PREMTB100 or PREMTA200 controller, please open an issue and we can consider adding it. +The optional energy/power sensors only publish values for units that send the relevant wired-bus +frames. Some LG units expose energy data through ThinQ/WiFi but do not send `0xCC` or `0xCF` +frames to the wired controller; for those units these sensors will not update. + # Tips * [Issue #43](https://github.com/JanM321/esphome-lg-controller/issues/43) has some information on temperature sensors that work well for this. * It's possible to use a Home Assistant template sensor as room temperature sensor. I'm [using this](https://gist.github.com/JanM321/b550285713f20231386509b2c227f0b8) to work around some issues with my LG Multi F unit in heating mode. diff --git a/esphome/base.yaml b/esphome/base.yaml index 875b3ec..2ac8bb9 100644 --- a/esphome/base.yaml +++ b/esphome/base.yaml @@ -120,12 +120,6 @@ climate: id: pipe_temperature_out icon: mdi:thermometer unit_of_measurement: "°C" - power_consumption: - name: Power Consumption - id: power_consumption - current_power: - name: Current Power - id: current_power defrost: name: Defrost id: defrost diff --git a/esphome/components/lg_controller/climate.py b/esphome/components/lg_controller/climate.py index bd8dc26..342ddcb 100644 --- a/esphome/components/lg_controller/climate.py +++ b/esphome/components/lg_controller/climate.py @@ -10,8 +10,8 @@ DEVICE_CLASS_POWER, STATE_CLASS_MEASUREMENT, STATE_CLASS_TOTAL_INCREASING, + UNIT_KILOWATT, UNIT_KILOWATT_HOURS, - UNIT_WATT, ) CODEOWNERS = ["JanM321"] @@ -94,8 +94,8 @@ state_class=STATE_CLASS_TOTAL_INCREASING, ), cv.Optional(CONF_CURRENT_POWER): sensor.sensor_schema( - unit_of_measurement=UNIT_WATT, - accuracy_decimals=0, + unit_of_measurement=UNIT_KILOWATT, + accuracy_decimals=1, device_class=DEVICE_CLASS_POWER, state_class=STATE_CLASS_MEASUREMENT, ), diff --git a/esphome/components/lg_controller/lg-controller.h b/esphome/components/lg_controller/lg-controller.h index 200f341..42f0679 100644 --- a/esphome/components/lg_controller/lg-controller.h +++ b/esphome/components/lg_controller/lg-controller.h @@ -1285,8 +1285,9 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub } } - static float decode_decimal_nibbles_(const uint8_t* buffer, size_t offset, size_t length, float divisor) { - // Energy/power payloads use decimal nibbles rather than binary integers. + static float decode_power_nibbles_(const uint8_t* buffer, size_t offset, size_t length, float divisor) { + // Power payloads use LG's nibble format rather than binary integers. Some documented + // current-power examples contain A-F nibbles, so this intentionally is not strict BCD. uint32_t result = 0; for (size_t i = 0; i < length; i++) { result = result * 100 + ((buffer[offset + i] >> 4) & 0xF) * 10 + (buffer[offset + i] & 0xF); @@ -1299,7 +1300,7 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub return; } if (power_consumption_ != nullptr) { - power_consumption_->publish_state(decode_decimal_nibbles_(buffer, 3, 3, 10.0f)); + power_consumption_->publish_state(decode_power_nibbles_(buffer, 3, 3, 10.0f)); } } @@ -1308,7 +1309,7 @@ class LgController final : public climate::Climate, public uart::UARTDevice, pub return; } if (current_power_ != nullptr) { - current_power_->publish_state(decode_decimal_nibbles_(buffer, 2, 3, 1.0f)); + current_power_->publish_state(decode_power_nibbles_(buffer, 2, 3, 1000.0f)); } }