├── .gitignore ├── LICENSE ├── MANIFEST.in ├── README.rst ├── samsungctl.conf ├── samsungctl ├── __init__.py ├── __main__.py ├── exceptions.py ├── interactive.py ├── remote.py ├── remote_legacy.py └── remote_websocket.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | dist/ 3 | *.egg-info/ 4 | *.pyc 5 | *.swp 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Lauri Niskanen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.rst 2 | include LICENSE 3 | include samsungctl.conf 4 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======================= 2 | This project is no longer being developed 3 | ======================= 4 | 5 | **The repository is now archived and read-only. Feel free to continue the development in a fork.** 6 | 7 | The most prominent fork as of the archival seems to be: 8 | https://github.com/kdschlosser/samsungctl 9 | 10 | ---- 11 | 12 | ========== 13 | samsungctl 14 | ========== 15 | 16 | samsungctl is a library and a command line tool for remote controlling Samsung 17 | televisions via a TCP/IP connection. It currently supports both pre-2016 TVs as 18 | well most of the modern Tizen-OS TVs with Ethernet or Wi-Fi connectivity. 19 | 20 | Dependencies 21 | ============ 22 | 23 | - Python 3 24 | - ``websocket-client`` (optional, for 2016+ TVs) 25 | - ``curses`` (optional, for the interactive mode) 26 | 27 | Installation 28 | ============ 29 | 30 | samsungctl can be installed using `pip <(https://pip.pypa.io/>`_: 31 | 32 | :: 33 | 34 | # pip install samsungctl 35 | 36 | Alternatively you can clone the Git repository and run: 37 | 38 | :: 39 | 40 | # python setup.py install 41 | 42 | It's possible to use the command line tool without installation: 43 | 44 | :: 45 | 46 | $ python -m samsungctl 47 | 48 | Command line usage 49 | ================== 50 | 51 | You can use ``samsungctl`` command to send keys to a TV: 52 | 53 | :: 54 | 55 | $ samsungctl --host [options] [key ...] 56 | 57 | ``host`` is the hostname or IP address of the TV. ``key`` is a key code, e.g. 58 | ``KEY_VOLDOWN``. See `Key codes`_. 59 | 60 | There is also an interactive mode (ncurses) for sending the key presses: 61 | 62 | :: 63 | 64 | $ samsungctl --host [options] --interactive 65 | 66 | Use ``samsungctl --help`` for more information about the command line 67 | arguments: 68 | 69 | :: 70 | 71 | usage: samsungctl [-h] [--version] [-v] [-q] [-i] [--host HOST] [--port PORT] 72 | [--method METHOD] [--name NAME] [--description DESC] 73 | [--id ID] [--timeout TIMEOUT] 74 | [key [key ...]] 75 | 76 | Remote control Samsung televisions via TCP/IP connection 77 | 78 | positional arguments: 79 | key keys to be sent (e.g. KEY_VOLDOWN) 80 | 81 | optional arguments: 82 | -h, --help show this help message and exit 83 | --version show program's version number and exit 84 | -v, --verbose increase output verbosity 85 | -q, --quiet suppress non-fatal output 86 | -i, --interactive interactive control 87 | --host HOST TV hostname or IP address 88 | --port PORT TV port number (TCP) 89 | --method METHOD Connection method (legacy or websocket) 90 | --name NAME remote control name 91 | --description DESC remote control description 92 | --id ID remote control id 93 | --timeout TIMEOUT socket timeout in seconds (0 = no timeout) 94 | 95 | E.g. samsungctl --host 192.168.0.10 --name myremote KEY_VOLDOWN 96 | 97 | The settings can be loaded from a configuration file. The file is searched from 98 | ``$XDG_CONFIG_HOME/samsungctl.conf``, ``~/.config/samsungctl.conf``, and 99 | ``/etc/samsungctl.conf`` in this order. A simple default configuration is 100 | bundled with the source as `samsungctl.conf `_. 101 | 102 | Library usage 103 | ============= 104 | 105 | samsungctl can be imported as a Python 3 library: 106 | 107 | .. code-block:: python 108 | 109 | import samsungctl 110 | 111 | A context managed remote controller object of class ``Remote`` can be 112 | constructed using the ``with`` statement: 113 | 114 | .. code-block:: python 115 | 116 | with samsungctl.Remote(config) as remote: 117 | # Use the remote object 118 | 119 | The constructor takes a configuration dictionary as a parameter. All 120 | configuration items must be specified. 121 | 122 | =========== ====== =========================================== 123 | Key Type Description 124 | =========== ====== =========================================== 125 | host string Hostname or IP address of the TV. 126 | port int TCP port number. (Default: ``55000``) 127 | method string Connection method (``legacy`` or ``websocket``) 128 | name string Name of the remote controller. 129 | description string Remote controller description. 130 | id string Additional remote controller ID. 131 | timeout int Timeout in seconds. ``0`` means no timeout. 132 | =========== ====== =========================================== 133 | 134 | The ``Remote`` object is very simple and you only need the ``control(key)`` 135 | method. The only parameter is a string naming the key to be sent (e.g. 136 | ``KEY_VOLDOWN``). See `Key codes`_. You can call ``control`` multiple times 137 | using the same ``Remote`` object. The connection is automatically closed when 138 | exiting the ``with`` statement. 139 | 140 | When something goes wrong you will receive an exception: 141 | 142 | ================= ======================================= 143 | Exception Description 144 | ================= ======================================= 145 | AccessDenied The TV does not allow you to send keys. 146 | ConnectionClosed The connection was closed. 147 | UnhandledResponse An unexpected response was received. 148 | socket.timeout The connection timed out. 149 | ================= ======================================= 150 | 151 | Example program 152 | --------------- 153 | 154 | This simple program opens and closes the menu a few times. 155 | 156 | .. code-block:: python 157 | 158 | #!/usr/bin/env python3 159 | 160 | import samsungctl 161 | import time 162 | 163 | config = { 164 | "name": "samsungctl", 165 | "description": "PC", 166 | "id": "", 167 | "host": "192.168.0.10", 168 | "port": 55000, 169 | "method": "legacy", 170 | "timeout": 0, 171 | } 172 | 173 | with samsungctl.Remote(config) as remote: 174 | for i in range(10): 175 | remote.control("KEY_MENU") 176 | time.sleep(0.5) 177 | 178 | Key codes 179 | ========= 180 | 181 | The list of accepted keys may vary depending on the TV model, but the following 182 | list has some common key codes and their descriptions. 183 | 184 | ================= ============ 185 | Key code Description 186 | ================= ============ 187 | KEY_POWEROFF Power off 188 | KEY_UP Up 189 | KEY_DOWN Down 190 | KEY_LEFT Left 191 | KEY_RIGHT Right 192 | KEY_CHUP P Up 193 | KEY_CHDOWN P Down 194 | KEY_ENTER Enter 195 | KEY_RETURN Return 196 | KEY_CH_LIST Channel List 197 | KEY_MENU Menu 198 | KEY_SOURCE Source 199 | KEY_GUIDE Guide 200 | KEY_TOOLS Tools 201 | KEY_INFO Info 202 | KEY_RED A / Red 203 | KEY_GREEN B / Green 204 | KEY_YELLOW C / Yellow 205 | KEY_BLUE D / Blue 206 | KEY_PANNEL_CHDOWN 3D 207 | KEY_VOLUP Volume Up 208 | KEY_VOLDOWN Volume Down 209 | KEY_MUTE Mute 210 | KEY_0 0 211 | KEY_1 1 212 | KEY_2 2 213 | KEY_3 3 214 | KEY_4 4 215 | KEY_5 5 216 | KEY_6 6 217 | KEY_7 7 218 | KEY_8 8 219 | KEY_9 9 220 | KEY_DTV TV Source 221 | KEY_HDMI HDMI Source 222 | KEY_CONTENTS SmartHub 223 | ================= ============ 224 | 225 | Please note that some codes are different on the 2016+ TVs. For example, 226 | ``KEY_POWEROFF`` is ``KEY_POWER`` on the newer TVs. 227 | 228 | References 229 | ========== 230 | 231 | I did not reverse engineer the control protocol myself and samsungctl is not 232 | the only implementation. Here is the list of things that inspired samsungctl. 233 | 234 | - http://sc0ty.pl/2012/02/samsung-tv-network-remote-control-protocol/ 235 | - https://gist.github.com/danielfaust/998441 236 | - https://github.com/Bntdumas/SamsungIPRemote 237 | - https://github.com/kyleaa/homebridge-samsungtv2016 238 | -------------------------------------------------------------------------------- /samsungctl.conf: -------------------------------------------------------------------------------- 1 | { 2 | "name": "samsungctl", 3 | "description": "PC", 4 | "id": "", 5 | "method": "legacy", 6 | "port": 55000, 7 | "timeout": 0 8 | } 9 | -------------------------------------------------------------------------------- /samsungctl/__init__.py: -------------------------------------------------------------------------------- 1 | """Remote control Samsung televisions via TCP/IP connection""" 2 | 3 | from .remote import Remote 4 | 5 | __title__ = "samsungctl" 6 | __version__ = "0.7.1+1" 7 | __url__ = "https://github.com/Ape/samsungctl" 8 | __author__ = "Lauri Niskanen" 9 | __author_email__ = "ape@ape3000.com" 10 | __license__ = "MIT" 11 | -------------------------------------------------------------------------------- /samsungctl/__main__.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | import collections 3 | import json 4 | import logging 5 | import os 6 | import socket 7 | import errno 8 | 9 | from . import __doc__ as doc 10 | from . import __title__ as title 11 | from . import __version__ as version 12 | from . import exceptions 13 | from . import Remote 14 | 15 | 16 | def _read_config(): 17 | config = collections.defaultdict(lambda: None, { 18 | "name": "samsungctl", 19 | "description": "PC", 20 | "id": "", 21 | "method": "legacy", 22 | "timeout": 0, 23 | }) 24 | 25 | file_loaded = False 26 | directories = [] 27 | 28 | xdg_config = os.getenv("XDG_CONFIG_HOME") 29 | if xdg_config: 30 | directories.append(xdg_config) 31 | 32 | directories.append(os.path.join(os.getenv("HOME"), ".config")) 33 | directories.append("/etc") 34 | 35 | for directory in directories: 36 | path = os.path.join(directory, "samsungctl.conf") 37 | try: 38 | config_file = open(path) 39 | except IOError as e: 40 | if e.errno == errno.ENOENT: 41 | continue 42 | else: 43 | raise 44 | else: 45 | file_loaded = True 46 | break 47 | 48 | if not file_loaded: 49 | return config 50 | 51 | with config_file: 52 | try: 53 | config_json = json.load(config_file) 54 | except ValueError as e: 55 | message = "Warning: Could not parse the configuration file.\n %s" 56 | logging.warning(message, e) 57 | return config 58 | 59 | config.update(config_json) 60 | 61 | return config 62 | 63 | 64 | def main(): 65 | epilog = "E.g. %(prog)s --host 192.168.0.10 --name myremote KEY_VOLDOWN" 66 | parser = argparse.ArgumentParser(prog=title, description=doc, 67 | epilog=epilog) 68 | parser.add_argument("--version", action="version", 69 | version="%(prog)s {0}".format(version)) 70 | parser.add_argument("-v", "--verbose", action="count", 71 | help="increase output verbosity") 72 | parser.add_argument("-q", "--quiet", action="store_true", 73 | help="suppress non-fatal output") 74 | parser.add_argument("-i", "--interactive", action="store_true", 75 | help="interactive control") 76 | parser.add_argument("--host", help="TV hostname or IP address") 77 | parser.add_argument("--port", type=int, help="TV port number (TCP)") 78 | parser.add_argument("--method", 79 | help="Connection method (legacy or websocket)") 80 | parser.add_argument("--name", help="remote control name") 81 | parser.add_argument("--description", metavar="DESC", 82 | help="remote control description") 83 | parser.add_argument("--id", help="remote control id") 84 | parser.add_argument("--timeout", type=float, 85 | help="socket timeout in seconds (0 = no timeout)") 86 | parser.add_argument("key", nargs="*", 87 | help="keys to be sent (e.g. KEY_VOLDOWN)") 88 | 89 | args = parser.parse_args() 90 | 91 | if args.quiet: 92 | log_level = logging.ERROR 93 | elif not args.verbose: 94 | log_level = logging.WARNING 95 | elif args.verbose == 1: 96 | log_level = logging.INFO 97 | else: 98 | log_level = logging.DEBUG 99 | 100 | logging.basicConfig(format="%(message)s", level=log_level) 101 | 102 | config = _read_config() 103 | config.update({k: v for k, v in vars(args).items() if v is not None}) 104 | 105 | if not config["host"]: 106 | logging.error("Error: --host must be set") 107 | return 108 | 109 | try: 110 | with Remote(config) as remote: 111 | for key in args.key: 112 | remote.control(key) 113 | 114 | if args.interactive: 115 | logging.getLogger().setLevel(logging.ERROR) 116 | from . import interactive 117 | interactive.run(remote) 118 | elif len(args.key) == 0: 119 | logging.warning("Warning: No keys specified.") 120 | except exceptions.ConnectionClosed: 121 | logging.error("Error: Connection closed!") 122 | except exceptions.AccessDenied: 123 | logging.error("Error: Access denied!") 124 | except exceptions.UnknownMethod: 125 | logging.error("Error: Unknown method '{}'".format(config["method"])) 126 | except socket.timeout: 127 | logging.error("Error: Timed out!") 128 | except OSError as e: 129 | logging.error("Error: %s", e.strerror) 130 | 131 | 132 | if __name__ == "__main__": 133 | main() 134 | -------------------------------------------------------------------------------- /samsungctl/exceptions.py: -------------------------------------------------------------------------------- 1 | class AccessDenied(Exception): 2 | """Connection was denied.""" 3 | pass 4 | 5 | 6 | class ConnectionClosed(Exception): 7 | """Connection was closed.""" 8 | pass 9 | 10 | 11 | class UnhandledResponse(Exception): 12 | """Received unknown response.""" 13 | pass 14 | 15 | 16 | class UnknownMethod(Exception): 17 | """Unknown method.""" 18 | pass 19 | -------------------------------------------------------------------------------- /samsungctl/interactive.py: -------------------------------------------------------------------------------- 1 | import curses 2 | 3 | 4 | _mappings = [ 5 | ["p", "KEY_POWEROFF", "P", "Power off"], 6 | ["KEY_UP", "KEY_UP", "Up", "Up"], 7 | ["KEY_DOWN", "KEY_DOWN", "Down", "Down"], 8 | ["KEY_LEFT", "KEY_LEFT", "Left", "Left"], 9 | ["KEY_RIGHT", "KEY_RIGHT", "Right", "Right"], 10 | ["KEY_PPAGE", "KEY_CHUP", "Page Up", "P Up"], 11 | ["KEY_NPAGE", "KEY_CHDOWN", "Page Down", "P Down"], 12 | ["\n", "KEY_ENTER", "Enter", "Enter"], 13 | ["KEY_BACKSPACE", "KEY_RETURN", "Backspace", "Return"], 14 | ["e", "KEY_EXIT", "E", "Exit"], 15 | ["h", "KEY_CONTENTS", "H", "Smart Hub"], 16 | ["l", "KEY_CH_LIST", "L", "Channel List"], 17 | ["m", "KEY_MENU", "M", "Menu"], 18 | ["s", "KEY_SOURCE", "S", "Source"], 19 | ["g", "KEY_GUIDE", "G", "Guide"], 20 | ["t", "KEY_TOOLS", "T", "Tools"], 21 | ["i", "KEY_INFO", "I", "Info"], 22 | ["z", "KEY_RED", "Z", "A / Red"], 23 | ["x", "KEY_GREEN", "X", "B / Green"], 24 | ["c", "KEY_YELLOW", "C", "C / Yellow"], 25 | ["v", "KEY_BLUE", "V", "D / Blue"], 26 | ["d", "KEY_PANNEL_CHDOWN", "D", "3D"], 27 | ["+", "KEY_VOLUP", "+", "Volume Up"], 28 | ["-", "KEY_VOLDOWN", "-", "Volume Down"], 29 | ["*", "KEY_MUTE", "*", "Mute"], 30 | ["0", "KEY_0", "0", "0"], 31 | ["1", "KEY_1", "1", "1"], 32 | ["2", "KEY_2", "2", "2"], 33 | ["3", "KEY_3", "3", "3"], 34 | ["4", "KEY_4", "4", "4"], 35 | ["5", "KEY_5", "5", "5"], 36 | ["6", "KEY_6", "6", "6"], 37 | ["7", "KEY_7", "7", "7"], 38 | ["8", "KEY_8", "8", "8"], 39 | ["9", "KEY_9", "9", "9"], 40 | ["KEY_F(1)", "KEY_DTV", "F1", "TV Source"], 41 | ["KEY_F(2)", "KEY_HDMI", "F2", "HDMI Source"], 42 | ] 43 | 44 | 45 | def run(remote): 46 | """Run interactive remote control application.""" 47 | curses.wrapper(_control, remote) 48 | 49 | 50 | def _control(stdscr, remote): 51 | height, width = stdscr.getmaxyx() 52 | 53 | stdscr.addstr("Interactive mode, press 'Q' to exit.\n") 54 | stdscr.addstr("Key mappings:\n") 55 | 56 | column_len = max(len(mapping[2]) for mapping in _mappings) + 1 57 | mappings_dict = {} 58 | for mapping in _mappings: 59 | mappings_dict[mapping[0]] = mapping[1] 60 | 61 | row = stdscr.getyx()[0] + 2 62 | if row < height: 63 | line = " {}= {} ({})\n".format(mapping[2].ljust(column_len), 64 | mapping[3], mapping[1]) 65 | stdscr.addstr(line) 66 | elif row == height: 67 | stdscr.addstr("[Terminal is too small to show all keys]\n") 68 | 69 | running = True 70 | while running: 71 | key = stdscr.getkey() 72 | 73 | if key == "q": 74 | running = False 75 | 76 | if key in mappings_dict: 77 | remote.control(mappings_dict[key]) 78 | 79 | try: 80 | stdscr.addstr(".") 81 | except curses.error: 82 | stdscr.deleteln() 83 | stdscr.move(stdscr.getyx()[0], 0) 84 | stdscr.addstr(".") 85 | -------------------------------------------------------------------------------- /samsungctl/remote.py: -------------------------------------------------------------------------------- 1 | from . import exceptions 2 | from .remote_legacy import RemoteLegacy 3 | from .remote_websocket import RemoteWebsocket 4 | 5 | 6 | class Remote: 7 | def __init__(self, config): 8 | if config["method"] == "legacy": 9 | self.remote = RemoteLegacy(config) 10 | elif config["method"] == "websocket": 11 | self.remote = RemoteWebsocket(config) 12 | else: 13 | raise exceptions.UnknownMethod() 14 | 15 | def __enter__(self): 16 | return self.remote.__enter__() 17 | 18 | def __exit__(self, type, value, traceback): 19 | self.remote.__exit__(type, value, traceback) 20 | 21 | def close(self): 22 | return self.remote.close() 23 | 24 | def control(self, key): 25 | return self.remote.control(key) 26 | -------------------------------------------------------------------------------- /samsungctl/remote_legacy.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import logging 3 | import socket 4 | import time 5 | 6 | from . import exceptions 7 | 8 | 9 | class RemoteLegacy(): 10 | """Object for remote control connection.""" 11 | 12 | def __init__(self, config): 13 | if not config["port"]: 14 | config["port"] = 55000 15 | 16 | """Make a new connection.""" 17 | self.connection = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 18 | 19 | if config["timeout"]: 20 | self.connection.settimeout(config["timeout"]) 21 | 22 | self.connection.connect((config["host"], config["port"])) 23 | 24 | payload = b"\x64\x00" \ 25 | + self._serialize_string(config["description"]) \ 26 | + self._serialize_string(config["id"]) \ 27 | + self._serialize_string(config["name"]) 28 | packet = b"\x00\x00\x00" + self._serialize_string(payload, True) 29 | 30 | logging.info("Sending handshake.") 31 | self.connection.send(packet) 32 | self._read_response(True) 33 | 34 | def __enter__(self): 35 | return self 36 | 37 | def __exit__(self, type, value, traceback): 38 | self.close() 39 | 40 | def close(self): 41 | """Close the connection.""" 42 | if self.connection: 43 | self.connection.close() 44 | self.connection = None 45 | logging.debug("Connection closed.") 46 | 47 | def control(self, key): 48 | """Send a control command.""" 49 | if not self.connection: 50 | raise exceptions.ConnectionClosed() 51 | 52 | payload = b"\x00\x00\x00" + self._serialize_string(key) 53 | packet = b"\x00\x00\x00" + self._serialize_string(payload, True) 54 | 55 | logging.info("Sending control command: %s", key) 56 | self.connection.send(packet) 57 | self._read_response() 58 | time.sleep(self._key_interval) 59 | 60 | _key_interval = 0.2 61 | 62 | def _read_response(self, first_time=False): 63 | header = self.connection.recv(3) 64 | tv_name_len = int.from_bytes(header[1:3], 65 | byteorder="little") 66 | tv_name = self.connection.recv(tv_name_len) 67 | 68 | if first_time: 69 | logging.debug("Connected to '%s'.", tv_name.decode()) 70 | 71 | response_len = int.from_bytes(self.connection.recv(2), 72 | byteorder="little") 73 | response = self.connection.recv(response_len) 74 | 75 | if len(response) == 0: 76 | self.close() 77 | raise exceptions.ConnectionClosed() 78 | 79 | if response == b"\x64\x00\x01\x00": 80 | logging.debug("Access granted.") 81 | return 82 | elif response == b"\x64\x00\x00\x00": 83 | raise exceptions.AccessDenied() 84 | elif response[0:1] == b"\x0a": 85 | if first_time: 86 | logging.warning("Waiting for authorization...") 87 | return self._read_response() 88 | elif response[0:1] == b"\x65": 89 | logging.warning("Authorization cancelled.") 90 | raise exceptions.AccessDenied() 91 | elif response == b"\x00\x00\x00\x00": 92 | logging.debug("Control accepted.") 93 | return 94 | 95 | raise exceptions.UnhandledResponse(response) 96 | 97 | @staticmethod 98 | def _serialize_string(string, raw=False): 99 | if isinstance(string, str): 100 | string = str.encode(string) 101 | 102 | if not raw: 103 | string = base64.b64encode(string) 104 | 105 | return bytes([len(string)]) + b"\x00" + string 106 | -------------------------------------------------------------------------------- /samsungctl/remote_websocket.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import json 3 | import logging 4 | import socket 5 | import time 6 | 7 | from . import exceptions 8 | 9 | 10 | URL_FORMAT = "ws://{}:{}/api/v2/channels/samsung.remote.control?name={}" 11 | 12 | 13 | class RemoteWebsocket(): 14 | """Object for remote control connection.""" 15 | 16 | def __init__(self, config): 17 | import websocket 18 | 19 | if not config["port"]: 20 | config["port"] = 8001 21 | 22 | if config["timeout"] == 0: 23 | config["timeout"] = None 24 | 25 | url = URL_FORMAT.format(config["host"], config["port"], 26 | self._serialize_string(config["name"])) 27 | 28 | self.connection = websocket.create_connection(url, config["timeout"]) 29 | 30 | self._read_response() 31 | 32 | def __enter__(self): 33 | return self 34 | 35 | def __exit__(self, type, value, traceback): 36 | self.close() 37 | 38 | def close(self): 39 | """Close the connection.""" 40 | if self.connection: 41 | self.connection.close() 42 | self.connection = None 43 | logging.debug("Connection closed.") 44 | 45 | def control(self, key): 46 | """Send a control command.""" 47 | if not self.connection: 48 | raise exceptions.ConnectionClosed() 49 | 50 | payload = json.dumps({ 51 | "method": "ms.remote.control", 52 | "params": { 53 | "Cmd": "Click", 54 | "DataOfCmd": key, 55 | "Option": "false", 56 | "TypeOfRemote": "SendRemoteKey" 57 | } 58 | }) 59 | 60 | logging.info("Sending control command: %s", key) 61 | self.connection.send(payload) 62 | time.sleep(self._key_interval) 63 | 64 | _key_interval = 0.5 65 | 66 | def _read_response(self): 67 | response = self.connection.recv() 68 | response = json.loads(response) 69 | 70 | if response["event"] != "ms.channel.connect": 71 | self.close() 72 | raise exceptions.UnhandledResponse(response) 73 | 74 | logging.debug("Access granted.") 75 | 76 | @staticmethod 77 | def _serialize_string(string): 78 | if isinstance(string, str): 79 | string = str.encode(string) 80 | 81 | return base64.b64encode(string).decode("utf-8") 82 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import setuptools 4 | 5 | import samsungctl 6 | 7 | setuptools.setup( 8 | name=samsungctl.__title__, 9 | version=samsungctl.__version__, 10 | description=samsungctl.__doc__, 11 | url=samsungctl.__url__, 12 | author=samsungctl.__author__, 13 | author_email=samsungctl.__author_email__, 14 | license=samsungctl.__license__, 15 | long_description=open("README.rst").read(), 16 | entry_points={ 17 | "console_scripts": ["samsungctl=samsungctl.__main__:main"] 18 | }, 19 | packages=["samsungctl"], 20 | install_requires=[], 21 | extras_require={ 22 | "websocket": ["websocket-client"], 23 | "interactive_ui": ["curses"], 24 | }, 25 | classifiers=[ 26 | "Development Status :: 4 - Beta", 27 | "Environment :: Console", 28 | "License :: OSI Approved :: MIT License", 29 | "Programming Language :: Python :: 3", 30 | "Topic :: Home Automation", 31 | ], 32 | ) 33 | --------------------------------------------------------------------------------