diff --git a/lghorizon/__init__.py b/lghorizon/__init__.py index 1080dd5..3bafef1 100644 --- a/lghorizon/__init__.py +++ b/lghorizon/__init__.py @@ -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", @@ -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", ] diff --git a/lghorizon/const.py b/lghorizon/const.py index dddbc22..a756e2b 100644 --- a/lghorizon/const.py +++ b/lghorizon/const.py @@ -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" diff --git a/lghorizon/lghorizon_api.py b/lghorizon/lghorizon_api.py index b98f8d5..fd7467e 100644 --- a/lghorizon/lghorizon_api.py +++ b/lghorizon/lghorizon_api.py @@ -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 @@ -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.""" diff --git a/lghorizon/lghorizon_device.py b/lghorizon/lghorizon_device.py index f66fa66..68d5b56 100644 --- a/lghorizon/lghorizon_device.py +++ b/lghorizon/lghorizon_device.py @@ -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", diff --git a/main.py b/main.py index c698fd8..899b4fe 100644 --- a/main.py +++ b/main.py @@ -377,14 +377,18 @@ def print_boxes(): elif cmd == "channel": if len(parts) < 3: - print(" Usage: channel ") + print(" Usage: channel ") 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}).") diff --git a/web.py b/web.py index 3a010f4..627b7d9 100644 --- a/web.py +++ b/web.py @@ -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, @@ -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.""" @@ -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: @@ -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) diff --git a/web_ui.html b/web_ui.html index 7d2f403..d288a56 100644 --- a/web_ui.html +++ b/web_ui.html @@ -4,7 +4,6 @@ LG Horizon Test UI - - +
-