import asyncio import base64 import codecs import json import os import websockets import logging logger = logging.getLogger(__name__) from .endpoints import * KEY_FILE_NAME = '.pylgtv' USER_HOME = 'HOME' HANDSHAKE_FILE_NAME = 'handshake.json' class PyLGTVPairException(Exception): def __init__(self, id, message): self.id = id self.message = message class WebOsClient(object): def __init__(self, ip, key_file_path=None, timeout_connect=2): """Initialize the client.""" self.ip = ip self.port = 3000 self.key_file_path = key_file_path self.client_key = None self.web_socket = None self.command_count = 0 self.last_response = None self.timeout_connect = timeout_connect self.load_key_file() @staticmethod def _get_key_file_path(): """Return the key file path.""" if os.getenv(USER_HOME) is not None and os.access(os.getenv(USER_HOME), os.W_OK): return os.path.join(os.getenv(USER_HOME), KEY_FILE_NAME) return os.path.join(os.getcwd(), KEY_FILE_NAME) def load_key_file(self): """Try to load the client key for the current ip.""" self.client_key = None if self.key_file_path: key_file_path = self.key_file_path else: key_file_path = self._get_key_file_path() key_dict = {} logger.debug('load keyfile from %s', key_file_path); if os.path.isfile(key_file_path): with open(key_file_path, 'r') as f: raw_data = f.read() if raw_data: key_dict = json.loads(raw_data) logger.debug('getting client_key for %s from %s', self.ip, key_file_path); if self.ip in key_dict: self.client_key = key_dict[self.ip] def save_key_file(self): """Save the current client key.""" if self.client_key is None: return if self.key_file_path: key_file_path = self.key_file_path else: key_file_path = self._get_key_file_path() logger.debug('save keyfile to %s', key_file_path); with open(key_file_path, 'w+') as f: raw_data = f.read() key_dict = {} if raw_data: key_dict = json.loads(raw_data) key_dict[self.ip] = self.client_key f.write(json.dumps(key_dict)) @asyncio.coroutine def _send_register_payload(self, websocket): """Send the register payload.""" file = os.path.join(os.path.dirname(__file__), HANDSHAKE_FILE_NAME) data = codecs.open(file, 'r', 'utf-8') raw_handshake = data.read() handshake = json.loads(raw_handshake) handshake['payload']['client-key'] = self.client_key yield from websocket.send(json.dumps(handshake)) raw_response = yield from websocket.recv() response = json.loads(raw_response) if response['type'] == 'response' and \ response['payload']['pairingType'] == 'PROMPT': raw_response = yield from websocket.recv() response = json.loads(raw_response) if response['type'] == 'registered': self.client_key = response['payload']['client-key'] self.save_key_file() def is_registered(self): """Paired with the tv.""" return self.client_key is not None @asyncio.coroutine def _register(self): """Register wrapper.""" logger.debug('register on %s', "ws://{}:{}".format(self.ip, self.port)); try: websocket = yield from websockets.connect( "ws://{}:{}".format(self.ip, self.port), timeout=self.timeout_connect) except: logger.error('register failed to connect to %s', "ws://{}:{}".format(self.ip, self.port)); return False logger.debug('register websocket connected to %s', "ws://{}:{}".format(self.ip, self.port)); try: yield from self._send_register_payload(websocket) finally: logger.debug('close register connection to %s', "ws://{}:{}".format(self.ip, self.port)); yield from websocket.close() def register(self): """Pair client with tv.""" loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(self._register()) @asyncio.coroutine def _command(self, msg): """Send a command to the tv.""" logger.debug('send command to %s', "ws://{}:{}".format(self.ip, self.port)); try: websocket = yield from websockets.connect( "ws://{}:{}".format(self.ip, self.port), timeout=self.timeout_connect) except: logger.debug('command failed to connect to %s', "ws://{}:{}".format(self.ip, self.port)); return False logger.debug('command websocket connected to %s', "ws://{}:{}".format(self.ip, self.port)); try: yield from self._send_register_payload(websocket) if not self.client_key: raise PyLGTVPairException("Unable to pair") yield from websocket.send(json.dumps(msg)) if msg['type'] == 'request': raw_response = yield from websocket.recv() self.last_response = json.loads(raw_response) finally: logger.debug('close command connection to %s', "ws://{}:{}".format(self.ip, self.port)); yield from websocket.close() def command(self, request_type, uri, payload): """Build and send a command.""" self.command_count += 1 if payload is None: payload = {} message = { 'id': "{}_{}".format(type, self.command_count), 'type': request_type, 'uri': "ssap://{}".format(uri), 'payload': payload, } self.last_response = None try: loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) loop.run_until_complete(asyncio.wait_for(self._command(message), self.timeout_connect, loop=loop)) finally: loop.close() def request(self, uri, payload=None): """Send a request.""" self.command('request', uri, payload) def send_message(self, message, icon_path=None): """Show a floating message.""" icon_encoded_string = '' icon_extension = '' if icon_path is not None: icon_extension = os.path.splitext(icon_path)[1][1:] with open(icon_path, 'rb') as icon_file: icon_encoded_string = base64.b64encode(icon_file.read()).decode('ascii') self.request(EP_SHOW_MESSAGE, { 'message': message, 'iconData': icon_encoded_string, 'iconExtension': icon_extension }) # Apps def get_apps(self): """Return all apps.""" self.request(EP_GET_APPS) return {} if self.last_response is None else self.last_response.get('payload').get('launchPoints') def get_current_app(self): """Get the current app id.""" self.request(EP_GET_CURRENT_APP_INFO) return None if self.last_response is None else self.last_response.get('payload').get('appId') def launch_app(self, app): """Launch an app.""" self.command('request', EP_LAUNCH, { 'id': app }) def launch_app_with_params(self, app, params): """Launch an app with parameters.""" self.request(EP_LAUNCH, { 'id': app, 'params': params }) def launch_app_with_content_id(self, app, contentId): """Launch an app with contentId.""" self.request(EP_LAUNCH, { 'id': app, 'contentId': contentId }) def close_app(self, app): """Close the current app.""" self.request(EP_LAUNCHER_CLOSE, { 'id': app }) # Services def get_services(self): """Get all services.""" self.request(EP_GET_SERVICES) return {} if self.last_response is None else self.last_response.get('payload').get('services') def get_software_info(self): """Return the current software status.""" self.request(EP_GET_SOFTWARE_INFO) return {} if self.last_response is None else self.last_response.get('payload') def power_off(self): """Play media.""" self.request(EP_POWER_OFF) def power_on(self): """Play media.""" self.request(EP_POWER_ON) # 3D Mode def turn_3d_on(self): """Turn 3D on.""" self.request(EP_3D_ON) def turn_3d_off(self): """Turn 3D off.""" self.request(EP_3D_OFF) # Inputs def get_inputs(self): """Get all inputs.""" self.request(EP_GET_INPUTS) return {} if self.last_response is None else self.last_response.get('payload').get('devices') def get_input(self): """Get current input.""" return self.get_current_app() def set_input(self, input): """Set the current input.""" self.request(EP_SET_INPUT, { 'inputId': input }) # Audio def get_audio_status(self): """Get the current audio status""" self.request(EP_GET_AUDIO_STATUS) return {} if self.last_response is None else self.last_response.get('payload') def get_muted(self): """Get mute status.""" return self.get_audio_status().get('mute') def set_mute(self, mute): """Set mute.""" self.request(EP_SET_MUTE, { 'mute': mute }) def get_volume(self): """Get the current volume.""" self.request(EP_GET_VOLUME) return 0 if self.last_response is None else self.last_response.get('payload').get('volume') def set_volume(self, volume): """Set volume.""" volume = max(0, volume) self.request(EP_SET_VOLUME, { 'volume': volume }) def volume_up(self): """Volume up.""" self.request(EP_VOLUME_UP) def volume_down(self): """Volume down.""" self.request(EP_VOLUME_DOWN) # TV Channel def channel_up(self): """Channel up.""" self.request(EP_TV_CHANNEL_UP) def channel_down(self): """Channel down.""" self.request(EP_TV_CHANNEL_DOWN) def get_channels(self): """Get all tv channels.""" self.request(EP_GET_TV_CHANNELS) return {} if self.last_response is None else self.last_response.get('payload').get('channelList') def get_current_channel(self): """Get the current tv channel.""" self.request(EP_GET_CURRENT_CHANNEL) return {} if self.last_response is None else self.last_response.get('payload') def get_channel_info(self): """Get the current channel info.""" self.request(EP_GET_CHANNEL_INFO) return {} if self.last_response is None else self.last_response.get('payload') def set_channel(self, channel): """Set the current channel.""" self.request(EP_SET_CHANNEL, { 'channelId': channel }) # Media control def play(self): """Play media.""" self.request(EP_MEDIA_PLAY) def pause(self): """Pause media.""" self.request(EP_MEDIA_PAUSE) def stop(self): """Stop media.""" self.request(EP_MEDIA_STOP) def close(self): """Close media.""" self.request(EP_MEDIA_CLOSE) def rewind(self): """Rewind media.""" self.request(EP_MEDIA_REWIND) def fast_forward(self): """Fast Forward media.""" self.request(EP_MEDIA_FAST_FORWARD) # Keys def send_enter_key(self): """Send enter key.""" self.request(EP_SEND_ENTER) def send_delete_key(self): """Send delete key.""" self.request(EP_SEND_DELETE) # Web def open_url(self, url): """Open URL.""" self.request(EP_OPEN, { 'target': url }) def close_web(self): """Close web app.""" self.request(EP_CLOSE_WEB_APP)