Skip to main content

lg-heatpump-modbus Python library

CI PyPI Python License

lg-heatpump-modbus is an asynchronous, transport-independent Python library for reading and controlling LG heat pumps — the Therma V family and the siblings that share its Modbus register map — over Modbus.

DISCLAIMER

This library is in early development. It is not yet feature-complete, bugs are expected!

Anyone with a real device is encouraged to test it and report issues. See section Contributing for a quick way to check your installation.

Purpose and scope

The library is meant for operational monitoring and day-to-day control of a commissioned installation: temperatures, pressures, pump and heater states, setpoints, operating mode, hot water and silent mode. It is not a commissioning tool and does not attempt to reproduce every installer menu.

The library:

  • contains the device-specific data model — the valid registers and coils, their data types, scaling, units, ranges and enumerations, and the rules for safe reads and writes;

  • structures the register map as device models, so a new variant is added as a ModelDefinition (data) rather than as new code;

  • carries neutral datapoint metadata — unit, precision, step, limits, enum options and writability — next to each address, so the model is the datasheet;

  • does not create or own the Modbus transport. Applications provide a modbus_connection.ModbusUnit and may use any backend modbus-connection supports (tmodbus, pymodbus, …).

Prerequisites

Development or testing this library can be done without a real heat pump, using the in-memory mock backend that ships with modbus-connection. The mock backend simulates a heat pump and responds to reads and writes.

Before you can use the library with a real heatpump, you must have a working Modbus connection to the heat pump.

This takes two steps:

  1. Get and connect a compatible Modbus interface to the heat pump. At this point, the Waveshare RS485 to ETH Adapter is known to work and well-tested.
  2. Configure the heat pump to enable Modbus communication.

Both steps are explained in more detail here, you can skip the middle Homeassistant section.

Installation

pip install lg-heatpump-modbus

The library itself only needs the protocol and the device-modelling framework. To use the bundled query script, install a concrete backend as well:

pip install "lg-heatpump-modbus[cli]"

Usage

import asyncio

from modbus_connection import ModbusTcpParams
from modbus_connection.tmodbus import ModbusConnection 

from lg_heatpump_modbus import LgHeatPump, OperationMode


async def main() -> None:
    connection = ModbusConnection(ModbusTcpParams(host="192.168.1.50", port=502))
    try:
        pump = LgHeatPump(connection.for_unit(1))
        await pump.async_update()

        print("Outdoor:", pump.sensors.outdoor_temperature, "°C")
        print("Flow:", pump.sensors.water_outlet_temperature, "°C")
        print("Compressor running:", pump.states.compressor)
        print("Circuit 1 target:", pump.controls.target_temperature_circuit_1)

        await pump.controls.set_operation_mode(OperationMode.HEAT)
        await pump.controls.set_target_temperature(1, 42.0)
        await pump.switches.set_silent_mode(True)
    finally:
        await connection.close()


asyncio.run(main())

async_update() fans out to each sub-system, and each sub-system reads only its own registers in as few Modbus round-trips as the map allows: the whole device is seven block reads. Poll the two halves at different rates if you prefer:

await pump.async_update_readings()  # what the heat pump measures
await pump.async_update_settings()  # what it has been configured to do

Every poll returns an UpdateReport naming the sub-systems that refreshed and those that failed, so one unanswered block does not take the rest with it.

Sub-systems

Attribute Modbus table Contents
pump.info input register Product information word
pump.sensors input registers Temperatures, pressures, flow rate, compressor frequency, error code, energy state
pump.states discrete inputs Pump, compressor, heater, defrost and fault flags
pump.controls holding registers Operation mode, control method, water/room/hot-water setpoints, auto-mode shift
pump.switches coils Power, hot water, silent mode, disinfection, emergency stop

Register map

The address range of Modbus is very large and divided into multiple sections called Function Code (FC). Each FC has its own address space at the beginning of the address.

The FCs are either written as hexadecimal (0x01, 0x02, …) or as FC01, FC02, … in the documentation.

Modbus is defined to have four kinds of tables:

Modbus Table Data Size Reading FC Writing FC
Coil 1-bit 0x01 0x05 / 0x0F
Discrete Input 1-bit 0x02 no, read-only
Holding 16-bit 0x03 0x06 / 0x10
Input 16-bit 0x04 no, read-only

Addresses used in this library are the actual protocol addresses. The manuals of the manufacturer have an offset of one: Input register 1 of the manufacturer documentation is address 0 here.

The manuals give human-friendly addresses with this scheme: XYYYY

  • X is the concerning the address space, X+1 equals the function code.
  • YYYY is the address with an offset of +1 to the protocol address, so the actual protocol address is YYYY-1.

For instance, when the manual states 10007, this means that:

  • 1: FC02, discrete input
  • 0008: Modbus address 7 (=8-1)

This is the Silent Mode sensor, see states.py:

silent_mode = discrete_input(7, description="Silent mode active")

Input registers (FC04, read-only)

Address Datapoint Scale Unit
0 error_code 1
1 odu_operation_cycle 1
2 water_inlet_temperature 0.1 °C
3 water_outlet_temperature 0.1 °C
4 backup_heater_outlet_temperature 0.1 °C
5 dhw_tank_temperature 0.1 °C
6 solar_collector_temperature 0.1 °C
7 room_air_temperature_circuit_1 0.1 °C
8 water_flow_rate 0.1 L/min
9 water_outlet_temperature_circuit_2 0.1 °C
10 room_air_temperature_circuit_2 0.1 °C
11 energy_state enum
12 outdoor_temperature 0.1 °C
16 liquid_pipe_temperature 1 °C
18 suction_temperature 1 °C
19 discharge_temperature 1 °C
20 evaporator_inlet_temperature 0.1 °C
21 evaporator_outlet_temperature 0.1 °C
22 refrigerant_high_pressure 1 bar
23 refrigerant_low_pressure 1 bar
24 compressor_frequency 1 Hz
9998 product_info raw

sensors.compressor_speed derives revolutions per minute from compressor_frequency, and sensors.water_temperature_difference the spread across the heat exchanger.

solar_collector_temperature reads as None when no solar collector is fitted: the heat pump reports a fixed dummy value of 300.0 °C instead of refusing the read, and that sentinel is masked. Set pump.debug = True to see the raw 300.0 °C reading instead, e.g. while diagnosing a device.

Holding registers (FC03 read, FC06 write)

Address Datapoint Scale Unit Writable
0 operation_mode enum ✔
1 control_method enum ✔
2 target_temperature_circuit_1 0.1 °C ✔
3 room_air_setpoint_circuit_1 0.1 °C ✔
4 shift_in_auto_mode_circuit_1 1 K ✔
5 target_temperature_circuit_2 0.1 °C ✔
6 room_air_setpoint_circuit_2 0.1 °C ✔
7 shift_in_auto_mode_circuit_2 1 K ✔
8 dhw_target_temperature 0.1 °C ✔
9 energy_state enum

Coils (FC01 read, FC05 write)

Address Datapoint
0 heating_circuit
1 dhw
2 silent_mode
3 dhw_disinfection
4 emergency_stop
5 trigger_emergency_operation

Discrete inputs (FC02, read-only)

Address Datapoint Address Datapoint
0 water_flow 9 solar_pump
1 water_pump 10 backup_heater_step_1
2 external_water_pump 11 backup_heater_step_2
3 compressor 12 dhw_boost_heater
4 defrosting 13 error
5 dhw_heating 14 emergency_operation_space
6 dhw_disinfection 15 emergency_operation_dhw
7 silent_mode 16 mixing_pump
8 cooling

Device models

An installation without a second circuit, a hot water tank or a solar collector should not be asked for registers it does not serve. Name the model and the read plan narrows itself:

from lg_heatpump_modbus import LgHeatPump, THERMA_V_SINGLE_CIRCUIT

pump = LgHeatPump(unit, model=THERMA_V_SINGLE_CIRCUIT)
pump.serves("target_temperature_circuit_2")  # False
Model key Circuits Hot water Solar Mixing valve
therma_v 2 ✔ ✔ ✔
therma_v_single_circuit 1 ✔
therma_v_heating_only 2 ✔ ✔

Individual datapoints can be excluded on top of the model, for a unit that refuses one particular register:

pump = LgHeatPump(unit, excluded_datapoints=["solar_collector_temperature"])

Adding a variant means adding a ModelDefinition to lg_heatpump_modbus/configurations/models.py, never a code change.

Datapoint metadata

Each datapoint carries neutral metadata, so an application can build its user interface from the model instead of hard-coding it:

metadata = pump.controls.require_metadata_for("dhw_target_temperature")
metadata.register_type  # 'holding'
metadata.address  # 8
metadata.writable  # True
metadata.number.unit  # '°C'
metadata.number.min_value, metadata.number.max_value  # 30.0, 80.0
metadata.number.step  # 0.1

The documented limits are the protocol limits. The range an installation actually accepts is narrower and set by the installer; a value the heat pump refuses comes back as a Modbus exception.

Writing values

Writes go through the component that owns the register, either by attribute name or through the named helpers:

await pump.controls.write("dhw_target_temperature", 48.0)
await pump.controls.set_dhw_target_temperature(48.0)
await pump.switches.set_heating_circuit(True)

A value outside the documented range raises LgValueValidationError before anything reaches the wire. Input registers and discrete inputs are read-only and raise AttributeError if written.

Contributing

Any contribution is welcome. Most valuable is testing against a real device.

1. Prepare your hardware

Following the instructions in Prerequisites, connect a Modbus interface to your heat pump and enable Modbus communication.

2. Set up the library (pre-PyPI stage)

Create a virtual Python environment and install modbus-connection, preferably with tmodbus:

python -m venv venv_modbus
pip install "modbus-connection[tmodbus]"

3a. Run the query script and dump the outputs

Set up the library and run script/query.py against your heat pump. Parameters usually like this:

192.168.0.XXX --port 502 --unit 1 --json-dir C:\tmp\lg_json_dumps

Directly report the JSON output, as well as any unexpected values or errors.

Occasional Response timeouts are expected when the heat pump is still connected to another Modbus device (e.g., legacy Home Assistant integration). If you see a timeout, wait a few seconds and try again.

Add --debug to see raw sentinel values (e.g. solar_collector_temperature's 300 °C no-sensor reading) instead of them being masked to None — handy when a reported value looks suspicious and you want to confirm what the heat pump actually sent.

3b. Use the library from a Python interpreter

Play around with the controls, read the sensors, change controls and switches. Report any findings, unexpected values, or errors. A JSON file from the query script (see section beforehand) is also very valuable for debugging.

Querying a real device

script/query.py connects to a heat pump, reads it once and prints every value. It is the quickest way to check an installation with no application around it. Add --json-dir <folder> to write a JSON snapshot for later analysis; omit it to keep the script purely terminal-based:

python script/query.py 192.168.1.50 --unit 1
python script/query.py /dev/ttyUSB0 --transport serial --unit 1 --baudrate 9600
python script/query.py 192.168.1.50 --unit 1 --json-dir ./query-dumps
python script/query.py --help

After collecting a few JSON snapshots, you can convert them into a single CSV table:

python script/json_to_csv.py --input-dir ./query-dumps --output ./query-dumps.csv

The CSV contains one row per snapshot and one column per flattened datapoint, with names like components.sensors.outdoor_temperature and derived.compressor_speed.

Development

script/run_checks.sh     # format check, lint, compile, test, build
python -m ruff check --fix .
python -m ruff format .

Tests run against the in-memory mock backend that ships with modbus-connection, so the whole suite runs without hardware, a server or a network.

Licence

Apache-2.0. See LICENSE.

Metadata

Release files for lg-heatpump-modbus 0.0.1

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for lg-heatpump-modbus 0.0.1
File Size Uploaded
lg_heatpump_modbus-0.0.1.tar.gz 72.8 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for lg-heatpump-modbus 0.0.1
File Interpreter ABI Platform
lg_heatpump_modbus-0.0.1-py3-none-any.whl Python 3 none any Details

Total release size: 106.8 kB

Release files / lg_heatpump_modbus-0.0.1.tar.gz

Download URL lg_heatpump_modbus-0.0.1.tar.gz
Size 72.8 kB
Tags Source
SHA-256 checksum
How to use checksums
e022f754e1521dba2600b2bcd5a948a9dfddf2d2d4d3e9a802a2849beb80b2af
BLAKE2b-256 checksum
How to use checksums
13215b521b84af7178bcbc7efd1c29cd111cfb9b6b6173841b2c1536b0651efc
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release files / lg_heatpump_modbus-0.0.1-py3-none-any.whl

Download URL lg_heatpump_modbus-0.0.1-py3-none-any.whl
Size 34.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
5cbaf4b8bfe589704791744432ba61836d493ea969c3d6ada76a46da5fc1e307
BLAKE2b-256 checksum
How to use checksums
28e3378d75712a572da5523deea6badf1212ed41d187625e351665af133f4631
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 9, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.0.1 This release

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page