pax_global_header00006660000000000000000000000064145565516620014531gustar00rootroot0000000000000052 comment=e5781eba2c1d246edfc40163c2a435e88e98f8da Drafteed-python-lgnetcast-588e2ef/000077500000000000000000000000001455655166200172005ustar00rootroot00000000000000Drafteed-python-lgnetcast-588e2ef/.github/000077500000000000000000000000001455655166200205405ustar00rootroot00000000000000Drafteed-python-lgnetcast-588e2ef/.github/workflows/000077500000000000000000000000001455655166200225755ustar00rootroot00000000000000Drafteed-python-lgnetcast-588e2ef/.github/workflows/publish.yml000066400000000000000000000020701455655166200247650ustar00rootroot00000000000000# This workflow will upload a Python Package using Twine when a release is created # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries # This workflow uses actions that are not certified by GitHub. # They are provided by a third-party and are governed by # separate terms of service, privacy policy, and support # documentation. name: Upload Python Package on: release: types: [published] permissions: contents: read jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Set up Python uses: actions/setup-python@v3 with: python-version: '3.x' - name: Install dependencies run: | python -m pip install --upgrade pip pip install build - name: Build package run: python -m build - name: Publish package uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29 with: user: __token__ password: ${{ secrets.PYPI_TOKEN }} Drafteed-python-lgnetcast-588e2ef/.gitignore000066400000000000000000000001371455655166200211710ustar00rootroot00000000000000*.egg-info/ __pycache__ .idea/ .cache/ .mypy_cache/ .tox/ .venv/ .coverage dist/ docs/_build/Drafteed-python-lgnetcast-588e2ef/LICENSE000066400000000000000000000020601455655166200202030ustar00rootroot00000000000000The MIT License (MIT) Copyright (c) 2016 wokar Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. Drafteed-python-lgnetcast-588e2ef/README.md000066400000000000000000000034411455655166200204610ustar00rootroot00000000000000# Python LG Netcast A Python 3 library and command line tool to control LG Smart TV running NetCast 3.0 (LG Smart TV models released in 2012) and NetCast 4.0 (LG Smart TV models released in 2013) via TCP/IP. This library was forked from [wokar/pylgnetcast](https://github.com/wokar/pylgnetcast) and is primarily being developed with the intent of supporting [home-assistant](https://github.com/home-assistant/home-assistant). ## Dependencies * Python 3 * [requests](https://pypi.python.org/pypi/requests) package. ## API Usage ```python from xml.etree import ElementTree from pylgnetcast import LgNetCastClient, LG_COMMAND, LG_QUERY with LgNetCastClient('192.168.1.5', '889955') as client: client.send_command(LG_COMMAND.MUTE_TOGGLE) data = client.query_data(LG_QUERY.VOLUME_INFO) if data: print(ElementTree.tostring(data[0], encoding='unicode')) ``` ## Command Line Tool PyLgNetCast also provides a simple command line tool to remote control a TV. The tool needs a pairing key to be allowed to send commands to your TV. To get the pairing key just start the tool while your TV is on: ```sh python -m pylgnetcast --host ``` This will display the pairing key on your TV. To retrieve status information from your TV start the tool like: ```sh python -m pylgnetcast --host --pairing_key ``` This will display information about the current channel, volume, 3D mode, etc. retrieved from your TV. If you want to send a command to your TV, for instance to turn the volume up, check the list of available commands in the pylgnetcast.py file. The class LG_COMMAND defines all supported commands and volume up would be defined as 24. ```sh python -m pylgnetcast --host --pairing_key --command 24 ``` Drafteed-python-lgnetcast-588e2ef/pylgnetcast/000077500000000000000000000000001455655166200215355ustar00rootroot00000000000000Drafteed-python-lgnetcast-588e2ef/pylgnetcast/__init__.py000066400000000000000000000001071455655166200236440ustar00rootroot00000000000000__all__ = [] from . pylgnetcast import * __all__ += pylgnetcast.__all__Drafteed-python-lgnetcast-588e2ef/pylgnetcast/__main__.py000066400000000000000000000046071455655166200236360ustar00rootroot00000000000000""" Simple command line tool to control a LG NetCast TV. """ import argparse import logging import sys from xml.etree import ElementTree from . pylgnetcast import LgNetCastClient, AccessTokenError, LG_QUERY _LOGGER = logging.getLogger(__name__) def main(): """Process command line and send commands to TV.""" parser = argparse.ArgumentParser(prog='pylgnetcast', description='Remote control for a LG NetCast TV.') parser.add_argument('--host', metavar='host', type=str, required=True, help='Address of the TV') parser.add_argument('--pairing_key', metavar='pairing_key', type=str, help='Pairing key to access the TV') parser.add_argument('--protocol', metavar='protocol', type=str, default='roap', help='LG TV protocol hdcp or roap') parser.add_argument('--command', metavar='command', type=int, help='Remote control command to send to the TV') parser.add_argument('--verbose', dest='verbose', action='store_const', const=True, default=False, help='debug output') args = parser.parse_args() if args.verbose: logging.basicConfig(level=logging.DEBUG) try: with LgNetCastClient(args.host, args.pairing_key, args.protocol) as client: if args.command: client.send_command(args.command) print('Sent command %s' % args.command) infos = {'Channel Info': LG_QUERY.CUR_CHANNEL, 'Volume Info': LG_QUERY.VOLUME_INFO, 'Context Info': LG_QUERY.CONTEXT_UI, 'Is 3D': LG_QUERY.IS_3D} for title, query in infos.items(): try: data = client.query_data(query) if data: print('%s: %s' % (title, ElementTree.tostring(data[0], encoding='unicode'))) except Exception as error: 'Can not retrieve %s - error: %s' % (title.lower(), error) except AccessTokenError: print('Access token is displayed on TV - ' 'use it for the --pairing_key parameter to connect to your TV.') if __name__ == '__main__': sys.exit(main()) Drafteed-python-lgnetcast-588e2ef/pylgnetcast/pylgnetcast.py000066400000000000000000000210751455655166200244510ustar00rootroot00000000000000""" Client library for the LG Smart TV running NetCast 3 or 4. LG Smart TV models released in 2012 (NetCast 3.0) and LG Smart TV models released in 2013 (NetCast 4.0) are supported. For pre 2012 LG TV remote commands are supported by the "hdcp" protocol. The client is inspired by the work of https://github.com/ubaransel/lgcommander """ import logging import requests import time from xml.etree import ElementTree _LOGGER = logging.getLogger(__name__) __all__ = [ "LgNetCastClient", "LG_COMMAND", "LG_QUERY", "LgNetCastError", "AccessTokenError", "SessionIdError", ] # LG TV handler LG_HANDLE_KEY_INPUT = "HandleKeyInput" LG_HANDLE_MOUSE_MOVE = "HandleTouchMove" LG_HANDLE_MOUSE_CLICK = "HandleTouchClick" LG_HANDLE_TOUCH_WHEEL = "HandleTouchWheel" LG_HANDLE_CHANNEL_CHANGE = "HandleChannelChange" LG_CHANGE_INPUT_SOURCE = "ChangeInputSource" DEFAULT_PORT = 8080 DEFAULT_TIMEOUT = 3 class LG_COMMAND(object): """LG TV remote control commands.""" POWER = 1 NUMBER_0 = 2 NUMBER_1 = 3 NUMBER_2 = 4 NUMBER_3 = 5 NUMBER_4 = 6 NUMBER_5 = 7 NUMBER_6 = 8 NUMBER_7 = 9 NUMBER_8 = 10 NUMBER_9 = 11 UP = 12 DOWN = 13 LEFT = 14 RIGHT = 15 OK = 20 HOME_MENU = 21 BACK = 23 VOLUME_UP = 24 VOLUME_DOWN = 25 MUTE_TOGGLE = 26 CHANNEL_UP = 27 CHANNEL_DOWN = 28 BLUE = 29 GREEN = 30 RED = 31 YELLOW = 32 PLAY = 33 PAUSE = 34 STOP = 35 FAST_FORWARD = 36 REWIND = 37 SKIP_FORWARD = 38 SKIP_BACKWARD = 39 RECORD = 40 RECORDING_LIST = 41 REPEAT = 42 LIVE_TV = 43 EPG = 44 PROGRAM_INFORMATION = 45 ASPECT_RATIO = 46 EXTERNAL_INPUT = 47 PIP_SECONDARY_VIDEO = 48 SHOW_SUBTITLE = 49 PROGRAM_LIST = 50 TELE_TEXT = 51 MARK = 52 VIDEO_3D = 400 LR_3D = 401 DASH = 402 PREVIOUS_CHANNEL = 403 FAVORITE_CHANNEL = 404 QUICK_MENU = 405 TEXT_OPTION = 406 AUDIO_DESCRIPTION = 407 ENERGY_SAVING = 409 AV_MODE = 410 SIMPLINK = 411 EXIT = 412 RESERVATION_PROGRAM_LIST = 413 PIP_CHANNEL_UP = 414 PIP_CHANNEL_DOWN = 415 SWITCH_VIDEO = 416 APPS = 417 class LG_QUERY(object): """LG TV data queries.""" CUR_CHANNEL = "cur_channel" CHANNEL_LIST = "channel_list" CONTEXT_UI = "context_ui" VOLUME_INFO = "volume_info" SCREEN_IMAGE = "screen_image" IS_3D = "is_3d" class LG_PROTOCOL(object): """Supported LG TV protcols.""" HDCP = "hdcp" ROAP = "roap" class LgNetCastClient(object): """LG NetCast TV client using the ROAP or HDCP protocol.""" HEADER = {"Content-Type": "application/atom+xml"} XML = '' KEY = XML + "AuthKeyReq" AUTH = XML + "%s%s" COMMAND = XML + "%s%s%s" def __init__(self, host, access_token, protocol=LG_PROTOCOL.ROAP): """Initialize the LG TV client.""" self.url = "http://%s:%s/%s/api/" % (host, DEFAULT_PORT, protocol) self.access_token = access_token self.protocol = protocol self._session = None def __enter__(self): """Context manager method to support with statement.""" self._session = self._get_session_id() return self def __exit__(self, exc_type, exc_val, exc_tb): """Context manager method to support with statement.""" self._session = None def send_command(self, command): """Send remote control commands to the TV.""" message = self.COMMAND % ( self._session, LG_HANDLE_KEY_INPUT, "%s" % command, ) self._send_to_tv("command", message) def query_device_info(self): """Get model information about the TV.""" # We're using UDAP to retrieve this information, which requires a specific User-Agent. # As self.HEADER is a class variable, we will make a copy to ensure we don't interfere # with other instances try: self.HEADER = {**self.HEADER, "User-Agent": "UDAP/2.0"} response = self._send_to_tv("data", payload={"target": "rootservice.xml"}) finally: # Remove the instance version of self.HEADER to restore original functionality del self.HEADER if response.status_code != requests.codes.ok: return None data = response.text tree = ElementTree.fromstring(data.encode("utf-8")) return { "uuid": tree.findtext("device/uuid"), "model_name": tree.findtext("device/modelName"), "friendly_name": tree.findtext("device/friendlyName") } def change_input_source(self, type, index): """Send change input source command to the TV.""" message = self.COMMAND % ( self._session, LG_CHANGE_INPUT_SOURCE, "%d%d" % (type, index), ) self._send_to_tv("command", message) def change_channel(self, channel): """Send change channel command to the TV.""" message = self.COMMAND % ( self._session, LG_HANDLE_CHANNEL_CHANGE, ElementTree.tostring(channel, encoding="utf-8"), ) self._send_to_tv("command", message) def query_data(self, query): """Query status information from the TV.""" response = self._send_to_tv("data", payload={"target": query}) if response.status_code == requests.codes.ok: data = response.text tree = ElementTree.fromstring(data.encode("utf-8")) data_list = [] for data in tree.iter("data"): data_list.append(data) return data_list def get_volume(self): """Get volume info from the TV.""" volume_info = self.query_data("volume_info") if volume_info: volume_info = volume_info[0] volume = float(volume_info.find("level").text) muted = volume_info.find("mute").text == "true" return volume, muted def set_volume(self, volume): """Try to set a specific volume level.""" for _ in range(2): current_volume, _ = self.get_volume() self._set_volume_diff(volume, current_volume) def _set_volume_diff(self, volume, current_volume): """Simulate setting a specific volume level based on the difference.""" volume_difference = volume - current_volume if volume_difference != 0: command = ( LG_COMMAND.VOLUME_UP if volume_difference > 0 else LG_COMMAND.VOLUME_DOWN ) for _ in range(abs(int(volume_difference))): self.send_command(command) time.sleep(0.45) def _get_session_id(self): """Get the session key for the TV connection. If a pair key is defined the session id is requested otherwise display the pair key on TV. """ if not self.access_token: self._display_pair_key() raise AccessTokenError("No access token specified to create session.") message = self.AUTH % ("AuthReq", self.access_token) response = self._send_to_tv("auth", message) if response.status_code != requests.codes.ok: raise SessionIdError("Can not get session id from TV.") data = response.text tree = ElementTree.XML(data) session = tree.find("session").text return session def _display_pair_key(self): """Send message to display the pair key on TV screen.""" self._send_to_tv("auth", self.KEY) def _send_to_tv(self, message_type, message=None, payload=None): """Send message of given type to the tv.""" if message_type != "command" and self.protocol == LG_PROTOCOL.HDCP: message_type = "dtv_wifirc" url = "%s%s" % (self.url, message_type) if message: response = requests.post( url, data=message, headers=self.HEADER, timeout=DEFAULT_TIMEOUT ) else: response = requests.get( url, params=payload, headers=self.HEADER, timeout=DEFAULT_TIMEOUT ) return response class LgNetCastError(Exception): """Base class for all exceptions in this module.""" class AccessTokenError(LgNetCastError): """No access token specified to create session.""" class SessionIdError(LgNetCastError): """No session id could be retrieved from TV.""" Drafteed-python-lgnetcast-588e2ef/setup.py000066400000000000000000000015261455655166200207160ustar00rootroot00000000000000from setuptools import setup from os import path this_directory = path.abspath(path.dirname(__file__)) with open(path.join(this_directory, 'README.md'), encoding='utf-8') as f: long_description = f.read() setup( name='pylgnetcast', version='0.3.9', maintainer='Artem Draft', maintainer_email='artemon_93@mail.ru', description='Client for the LG Smart TV running NetCast 3 or 4.', long_description=long_description, long_description_content_type='text/markdown', url='https://github.com/Drafteed/python-lgnetcast', license='MIT', packages=['pylgnetcast'], install_requires=['requests'], classifiers=[ "Programming Language :: Python :: 3", "License :: OSI Approved :: MIT License", "Operating System :: OS Independent", ], python_requires='>=3.6', zip_safe=False )