Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,4 @@ __pycache__/*
test/nl.py
secrets.json
logfile.log
lghorizon.log
15 changes: 15 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Python Debugger: Debug LGHorizon",
"type": "debugpy",
"request": "launch",
"program": "main.py",
"console": "integratedTerminal"
}
]
}
152 changes: 151 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,153 @@
# LG Horizon Api

Python library to control multiple LG Horizon boxes
# LG Horizon API Python Library

A Python library to interact with and control LG Horizon set-top boxes. This library provides functionalities for authentication, real-time device status monitoring via MQTT, and various control commands for your Horizon devices.

## Features

- **Authentication**: Supports authentication using username/password or a refresh token. The library automatically handles access token refreshing.
- **Device Management**: Discover and manage multiple LG Horizon set-top boxes associated with your account.
- **Real-time Status**: Monitor device status (online/running/standby) and current playback information (channel, show, VOD, recording, app) through MQTT.
- **Channel Information**: Retrieve a list of available channels and profile-specific favorite channels.
- **Recording Management**:
- Get a list of all recordings.
- Retrieve recordings for specific shows.
- Check recording quota and usage.
- **Device Control**: Send various commands to your set-top box:
- Power on/off.
- Play, pause, stop, rewind, fast forward.
- Change channels (up/down, direct channel selection).
- Record current program.
- Set player position for VOD/recordings.
- Display custom messages on the TV screen.
- Send emulated remote control key presses.
- **Robustness**: Includes automatic MQTT reconnection with exponential backoff and token refresh logic to maintain a stable connection.

## Installation

```bash
pip install lghorizon-python # (Replace with actual package name if different)
```

## Usage

Here's a basic example of how to use the library to connect to your LG Horizon devices and monitor their state:

First, create a `secrets.json` file in the root of your project with your LG Horizon credentials:

```json
{
"username": "your_username",
"password": "your_password",
"country": "nl" // e.g., "nl" for Netherlands, "be" for Belgium
}
```

Then, you can use the library as follows:

```python
import asyncio
import json
import logging
import aiohttp

from lghorizon.lghorizon_api import LGHorizonApi
from lghorizon.lghorizon_models import LGHorizonAuth

_LOGGER = logging.getLogger(__name__)

async def main():
logging.basicConfig(level=logging.INFO) # Set to DEBUG for more verbose output

with open("secrets.json", encoding="utf-8") as f:
secrets = json.load(f)
username = secrets.get("username")
password = secrets.get("password")
country = secrets.get("country", "nl")

async with aiohttp.ClientSession() as session:
auth = LGHorizonAuth(session, country, username=username, password=password)
api = LGHorizonApi(auth)

async def device_state_changed_callback(device_id: str):
device = devices[device_id]
_LOGGER.info(
f"Device {device.device_friendly_name} ({device.device_id}) state changed:\n"
f" State: {device.device_state.state.value}\n"
f" UI State: {device.device_state.ui_state_type.value}\n"
f" Source Type: {device.device_state.source_type.value}\n"
f" Channel: {device.device_state.channel_name or 'N/A'} ({device.device_state.channel_id or 'N/A'})\n"
f" Show: {device.device_state.show_title or 'N/A'}\n"
f" Episode: {device.device_state.episode_title or 'N/A'}\n"
f" Position: {device.device_state.position or 'N/A'} / {device.device_state.duration or 'N/A'}\n"
)

try:
_LOGGER.info("Initializing LG Horizon API...")
await api.initialize()
devices = await api.get_devices()

for device in devices.values():
_LOGGER.info(f"Registering callback for device: {device.device_friendly_name}")
await device.set_callback(device_state_changed_callback)

_LOGGER.info("API initialized. Monitoring device states. Press Ctrl+C to exit.")
# Keep the script running to receive MQTT updates
while True:
await asyncio.sleep(3600) # Sleep for a long time, MQTT callbacks will still fire

except Exception as e:
_LOGGER.error(f"An error occurred: {e}", exc_info=True)
finally:
_LOGGER.info("Disconnecting from LG Horizon API.")
await api.disconnect()
_LOGGER.info("Disconnected.")

if __name__ == "__main__":
asyncio.run(main())
```

## Authentication

The `LGHorizonAuth` class handles authentication. You can initialize it with a username and password, or directly with a refresh token if you have one. The library automatically refreshes access tokens as needed.

```python
# Using username and password
auth = LGHorizonAuth(session, "nl", username="your_username", password="your_password")

# Using a refresh token (e.g., if you've saved it from a previous session)
# auth = LGHorizonAuth(session, "nl", refresh_token="your_refresh_token")
```

You can also set a callback to receive the updated refresh token when it's refreshed, allowing you to persist it for future sessions:

```python
def token_updated_callback(new_refresh_token: str):
print(f"New refresh token received: {new_refresh_token}")
# Here you would typically save this new_refresh_token
# to your secrets.json or other persistent storage.

# After initializing LGHorizonApi:
# api.set_token_refresh_callback(token_updated_callback)
```

## Error Handling

The library defines custom exceptions for common error scenarios:

- `LGHorizonApiError`: Base exception for all API-related errors.
- `LGHorizonApiConnectionError`: Raised for network or connection issues.
- `LGHorizonApiUnauthorizedError`: Raised when authentication fails (e.g., invalid credentials).
- `LGHorizonApiLockedError`: A specific type of `LGHorizonApiUnauthorizedError` indicating a locked account.

These exceptions allow for more granular error handling in your application.

## Development

To run the example script (`main.py`) from the repository:

1. Clone this repository.
2. Install dependencies: `pip install -r requirements.txt` (ensure `requirements.txt` is up-to-date).
3. Create a `secrets.json` file as described in the Usage section.
4. Run `python main.py`.
80 changes: 55 additions & 25 deletions lghorizon/__init__.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,71 @@
"""Python client for LG Horizon."""

from .lghorizon_api import LGHorizonApi
from .models import (
LGHorizonBox,
LGHorizonRecordingListSeasonShow,
from .lghorizon_device import LGHorizonDevice
from .lghorizon_models import (
LGHorizonAuth,
LGHorizonChannel,
LGHorizonCustomer,
LGHorizonDeviceState,
LGHorizonProfile,
LGHorizonRecording,
LGHorizonRecordingList,
LGHorizonShowRecordingList,
LGHorizonRecordingSeason,
LGHorizonRecordingSingle,
LGHorizonRecordingShow,
LGHorizonRecordingEpisode,
LGHorizonCustomer,
LGHorizonRecordingQuota,
LGHorizonRecordingType,
LGHorizonUIStateType,
LGHorizonMessageType,
LGHorizonRunningState,
LGHorizonRecordingSource,
LGHorizonRecordingState,
LGHorizonSourceType,
LGHorizonPlayerState,
LGHorizonAppsState,
LGHorizonUIState,
LGHorizonProfileOptions,
LGHorizonServicesConfig,
)
from .exceptions import (
LGHorizonApiUnauthorizedError,
LGHorizonApiError,
LGHorizonApiConnectionError,
LGHorizonApiUnauthorizedError,
LGHorizonApiLockedError,
)
from .const import (
ONLINE_RUNNING,
ONLINE_STANDBY,
RECORDING_TYPE_SHOW,
RECORDING_TYPE_SEASON,
RECORDING_TYPE_SINGLE,
)

__all__ = [
"LGHorizonApi",
"LGHorizonBox",
"LGHorizonRecordingListSeasonShow",
"LGHorizonRecordingSingle",
"LGHorizonRecordingShow",
"LGHorizonRecordingEpisode",
"LGHorizonDevice",
"LGHorizonAuth",
"LGHorizonChannel",
"LGHorizonCustomer",
"LGHorizonApiUnauthorizedError",
"LGHorizonDeviceState",
"LGHorizonProfile",
"LGHorizonApiError",
"LGHorizonApiConnectionError",
"LGHorizonApiUnauthorizedError",
"LGHorizonApiLockedError",
"ONLINE_RUNNING",
"ONLINE_STANDBY",
"RECORDING_TYPE_SHOW",
"RECORDING_TYPE_SEASON",
"RECORDING_TYPE_SINGLE",
] # noqa
"LGHorizonRecordingList",
"LGHorizonRecordingSeason",
"LGHorizonRecordingSingle",
"LGHorizonRecordingShow",
"LGHorizonRecordingQuota",
"LGHorizonRecordingType",
"LGHorizonUIStateType",
"LGHorizonMessageType",
"LGHorizonRunningState",
"LGHorizonRecordingSource",
"LGHorizonRecordingState",
"LGHorizonSourceType",
"LGHorizonPlayerState",
"LGHorizonAppsState",
"LGHorizonUIState",
"LGHorizonProfileOptions",
"LGHorizonProfile",
"LGHorizonAuth",
"LGHorizonServicesConfig",
"LGHorizonRecording",
"LGHorizonShowRecordingList",
]
15 changes: 11 additions & 4 deletions lghorizon/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,18 @@

BE_AUTH_URL = "https://login.prd.telenet.be/openid/login.do"

PLATFORM_TYPES = {
"EOS": {"manufacturer": "Arris", "model": "DCX960"},
"EOS2": {"manufacturer": "HUMAX", "model": "2008C-STB-TN"},
"HORIZON": {"manufacturer": "Arris", "model": "DCX960"},
"APOLLO": {"manufacturer": "Arris", "model": "VIP5002W"},
}

COUNTRY_SETTINGS = {
"nl": {
"api_url": "https://spark-prod-nl.gnp.cloud.ziggogo.tv",
"mqtt_url": "obomsg.prod.nl.horizon.tv",
"use_oauth": False,
"use_refreshtoken": False,
"channels": [
{
"channelId": "NL_000073_019506",
Expand Down Expand Up @@ -106,7 +113,7 @@
},
"be-nl-preprod": {
"api_url": "https://spark-preprod-be.gnp.cloud.telenet.tv",
"use_oauth": True,
"use_refreshtoken": True,
"oauth_username_fieldname": "j_username",
"oauth_password_fieldname": "j_password",
"oauth_add_accept_header": False,
Expand All @@ -131,13 +138,13 @@
},
"ie": {
"api_url": "https://spark-prod-ie.gnp.cloud.virginmediatv.ie",
"use_oauth": False,
"use_refreshtoken": False,
"channels": [],
"language": "en",
},
"pl": {
"api_url": "https://spark-prod-pl.gnp.cloud.upctv.pl",
"use_oauth": False,
"use_refreshtoken": False,
"channels": [],
"language": "pl",
"platform_types": {
Expand Down
6 changes: 3 additions & 3 deletions lghorizon/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ class LGHorizonApiError(Exception):


class LGHorizonApiConnectionError(LGHorizonApiError):
"""Generic LGHorizon exception."""
"""Exception for connection-related errors with the LG Horizon API."""


class LGHorizonApiUnauthorizedError(Exception):
"""Generic LGHorizon exception."""
"""Exception for unauthorized access to the LG Horizon API."""


class LGHorizonApiLockedError(LGHorizonApiUnauthorizedError):
"""Generic LGHorizon exception."""
"""Exception for locked account errors with the LG Horizon API."""
2 changes: 1 addition & 1 deletion lghorizon/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import random


def make_id(string_length=10):
async def make_id(string_length=10):
"""Create an id with given length."""
letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
return "".join(random.choice(letters) for i in range(string_length))
Loading