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(), aConnectionErroris raised. - If the serial connection is lost, subscribers receive
NoneandconnectedbecomesFalse. - Commands return a
Response; an NG (not-good) acknowledgement raisesCommandRejected.
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)
| File | Size | Uploaded | |
|---|---|---|---|
| lg_rs232_tv-1.2.0.tar.gz | 17.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| 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 logRelease 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