Metadata-Version: 2.4 Name: lg-rs232-tv Version: 1.2.0 Summary: Async library to control LG TVs over RS232 Author: Paulus Schoutsen Author-email: Paulus Schoutsen License-Expression: MIT Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Topic :: Home Automation Classifier: Topic :: System :: Hardware Classifier: Framework :: AsyncIO Requires-Dist: serialx>=0.8.0 Requires-Dist: serialx[esphome]>=0.8.0 ; extra == 'esphome' Requires-Dist: aiowebostv>=0.7.4 ; extra == 'remote' Requires-Python: >=3.12 Project-URL: Repository, https://github.com/home-assistant-libs/lg-rs232-tv Provides-Extra: esphome Provides-Extra: remote Description-Content-Type: text/markdown # lg-rs232-tv Async Python library to control LG TVs over RS232 serial, built on [serialx](https://github.com/puddly/serialx). ## Installation ```bash 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 ```python 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: ```bash # 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`. ```python 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. ```python 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 ```python 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 ```python 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 ```python 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. ```python 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 ```python await tv.set_treble(50) await tv.set_bass(50) await tv.set_balance(50) ``` ### Modes ```python 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 ```python 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: ```python 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`. ```python 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: ```python tv1 = LGTV("/dev/ttyUSB0", set_id=1) tv2 = LGTV("/dev/ttyUSB0", set_id=2) ``` ## Serial connection The library uses [serialx](https://github.com/puddly/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] 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 ```bash # Install dev dependencies uv sync # Run tests uv run pytest ``` ## License MIT