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
96 changes: 95 additions & 1 deletion lghorizon/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,55 @@
LGHorizonApiLockedError,
)

from .const import COUNTRY_SETTINGS
from .const import (
COUNTRY_SETTINGS,
MEDIA_KEYS,
MEDIA_KEY_POWER,
MEDIA_KEY_STANDBY,
MEDIA_KEY_WAKEUP,
MEDIA_KEY_PLAY,
MEDIA_KEY_PAUSE,
MEDIA_KEY_PLAY_PAUSE,
MEDIA_KEY_STOP,
MEDIA_KEY_RECORD,
MEDIA_KEY_FAST_FORWARD,
MEDIA_KEY_REWIND,
MEDIA_KEY_TRACK_NEXT,
MEDIA_KEY_TRACK_PREVIOUS,
MEDIA_KEY_CHANNEL_UP,
MEDIA_KEY_CHANNEL_DOWN,
MEDIA_KEY_TOP_MENU,
MEDIA_KEY_GUIDE,
MEDIA_KEY_HELP,
MEDIA_KEY_INFO,
MEDIA_KEY_CONTEXT_MENU,
MEDIA_KEY_NEXT_USER_PROFILE,
MEDIA_KEY_TV,
MEDIA_KEY_TELETEXT,
MEDIA_KEY_SUBTITLE,
MEDIA_KEY_AUDIO_TRACK,
MEDIA_KEY_ARROW_UP,
MEDIA_KEY_ARROW_DOWN,
MEDIA_KEY_ARROW_LEFT,
MEDIA_KEY_ARROW_RIGHT,
MEDIA_KEY_ENTER,
MEDIA_KEY_ESCAPE,
MEDIA_KEY_BACKSPACE,
MEDIA_KEY_RED,
MEDIA_KEY_GREEN,
MEDIA_KEY_YELLOW,
MEDIA_KEY_BLUE,
MEDIA_KEY_0,
MEDIA_KEY_1,
MEDIA_KEY_2,
MEDIA_KEY_3,
MEDIA_KEY_4,
MEDIA_KEY_5,
MEDIA_KEY_6,
MEDIA_KEY_7,
MEDIA_KEY_8,
MEDIA_KEY_9,
)

__all__ = [
"LGHorizonAdBreak",
Expand Down Expand Up @@ -87,4 +135,50 @@
"LGHorizonManagedRecording",
"LGHorizonManagedRecordingList",
"COUNTRY_SETTINGS",
"MEDIA_KEYS",
"MEDIA_KEY_POWER",
"MEDIA_KEY_STANDBY",
"MEDIA_KEY_WAKEUP",
"MEDIA_KEY_PLAY",
"MEDIA_KEY_PAUSE",
"MEDIA_KEY_PLAY_PAUSE",
"MEDIA_KEY_STOP",
"MEDIA_KEY_RECORD",
"MEDIA_KEY_FAST_FORWARD",
"MEDIA_KEY_REWIND",
"MEDIA_KEY_TRACK_NEXT",
"MEDIA_KEY_TRACK_PREVIOUS",
"MEDIA_KEY_CHANNEL_UP",
"MEDIA_KEY_CHANNEL_DOWN",
"MEDIA_KEY_TOP_MENU",
"MEDIA_KEY_GUIDE",
"MEDIA_KEY_HELP",
"MEDIA_KEY_INFO",
"MEDIA_KEY_CONTEXT_MENU",
"MEDIA_KEY_NEXT_USER_PROFILE",
"MEDIA_KEY_TV",
"MEDIA_KEY_TELETEXT",
"MEDIA_KEY_SUBTITLE",
"MEDIA_KEY_AUDIO_TRACK",
"MEDIA_KEY_ARROW_UP",
"MEDIA_KEY_ARROW_DOWN",
"MEDIA_KEY_ARROW_LEFT",
"MEDIA_KEY_ARROW_RIGHT",
"MEDIA_KEY_ENTER",
"MEDIA_KEY_ESCAPE",
"MEDIA_KEY_BACKSPACE",
"MEDIA_KEY_RED",
"MEDIA_KEY_GREEN",
"MEDIA_KEY_YELLOW",
"MEDIA_KEY_BLUE",
"MEDIA_KEY_0",
"MEDIA_KEY_1",
"MEDIA_KEY_2",
"MEDIA_KEY_3",
"MEDIA_KEY_4",
"MEDIA_KEY_5",
"MEDIA_KEY_6",
"MEDIA_KEY_7",
"MEDIA_KEY_8",
"MEDIA_KEY_9",
]
89 changes: 78 additions & 11 deletions lghorizon/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,90 @@
BOX_PLAY_STATE_VOD = "VOD"

# List with available media keys.
# Power
MEDIA_KEY_POWER = "Power"
MEDIA_KEY_ENTER = "Enter"
MEDIA_KEY_ESCAPE = "Escape" # Not yet implemented
MEDIA_KEY_STANDBY = "Standby"
MEDIA_KEY_WAKEUP = "WakeUp"

MEDIA_KEY_HELP = "Help" # Not yet implemented
MEDIA_KEY_INFO = "Info" # Not yet implemented
MEDIA_KEY_GUIDE = "Guide" # Not yet implemented
# Media Playback
MEDIA_KEY_PLAY = "MediaPlay"
MEDIA_KEY_PAUSE = "MediaPause"
MEDIA_KEY_PLAY_PAUSE = "MediaPlayPause"
MEDIA_KEY_STOP = "MediaStop"
MEDIA_KEY_RECORD = "MediaRecord"
MEDIA_KEY_FAST_FORWARD = "MediaFastForward"
MEDIA_KEY_REWIND = "MediaRewind"
MEDIA_KEY_TRACK_NEXT = "MediaTrackNext"
MEDIA_KEY_TRACK_PREVIOUS = "MediaTrackPrevious"

MEDIA_KEY_CONTEXT_MENU = "ContextMenu" # Not yet implemented
# Channel / Navigation
MEDIA_KEY_CHANNEL_UP = "ChannelUp"
MEDIA_KEY_CHANNEL_DOWN = "ChannelDown"
MEDIA_KEY_TOP_MENU = "MediaTopMenu"
MEDIA_KEY_GUIDE = "Guide"
MEDIA_KEY_HELP = "Help"
MEDIA_KEY_INFO = "Info"
MEDIA_KEY_CONTEXT_MENU = "ContextMenu"
MEDIA_KEY_NEXT_USER_PROFILE = "NextUserProfile"
MEDIA_KEY_TV = "TV"
MEDIA_KEY_TELETEXT = "Teletext"
MEDIA_KEY_SUBTITLE = "Subtitle"
MEDIA_KEY_AUDIO_TRACK = "AudioTrack"

MEDIA_KEY_RECORD = "MediaRecord"
MEDIA_KEY_PLAY_PAUSE = "MediaPlayPause"
MEDIA_KEY_STOP = "MediaStop"
MEDIA_KEY_REWIND = "MediaRewind"
MEDIA_KEY_FAST_FORWARD = "MediaFastForward"
# UI Navigation (D-Pad)
MEDIA_KEY_ARROW_UP = "ArrowUp"
MEDIA_KEY_ARROW_DOWN = "ArrowDown"
MEDIA_KEY_ARROW_LEFT = "ArrowLeft"
MEDIA_KEY_ARROW_RIGHT = "ArrowRight"
MEDIA_KEY_ENTER = "Enter"
MEDIA_KEY_ESCAPE = "Escape"
MEDIA_KEY_BACKSPACE = "Backspace"

# Colour Buttons
MEDIA_KEY_RED = "Red"
MEDIA_KEY_GREEN = "Green"
MEDIA_KEY_YELLOW = "Yellow"
MEDIA_KEY_BLUE = "Blue"

# Digit Keys
MEDIA_KEY_0 = "0"
MEDIA_KEY_1 = "1"
MEDIA_KEY_2 = "2"
MEDIA_KEY_3 = "3"
MEDIA_KEY_4 = "4"
MEDIA_KEY_5 = "5"
MEDIA_KEY_6 = "6"
MEDIA_KEY_7 = "7"
MEDIA_KEY_8 = "8"
MEDIA_KEY_9 = "9"

# Grouped for API/UI consumers
MEDIA_KEYS = {
"Power": [MEDIA_KEY_POWER, MEDIA_KEY_STANDBY, MEDIA_KEY_WAKEUP],
"Playback": [
MEDIA_KEY_PLAY, MEDIA_KEY_PAUSE, MEDIA_KEY_PLAY_PAUSE,
MEDIA_KEY_STOP, MEDIA_KEY_RECORD,
MEDIA_KEY_FAST_FORWARD, MEDIA_KEY_REWIND,
MEDIA_KEY_TRACK_NEXT, MEDIA_KEY_TRACK_PREVIOUS,
],
"Navigation": [
MEDIA_KEY_CHANNEL_UP, MEDIA_KEY_CHANNEL_DOWN,
MEDIA_KEY_TOP_MENU, MEDIA_KEY_GUIDE, MEDIA_KEY_HELP,
MEDIA_KEY_INFO, MEDIA_KEY_CONTEXT_MENU,
MEDIA_KEY_NEXT_USER_PROFILE, MEDIA_KEY_TV,
MEDIA_KEY_TELETEXT, MEDIA_KEY_SUBTITLE, MEDIA_KEY_AUDIO_TRACK,
],
"D-Pad": [
MEDIA_KEY_ARROW_UP, MEDIA_KEY_ARROW_DOWN,
MEDIA_KEY_ARROW_LEFT, MEDIA_KEY_ARROW_RIGHT,
MEDIA_KEY_ENTER, MEDIA_KEY_ESCAPE, MEDIA_KEY_BACKSPACE,
],
"Colour": [MEDIA_KEY_RED, MEDIA_KEY_GREEN, MEDIA_KEY_YELLOW, MEDIA_KEY_BLUE],
"Digits": [
MEDIA_KEY_0, MEDIA_KEY_1, MEDIA_KEY_2, MEDIA_KEY_3, MEDIA_KEY_4,
MEDIA_KEY_5, MEDIA_KEY_6, MEDIA_KEY_7, MEDIA_KEY_8, MEDIA_KEY_9,
],
}

RECORDING_TYPE_SINGLE = "single"
RECORDING_TYPE_SHOW = "show"
Expand Down
29 changes: 23 additions & 6 deletions lghorizon/lghorizon_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def has_cloud_recording(self) -> bool:
async def get_profile_channels(
self, profile_id: Optional[str] = None
) -> Dict[str, LGHorizonChannel]:
"""Returns channels to display baed on profile."""
"""Returns channels to display based on profile."""
# Attempt to retrieve the profile by the given profile_id
if not profile_id:
profile_id = self._profile_id
Expand All @@ -129,16 +129,33 @@ async def get_profile_channels(
_LOGGER.debug("Returning favorite channels for profile '%s'.", profile.name)
# Use a set for faster lookup of favorite channel IDs
profile_channel_ids = set(profile.favorite_channels)
return {
channels = {
channel.id: channel
for channel in self._channels.values()
if channel.id in profile_channel_ids
}
else:
# If no profile is found (even after defaulting) or the profile has no favorite channels,
# return all available channels.
_LOGGER.debug("No specific profile channels found, returning all channels.")
channels = dict(self._channels)

# Deduplicate by channel number for display purposes:
# keep the last entry per logicalChannelNumber (typically HD over SD)
seen_numbers: dict[str, str] = {}
for channel in channels.values():
ch_num = str(channel.channel_number)
if ch_num in seen_numbers:
_LOGGER.debug(
"Duplicate channel number %s: preferring %s over %s",
ch_num, channel.id, seen_numbers[ch_num],
)
seen_numbers[ch_num] = channel.id

# If no profile is found (even after defaulting) or the profile has no favorite channels,
# return all available channels.
_LOGGER.debug("No specific profile channels found, returning all channels.")
return self._channels
return {
cid: channels[cid]
for cid in seen_numbers.values()
}

async def _register_devices(self) -> None:
"""Register devices."""
Expand Down
14 changes: 14 additions & 0 deletions lghorizon/lghorizon_device.py
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,20 @@ async def set_channel(self, source: str) -> None:
)
if channel is None:
raise ValueError(f"Channel '{source}' not found")
await self._tune_to_channel(channel)

async def set_channel_by_number(self, channel_number: str | int) -> None:
"""Change the channel by its number."""
number_str = str(channel_number)
channel = next(
(src for src in self._channels.values() if str(src.channel_number) == number_str), None
)
if channel is None:
raise ValueError(f"Channel number '{number_str}' not found")
await self._tune_to_channel(channel)

async def _tune_to_channel(self, channel) -> None:
"""Tune to a specific channel object via CPE.pushToTV."""
payload = {
"id": make_id(8),
"type": "CPE.pushToTV",
Expand Down
12 changes: 8 additions & 4 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -377,14 +377,18 @@ def print_boxes():

elif cmd == "channel":
if len(parts) < 3:
print(" Usage: channel <box_nr> <channel_name>")
print(" Usage: channel <box_nr> <number_or_name>")
continue
try:
idx = int(parts[1])
channel_name = parts[2]
channel_input = parts[2]
dev = device_list[idx]
print(f" Switching {dev.device_friendly_name} to {channel_name}...")
await dev.set_channel(channel_name)
print(f" Switching {dev.device_friendly_name} to {channel_input}...")
# Try by number first, fall back to name
try:
await dev.set_channel_by_number(channel_input)
except ValueError:
await dev.set_channel(channel_input)
print(" ✓ Channel switched!")
except (ValueError, IndexError):
print(f" Invalid box number. Use 'boxes' to see available boxes (0-{len(device_list)-1}).")
Expand Down
14 changes: 13 additions & 1 deletion web.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@

from lghorizon.lghorizon_api import LGHorizonApi
from lghorizon.lghorizon_models import LGHorizonAuth
from lghorizon.const import COUNTRY_SETTINGS
from lghorizon.const import COUNTRY_SETTINGS, MEDIA_KEYS

logging.basicConfig(
level=logging.DEBUG,
Expand Down Expand Up @@ -143,6 +143,12 @@ async def get_countries(request: web.Request):
return web.json_response({"countries": countries})


@routes.get("/api/keys")
async def get_keys(request: web.Request):
"""Return all available media keys grouped by category."""
return web.json_response({"keys": MEDIA_KEYS})


@routes.post("/api/login")
async def login(request: web.Request):
"""Authenticate and initialize the API connection."""
Expand Down Expand Up @@ -205,6 +211,7 @@ async def login(request: web.Request):
"success": True,
"devices": device_list,
"channels": channel_list,
"keys": MEDIA_KEYS,
})

except Exception as e:
Expand Down Expand Up @@ -330,6 +337,11 @@ async def handle_command(request: web.Request):
if not channel_name:
return web.json_response({"error": "Missing 'channel_name' parameter."}, status=400)
await device.set_channel(channel_name)
elif command == "set_channel_by_number":
channel_number = data.get("channel_number", "")
if not channel_number:
return web.json_response({"error": "Missing 'channel_number' parameter."}, status=400)
await device.set_channel_by_number(channel_number)
else:
return web.json_response({"error": f"Unknown command: {command}"}, status=400)

Expand Down
Loading