Skip to main content

lg-rs232-tv

Async Python library to control LG TVs over RS232 serial, built on serialx.

Installation

pip install lg-rs232-tv

# To talk to a TV over an ESPHome serial proxy:
pip install 'lg-rs232-tv[esphome]'

Requires Python 3.12+.

Quick start

import asyncio
from lg_rs232_tv import LGTV, InputSource

async def main():
    tv = LGTV("/dev/ttyUSB0")
    await tv.connect()
    await tv.query_state()

    print(f"Power:  {tv.state.power}")
    print(f"Input:  {tv.state.input_source}")
    print(f"Volume: {tv.state.volume}%")

    await tv.set_volume(20)
    await tv.select_input_source(InputSource.HDMI1)

    await tv.disconnect()

asyncio.run(main())

CLI

A built-in CLI lets you quickly test your serial connection:

# Query and print TV status
python -m lg_rs232_tv /dev/ttyUSB0

# Talk to a TV via an ESPHome serial proxy ("TTL" port)
python -m lg_rs232_tv 'esphome://192.168.1.29/?port_name=TTL'

# Talk to a TV over a raw TCP socket (e.g. ser2net)
python -m lg_rs232_tv socket://192.168.1.29:5000

# Single-shot actions
python -m lg_rs232_tv /dev/ttyUSB0 --power on
python -m lg_rs232_tv /dev/ttyUSB0 --power off
python -m lg_rs232_tv /dev/ttyUSB0 --input HDMI2
python -m lg_rs232_tv /dev/ttyUSB0 --volume 30
python -m lg_rs232_tv /dev/ttyUSB0 --mute on
python -m lg_rs232_tv /dev/ttyUSB0 --aspect R_16_9
python -m lg_rs232_tv /dev/ttyUSB0 --key MENU

# Use a non-default set ID (when daisy-chaining multiple sets)
python -m lg_rs232_tv /dev/ttyUSB0 --set-id 2

Features

Full state after query

connect() only opens and verifies the serial connection (by querying power). Call query_state() to populate the current TV state into tv.state.

tv = LGTV("/dev/ttyUSB0")
await tv.connect()
await tv.query_state()

state = tv.state
state.power           # PowerState.ON / PowerState.OFF
state.input_source    # InputSource enum
state.aspect_ratio    # AspectRatio enum
state.volume          # 0..100 percent
state.volume_mute     # bool
state.picture_mode    # PictureMode enum
state.color_temperature  # ColorTemperature enum
# ...etc

Note: Most LG TVs only respond to status queries (other than power) when the set is on. While in standby, only ka (power) is answered.

Event subscription

Subscribe to state changes to react in real-time. Callbacks receive a TVState snapshot, or None when the connection is lost.

def on_state_change(state):
    if state is None:
        print("Disconnected!")
        return
    print(f"Volume: {state.volume}%, Source: {state.input_source}")

unsub = tv.subscribe(on_state_change)
# Later:
unsub()

Power

await tv.power_on()    # often ignored when in standby; use IR/WoL instead
await tv.power_off()
power = await tv.query_power()  # PowerState.ON / PowerState.OFF

Input source

from lg_rs232_tv import InputSource, LegacyInputSource

# Modern xb command (~2010+)
await tv.select_input_source(InputSource.HDMI1)
source = await tv.query_input_source()  # InputSource enum

# Legacy kb command (older sets)
await tv.select_legacy_input_source(LegacyInputSource.HDMI1)

Available modern sources: DTV_ANTENNA, DTV_CABLE, ANALOG_ANTENNA, ANALOG_CABLE, AV1, AV2, COMPONENT1-3, RGB_PC, HDMI1-4.

Volume / mute

await tv.set_volume(30)       # 0..100
await tv.mute_on()
await tv.mute_off()
volume = await tv.query_volume()  # int 0..100
muted = await tv.query_mute()     # True if muted

Picture controls

All on a 0..100 scale.

await tv.set_contrast(70)
await tv.set_brightness(50)
await tv.set_color(50)
await tv.set_tint(50)
await tv.set_sharpness(50)
await tv.set_backlight(80)

Audio controls

await tv.set_treble(50)
await tv.set_bass(50)
await tv.set_balance(50)

Modes

from lg_rs232_tv import (
    AspectRatio, ColorTemperature, EnergySaving, PictureMode, SoundMode
)

await tv.set_aspect_ratio(AspectRatio.R_16_9)
await tv.set_color_temperature(ColorTemperature.WARM)
await tv.set_energy_saving(EnergySaving.MEDIUM)
await tv.set_picture_mode(PictureMode.CINEMA)
await tv.set_sound_mode(SoundMode.MUSIC)

Screen mute / OSD / remote lock

from lg_rs232_tv import ScreenMute

await tv.set_screen_mute(ScreenMute.SCREEN_ON)  # picture off, audio on
await tv.set_screen_mute(ScreenMute.OFF)        # back to normal

await tv.osd_on()
await tv.osd_off()

await tv.remote_lock_on()
await tv.remote_lock_off()

Remote control keys

Send any IR remote key code over RS232 with the mc command:

from lg_rs232_tv import RemoteKey

await tv.send_remote_key(RemoteKey.MENU)
await tv.send_remote_key(RemoteKey.HOME)
await tv.send_remote_key(RemoteKey.PLAY)
await tv.send_remote_key_code(0x08)   # arbitrary hex code

Connection handling

  • If the TV doesn't respond during connect(), a ConnectionError is raised.
  • If the serial connection is lost, subscribers receive None and connected becomes False.
  • Commands return a Response; an NG (not-good) acknowledgement raises CommandRejected.
from lg_rs232_tv import CommandRejected

try:
    await tv.connect()
except ConnectionError:
    print("TV not responding")

try:
    await tv.set_volume(50)
except CommandRejected as err:
    print(f"TV rejected command: {err}")

Multiple sets / set ID

When multiple TVs are daisy-chained on the same RS232 bus, each set is addressed by its set ID (1..99). Pass set_id= at construction time:

tv1 = LGTV("/dev/ttyUSB0", set_id=1)
tv2 = LGTV("/dev/ttyUSB0", set_id=2)

Serial connection

The library uses serialx. LG TVs use 9600 baud, 8 data bits, no parity, 1 stop bit.

Most LG TVs use a DE-9 male connector (requires a null-modem cable). Some sets expose RS232 on a 3.5mm phone jack instead. The library accepts any serialx-compatible URL:

URL form Use case
/dev/ttyUSB0 local USB-serial adapter
socket://host:port raw TCP serial bridge (ser2net)
esphome://host/?port_name=TTL ESPHome serial proxy component
esphome://host/?port_name=RS-232 ESPHome serial proxy component

Protocol

LG TVs use a simple ASCII request/response protocol:

Transmission:    [Command1][Command2] [SetID] [Data]<CR>
                 e.g. "ka 01 ff\r"  (query power on set 1)

Response:        [Command2] [SetID] (OK|NG)[Data]x
                 e.g. "a 01 OK01x"  (set 1 acks: power = on)

Note that responses are terminated by the literal ASCII character x, not by a carriage return.

FF data sent to a setter command means "query current value". The acknowledgement contains the current value as the data byte. The library exposes both a set_* and a query_* method for each attribute.

Development

# Install dev dependencies
uv sync

# Run tests
uv run pytest

License

MIT

Release files for lg-rs232-tv 1.2.0

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-rs232-tv 1.2.0
File Size Uploaded
lg_rs232_tv-1.2.0.tar.gz 17.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for lg-rs232-tv 1.2.0
File Interpreter ABI Platform
lg_rs232_tv-1.2.0-py3-none-any.whl Python 3 none any Details

Total release size: 38.1 kB

Release files / lg_rs232_tv-1.2.0.tar.gz

Download URL lg_rs232_tv-1.2.0.tar.gz
Size 17.0 kB
Tags Source
SHA-256 checksum
How to use checksums
46297f0d7b6acce03631a95aab005b80f21fc6b412cfca8ac40664fdc2c8e68c
BLAKE2b-256 checksum
How to use checksums
a6eae47991eabce3adf9a791d31558ab06a2cc659ba7be5e50969a6d1397050a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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 May 17, 2026.

Transparency log

Release files / lg_rs232_tv-1.2.0-py3-none-any.whl

Download URL lg_rs232_tv-1.2.0-py3-none-any.whl
Size 21.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bafe1ad57ff98bbe4c92d575dd17ff8be40532f432f8bebf4d96d288a38931b3
BLAKE2b-256 checksum
How to use checksums
1eaa0494135bc4e5cd591a5284dc6ee11843fb24a9888edacd8c17c9fb4f1bd6
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.12

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 May 17, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.2.0 This release

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.0

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