├── .gitignore ├── CONTRIBUTING.rst ├── LICENSE ├── README.rst ├── app.py ├── bin └── capture.sh ├── media └── .gitignore ├── requirements.txt ├── requirements_dev.txt ├── samsungtv ├── __init__.py ├── __main__.py ├── app.py ├── dlna │ ├── __init__.py │ ├── device.py │ ├── device_services.py │ ├── devices.py │ └── utils.py ├── httpd │ ├── __init__.py │ ├── composite_handler.py │ ├── proxy_handler.py │ ├── server.py │ ├── serverctrl.py │ └── subscribe_handler.py ├── services │ ├── __init__.py │ ├── dial.py │ ├── remote_control.py │ └── ssdp.py ├── upnpevents │ ├── __init__.py │ └── subscriber.py └── upnpservice │ ├── __init__.py │ ├── avtransport.py │ ├── base.py │ ├── connectionmanager.py │ ├── dialreceiver.py │ ├── rendering.py │ └── wfaconfig.py ├── setup.cfg ├── setup.py ├── tests ├── __init__.py ├── mocks.py ├── test_dial_service.py ├── test_dlna_device.py ├── test_dlna_devices.py ├── test_event_subscriber.py ├── test_proxy_http_handler.py ├── test_remote_control.py ├── test_ssdp_discovery.py ├── test_subscribe_http_handler.py ├── test_upnpservice_av_transport.py ├── test_upnpservice_connection_manager.py ├── test_upnpservice_rendering.py └── test_upnpservice_wfa_config.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | ./cache/* 3 | ./media/* 4 | 5 | # Created by https://www.gitignore.io/api/python,eclipse,phpstorm 6 | 7 | ### Eclipse ### 8 | 9 | .metadata 10 | bin/ 11 | tmp/ 12 | *.tmp 13 | *.bak 14 | *.swp 15 | *~.nib 16 | local.properties 17 | .settings/ 18 | .loadpath 19 | .recommenders 20 | 21 | # External tool builders 22 | .externalToolBuilders/ 23 | 24 | # Locally stored "Eclipse launch configurations" 25 | *.launch 26 | 27 | # PyDev specific (Python IDE for Eclipse) 28 | *.pydevproject 29 | 30 | # CDT-specific (C/C++ Development Tooling) 31 | .cproject 32 | 33 | # Java annotation processor (APT) 34 | .factorypath 35 | 36 | # PDT-specific (PHP Development Tools) 37 | .buildpath 38 | 39 | # sbteclipse plugin 40 | .target 41 | 42 | # Tern plugin 43 | .tern-project 44 | 45 | # TeXlipse plugin 46 | .texlipse 47 | 48 | # STS (Spring Tool Suite) 49 | .springBeans 50 | 51 | # Code Recommenders 52 | .recommenders/ 53 | 54 | # Scala IDE specific (Scala & Java development for Eclipse) 55 | .cache-main 56 | .scala_dependencies 57 | .worksheet 58 | 59 | ### Eclipse Patch ### 60 | # Eclipse Core 61 | .project 62 | 63 | # JDT-specific (Eclipse Java Development Tools) 64 | .classpath 65 | 66 | ### PhpStorm ### 67 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 68 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 69 | 70 | # User-specific stuff: 71 | .idea/**/workspace.xml 72 | .idea/**/tasks.xml 73 | .idea/dictionaries 74 | 75 | # Sensitive or high-churn files: 76 | .idea/**/dataSources/ 77 | .idea/**/dataSources.ids 78 | .idea/**/dataSources.xml 79 | .idea/**/dataSources.local.xml 80 | .idea/**/sqlDataSources.xml 81 | .idea/**/dynamic.xml 82 | .idea/**/uiDesigner.xml 83 | 84 | # Gradle: 85 | .idea/**/gradle.xml 86 | .idea/**/libraries 87 | 88 | # CMake 89 | cmake-build-debug/ 90 | 91 | # Mongo Explorer plugin: 92 | .idea/**/mongoSettings.xml 93 | 94 | ## File-based project format: 95 | *.iws 96 | 97 | ## Plugin-specific files: 98 | 99 | # IntelliJ 100 | /out/ 101 | 102 | # mpeltonen/sbt-idea plugin 103 | .idea_modules/ 104 | 105 | # JIRA plugin 106 | atlassian-ide-plugin.xml 107 | 108 | # Cursive Clojure plugin 109 | .idea/replstate.xml 110 | 111 | # Ruby plugin and RubyMine 112 | /.rakeTasks 113 | 114 | # Crashlytics plugin (for Android Studio and IntelliJ) 115 | com_crashlytics_export_strings.xml 116 | crashlytics.properties 117 | crashlytics-build.properties 118 | fabric.properties 119 | 120 | ### PhpStorm Patch ### 121 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 122 | 123 | # *.iml 124 | # modules.xml 125 | # .idea/misc.xml 126 | # *.ipr 127 | 128 | # Sonarlint plugin 129 | .idea/sonarlint 130 | 131 | ### Python ### 132 | # Byte-compiled / optimized / DLL files 133 | __pycache__/ 134 | *.py[cod] 135 | *$py.class 136 | 137 | # C extensions 138 | *.so 139 | 140 | # Distribution / packaging 141 | .Python 142 | build/ 143 | develop-eggs/ 144 | dist/ 145 | downloads/ 146 | eggs/ 147 | .eggs/ 148 | lib/ 149 | lib64/ 150 | parts/ 151 | sdist/ 152 | var/ 153 | wheels/ 154 | *.egg-info/ 155 | .installed.cfg 156 | *.egg 157 | 158 | # PyInstaller 159 | # Usually these files are written by a python script from a template 160 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 161 | *.manifest 162 | *.spec 163 | 164 | # Installer logs 165 | pip-log.txt 166 | pip-delete-this-directory.txt 167 | 168 | # Unit test / coverage reports 169 | htmlcov/ 170 | .tox/ 171 | .coverage 172 | .coverage.* 173 | .cache 174 | .pytest_cache/ 175 | nosetests.xml 176 | coverage.xml 177 | *.cover 178 | .hypothesis/ 179 | 180 | # Translations 181 | *.mo 182 | *.pot 183 | 184 | # Flask stuff: 185 | instance/ 186 | .webassets-cache 187 | 188 | # Scrapy stuff: 189 | .scrapy 190 | 191 | # Sphinx documentation 192 | docs/_build/ 193 | 194 | # PyBuilder 195 | target/ 196 | 197 | # Jupyter Notebook 198 | .ipynb_checkpoints 199 | 200 | # pyenv 201 | .python-version 202 | 203 | # celery beat schedule file 204 | celerybeat-schedule.* 205 | 206 | # SageMath parsed files 207 | *.sage.py 208 | 209 | # Environments 210 | .env 211 | .venv 212 | env/ 213 | venv/ 214 | ENV/ 215 | env.bak/ 216 | venv.bak/ 217 | 218 | # Spyder project settings 219 | .spyderproject 220 | .spyproject 221 | 222 | # Rope project settings 223 | .ropeproject 224 | 225 | # mkdocs documentation 226 | /site 227 | 228 | # mypy 229 | .mypy_cache/ 230 | 231 | 232 | # End of https://www.gitignore.io/api/python,eclipse,phpstorm 233 | -------------------------------------------------------------------------------- /CONTRIBUTING.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: shell 2 | 3 | ============ 4 | Contributing 5 | ============ 6 | 7 | Contributions are welcome, and they are greatly appreciated! Every little bit 8 | helps, and credit will always be given. 9 | 10 | You can contribute in many ways: 11 | 12 | Types of Contributions 13 | ---------------------- 14 | 15 | Report Bugs 16 | ~~~~~~~~~~~ 17 | 18 | Report bugs at https://github.com/svilborg/samsungtv/issues. 19 | 20 | If you are reporting a bug, please include: 21 | 22 | * Your operating system name and version. 23 | * Any details about your local setup that might be helpful in troubleshooting. 24 | * Detailed steps to reproduce the bug. 25 | 26 | Fix Bugs 27 | ~~~~~~~~ 28 | 29 | Look through the GitHub issues for bugs. Anything tagged with "bug" and "help 30 | wanted" is open to whoever wants to implement it. 31 | 32 | Implement Features 33 | ~~~~~~~~~~~~~~~~~~ 34 | 35 | Look through the GitHub issues for features. Anything tagged with "enhancement" 36 | and "help wanted" is open to whoever wants to implement it. 37 | 38 | Write Documentation 39 | ~~~~~~~~~~~~~~~~~~~ 40 | 41 | Samsung TV could always use more documentation, whether as part of the 42 | official Samsung TV docs, in docstrings, or even on the web in blog posts, 43 | articles, and such. 44 | 45 | Submit Feedback 46 | ~~~~~~~~~~~~~~~ 47 | 48 | The best way to send feedback is to file an issue at https://github.com/svilborg/samsungtv/issues. 49 | 50 | If you are proposing a feature: 51 | 52 | * Explain in detail how it would work. 53 | * Keep the scope as narrow as possible, to make it easier to implement. 54 | * Remember that this is a volunteer-driven project, and that contributions 55 | are welcome :) 56 | 57 | Get Started! 58 | ------------ 59 | 60 | Ready to contribute? Here's how to set up `samsungtv` for local development. 61 | 62 | 1. Fork the `samsungtv` repo on GitHub. 63 | 2. Clone your fork locally:: 64 | 65 | $ git clone git@github.com:your_name_here/samsungtv.git 66 | 67 | 3. Install your local copy into a virtualenv. Assuming you have virtualenvwrapper installed, this is how you set up your fork for local development:: 68 | 69 | $ mkvirtualenv samsungtv 70 | $ cd samsungtv/ 71 | $ python setup.py develop 72 | 73 | 4. Create a branch for local development:: 74 | 75 | $ git checkout -b name-of-your-bugfix-or-feature 76 | 77 | Now you can make your changes locally. 78 | 79 | 5. When you're done making changes, check that your changes pass flake8 and the 80 | tests, including testing other Python versions with tox:: 81 | 82 | $ flake8 samsungtv tests 83 | $ python setup.py test or py.test 84 | $ tox 85 | 86 | To get flake8 and tox, just pip install them into your virtualenv. 87 | 88 | 6. Commit your changes and push your branch to GitHub:: 89 | 90 | $ git add . 91 | $ git commit -m "Your detailed description of your changes." 92 | $ git push origin name-of-your-bugfix-or-feature 93 | 94 | 7. Submit a pull request through the GitHub website. 95 | 96 | Pull Request Guidelines 97 | ----------------------- 98 | 99 | Before you submit a pull request, check that it meets these guidelines: 100 | 101 | 1. The pull request should include tests. 102 | 2. If the pull request adds functionality, the docs should be updated. Put 103 | your new functionality into a function with a docstring, and add the 104 | feature to the list in README.rst. 105 | 3. The pull request should work for Python 2.7, 3.4, 3.5 and 3.6, and for PyPy. Check 106 | https://travis-ci.org/svilborg/samsungtv/pull_requests 107 | and make sure that the tests pass for all supported Python versions. 108 | 109 | Tips 110 | ---- 111 | 112 | To run a subset of tests:: 113 | 114 | 115 | $ python -m unittest tests.test_samsungtv 116 | 117 | Deploying 118 | --------- 119 | 120 | A reminder for the maintainers on how to deploy. 121 | Make sure all your changes are committed (including an entry in HISTORY.rst). 122 | Then run:: 123 | 124 | $ bumpversion patch # possible: major / minor / patch 125 | $ git push 126 | $ git push --tags 127 | 128 | Travis will then deploy to PyPI if tests pass. 129 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Samsung TV 3 | ========== 4 | 5 | Samsung Tv UPnP/DIAL/Remote Automations 6 | 7 | 8 | * Free software: Apache license 2.0 9 | 10 | 11 | Features 12 | -------- 13 | 14 | 15 | Usage: samsungtv-cli [OPTIONS] 16 | 17 | --scan SSPD Scan 18 | --rescan SSPD Scan (refreshed cache) 19 | --volume Set Volume 20 | --volup Incr Volume 21 | --voldown Decr Volume 22 | --mute Mute 23 | --unmute Unmute 24 | --app_on Start App 25 | --app_off Stop App 26 | --file Play Media File 27 | --add_file Add Media File 28 | --play Play Media 29 | --stop Stop Media 30 | --key Send a Key 31 | --keys List Available Keys 32 | --launch Launch App 33 | --help This help menu 34 | --start_httpd Start http server 35 | --stop_httpd Stop http server 36 | -------------------------------------------------------------------------------- /app.py: -------------------------------------------------------------------------------- 1 | import getopt 2 | import sys 3 | 4 | from samsungtv import SamsungTvApp 5 | 6 | 7 | def usage_option(name, description): 8 | return "\t--%s \t %s" % (name, description) 9 | 10 | 11 | def usage(): 12 | print "" 13 | print "Usage: " + sys.argv[0] + " [OPTIONS]" 14 | print usage_option("scan", "SSPD Scan") 15 | print usage_option("rescan", "SSPD Scan (refreshed cache)") 16 | print usage_option("volume", "Set Volume") 17 | print usage_option("volup", "Incr Volume") 18 | print usage_option("voldown", "Decr Volume") 19 | print usage_option("mute", "Mute") 20 | print usage_option("unmute", "Unmute") 21 | print usage_option("app_on", "Start App") 22 | print usage_option("app_off", "Stop App") 23 | print usage_option("file", "Play Media File") 24 | print usage_option("add_file", "Add Media File") 25 | print usage_option("play", "Play Media") 26 | print usage_option("stop", "Stop Media") 27 | print usage_option("key", "Send a Key") 28 | print usage_option("keys", "List Available Keys") 29 | print usage_option("launch", "Launch App") 30 | print usage_option("help", "This help menu") 31 | print usage_option("start_httpd", "Start http server") 32 | print usage_option("stop_httpd", "Stop http server") 33 | print "" 34 | 35 | 36 | if __name__ == "__main__": 37 | 38 | try: 39 | opts, args = getopt.getopt(sys.argv[1:], "s:hv", ["help", "version", 40 | "scan", "rescan", 41 | "volume=", "volup", "voldown", "mute", "unmute", 42 | "file=", "add_file=", 43 | "play", "stop", "next", "prev", 44 | "start_httpd", "stop_httpd", 45 | "app=", "app_on=", "app_off=", "app_install=", 46 | "key=", "keys", "apps", "launch=" 47 | ]) 48 | except getopt.GetoptError, err: 49 | print(err) 50 | sys.exit(-1) 51 | 52 | for o, arg in opts: 53 | if o in ("-h", "--help"): 54 | usage() 55 | sys.exit() 56 | elif o in ("-V", "--version"): 57 | print VERSION 58 | sys.exit(0) 59 | elif o in ("-s", "--scan"): 60 | SamsungTvApp.scan() 61 | elif o in ("--rescan"): 62 | SamsungTvApp.scan(True) 63 | else: 64 | tv = SamsungTvApp() 65 | 66 | method = o.replace("--", "") 67 | print tv.run(method, arg) 68 | -------------------------------------------------------------------------------- /bin/capture.sh: -------------------------------------------------------------------------------- 1 | #! /bin/bash 2 | 3 | pulseaudio-monitor() { 4 | echo $(pactl list | grep -A2 '^Source #' | grep 'Name: .*\.monitor$' | awk '{print $NF}' | tail -n1) 5 | } 6 | 7 | # cvlc screen:// \ 8 | # :sout=#transcode{vcodec=h264,vb=0,scale=0,acodec=mpga,ab=128,channels=2,samplerate=44100}:file{dst=./screen.mp4} :sout-keep 9 | 10 | # cvlc screen:// :screen-fps=25 :screen-caching=100 \ 11 | # --sout '#transcode{vcodec=MJPG,vb=0,width=1022,height=575,acodec=none}:http{mux=ogg,dst=:8554/myscreen}' 12 | 13 | # cvlc screen:// :screen-fps=25 :screen-caching=100 \ 14 | # --sout '#transcode{h264,vb=0,scale=0,acodec=mpga,ab=128,channels=2,samplerate=44100}:http{mux=ogg,dst=:8554/myscreen}' 15 | 16 | desktop-capture-ff() { 17 | 18 | local RESOLUTION="640x360" 19 | local QUAL="fast" 20 | local TUNE="animation" 21 | local OUTPUT="$(dirname "$0")/../media/capture.mov" 22 | local OFFSET="0,0" 23 | local FPS="30" 24 | 25 | if [[ -f "$OUTPUT" ]]; then 26 | rm "$OUTPUT" 27 | fi 28 | 29 | # avconv \ 30 | # -f alsa -i hw:0 \ 31 | # -f x11grab -r "$FPS" \ 32 | # -i :0.0+"$OFFSET" \ 33 | # -s "$RESOLUTION" \ 34 | # -vcodec libx264 -crf 38 \ 35 | # -preset "$QUAL" -tune "$TUNE" \ 36 | # -threads 2 \ 37 | # -an \ 38 | # "$OUTPUT" 39 | 40 | # -profile:v baseline -level 30 \ 41 | # -acodec copy \ 42 | # -acodec libmp3lame -ar 44100 \ 43 | 44 | ffmpeg -y -f alsa \ 45 | -i hw:0 \ 46 | -f x11grab -framerate 30 \ 47 | -i :0.0+0,0 \ 48 | -video_size 1024×576 \ 49 | -acodec ac3 -ac 1 \ 50 | -vcodec libx264 \ 51 | -preset fast \ 52 | -threads 0 \ 53 | -f matroska \ 54 | "$OUTPUT" 55 | 56 | } 57 | 58 | echo $(dirname "$0") 59 | desktop-capture-ff 60 | -------------------------------------------------------------------------------- /media/.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svilborg/samsungtv/007d8cbf4326ae13d74ab138eb77798d11aae108/media/.gitignore -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | setuptools==38.5.1 2 | requests>=2.20.0 3 | websocket_client==0.47.0 -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | tox==2.9.1 2 | Sphinx==1.7.0 3 | coverage==4.5.1 4 | nose==1.3.7 5 | nose-cov==1.6 6 | nose-cover3==0.1.0 7 | mock==2.0.0 -------------------------------------------------------------------------------- /samsungtv/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | """Top-level package for Samsung TV.""" 4 | 5 | __author__ = """S.Simeonov""" 6 | __email__ = 'svilborg@gmail.com' 7 | __version__ = '0.1.0' 8 | 9 | 10 | from app import SamsungTvApp 11 | -------------------------------------------------------------------------------- /samsungtv/__main__.py: -------------------------------------------------------------------------------- 1 | import getopt 2 | import sys 3 | 4 | from samsungtv import SamsungTvApp 5 | 6 | def usage_option(name, description): 7 | return "\t--%s \t %s" % (name, description) 8 | 9 | 10 | def usage(): 11 | print "" 12 | print "Usage: " + sys.argv[0] + " [OPTIONS]" 13 | print usage_option("scan", "SSPD Scan") 14 | print usage_option("rescan", "SSPD Scan (refreshed cache)") 15 | print usage_option("volume", "Set Volume") 16 | print usage_option("volup", "Incr Volume") 17 | print usage_option("voldown", "Decr Volume") 18 | print usage_option("mute", "Mute") 19 | print usage_option("unmute", "Unmute") 20 | print usage_option("app_on", "Start App") 21 | print usage_option("app_off", "Stop App") 22 | print usage_option("file", "Play Media File") 23 | print usage_option("add_file", "Add Media File") 24 | print usage_option("play", "Play Media") 25 | print usage_option("stop", "Stop Media") 26 | print usage_option("key", "Send a Key") 27 | print usage_option("keys", "List Available Keys") 28 | print usage_option("launch", "Launch App") 29 | print usage_option("help", "This help menu") 30 | print usage_option("start_httpd", "Start http server") 31 | print usage_option("stop_httpd", "Stop http server") 32 | print "" 33 | 34 | def main(): 35 | try: 36 | opts, args = getopt.getopt(sys.argv[1:], "s:hv", ["help", "version", 37 | "scan", "rescan", 38 | "volume=", "volup", "voldown", "mute", "unmute", 39 | "file=", "add_file=", 40 | "play", "stop", "next", "prev", 41 | "start_httpd", "stop_httpd", 42 | "app=", "app_on=", "app_off=", "app_install=", 43 | "key=", "keys", "apps", "launch=" 44 | ]) 45 | except getopt.GetoptError, err: 46 | print(err) 47 | sys.exit(-1) 48 | 49 | for o, arg in opts: 50 | if o in ("-h", "--help"): 51 | usage() 52 | sys.exit() 53 | elif o in ("-V", "--version"): 54 | # print VERSION 55 | sys.exit(0) 56 | elif o in ("-s", "--scan"): 57 | SamsungTvApp.scan() 58 | elif o in ("--rescan"): 59 | SamsungTvApp.scan(True) 60 | else: 61 | tv = SamsungTvApp() 62 | 63 | method = o.replace("--", "") 64 | print tv.run(method, arg) 65 | 66 | 67 | if __name__ == "__main__": 68 | main() -------------------------------------------------------------------------------- /samsungtv/app.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | from httpd import HttpProxyServerCtrl 4 | from dlna import DlnaDevices, utils, DlnaDeviceServices 5 | from samsungtv.services.remote_control import RemoteControl 6 | 7 | VERSION = "1.0" 8 | 9 | 10 | class SamsungTVAction(object): 11 | 12 | def __init__(self, label, result=None): 13 | self.label = label 14 | self.result = result 15 | pass 16 | 17 | def __repr__(self): 18 | return self.label 19 | 20 | 21 | class SamsungTVActionList(object): 22 | 23 | def __init__(self, label, result=None, fields=[]): 24 | self.label = label 25 | self.result = result 26 | self.fields = fields 27 | 28 | pass 29 | 30 | def __repr__(self): 31 | 32 | res = "\n" 33 | res += self.label + "\n" 34 | for item in self.result: 35 | 36 | if isinstance(item, dict): 37 | for field in self.fields: 38 | res += item[field] + " " 39 | pass 40 | elif isinstance(item, str): 41 | res += item 42 | else: 43 | raise Exception("Unknown item type {}".format(type(item))) 44 | 45 | res += "\n" 46 | 47 | return res 48 | 49 | 50 | class SamsungTvApp(object): 51 | """docstring for SamsungTvApp""" 52 | 53 | def __init__(self): 54 | 55 | self.host = utils.detect_ip_address() 56 | self.port = 8000 57 | self.name = u'SamsungTvApp' 58 | self.uri = "http://" + self.host + ":" + str(self.port) 59 | self.app_ip = utils.detect_ip_address() 60 | self.volume_step = 2 61 | 62 | devices = DlnaDevices("devices") 63 | 64 | tv_device = devices.get_device_by_type(DlnaDevices.MEDIA_RENDERER) 65 | dial_device = devices.get_device_by_type(DlnaDevices.DIAL_RECEIVER) 66 | 67 | if not tv_device: 68 | print "Unable to find a tv device in local network" 69 | devices.clean() # Cleanup if cache 70 | exit(1) 71 | 72 | if not dial_device: 73 | print "Unable to find a ttv dial device in local network" 74 | devices.clean() # Cleanup if cache 75 | exit(1) 76 | 77 | # c = DlnaDeviceServices.subscribe_to_all(tv_device, self.uri) 78 | # print c 79 | # exit(1) 80 | 81 | self.service = DlnaDeviceServices.get_service(tv_device, DlnaDeviceServices.SERVICE_AV) 82 | self.service_rendering = DlnaDeviceServices.get_service(tv_device, DlnaDeviceServices.SERVICE_RC) 83 | self.service_dial = DlnaDeviceServices.get_service(dial_device, DlnaDeviceServices.SERVICE_DIAL) 84 | self.remote_control = RemoteControl("192.168.0.100", name=self.name) 85 | 86 | self.httpctrl = HttpProxyServerCtrl(port=self.port) 87 | 88 | pass 89 | 90 | @staticmethod 91 | def scan(rescan=False): 92 | 93 | print "Scanning ..." 94 | 95 | devices = DlnaDevices("devices") 96 | 97 | for key, device in devices.get_devices(rescan).items(): 98 | print device 99 | # import pprint 100 | # pprint.pprint( device.info) 101 | pass 102 | 103 | def stop_httpd(self, args=None): 104 | 105 | self.httpctrl.stop() 106 | return "Http Server Stopped" 107 | 108 | def start_httpd(self, args=None): 109 | self.httpctrl.start() 110 | 111 | print "Http Server Started" 112 | 113 | def file(self, arg): 114 | self.start_httpd() 115 | 116 | time.sleep(2) 117 | 118 | url = arg 119 | print url 120 | self.service.url(url) 121 | 122 | def add_file(self, arg): 123 | 124 | return SamsungTVAction( 125 | "Added URL %s" % (arg), 126 | self.service.set_next_url(arg) 127 | ) 128 | 129 | def play(self, arg): 130 | return SamsungTVAction( 131 | "Play %s" % (arg), 132 | self.service.play() 133 | ) 134 | 135 | def stop(self, arg): 136 | return SamsungTVAction( 137 | "Stop %s" % (arg), 138 | self.service.stop() 139 | ) 140 | 141 | def next(self, arg): 142 | return SamsungTVAction( 143 | "Next %s" % (arg), 144 | self.service.next() 145 | ) 146 | 147 | def prev(self, arg): 148 | return SamsungTVAction( 149 | "Stop %s" % (arg), 150 | self.service.previous() 151 | ) 152 | 153 | def volume(self, arg): 154 | return SamsungTVAction( 155 | "Set Volime to %s" % (arg), 156 | self.service_rendering.volume(arg) 157 | ) 158 | 159 | def volup(self, arg): 160 | vol = str(self.service_rendering.volume() + self.volume_step) 161 | 162 | return SamsungTVAction( 163 | "Incr Volime to %s" % (vol), 164 | self.service_rendering.volume(vol) 165 | ) 166 | 167 | def voldown(self, arg): 168 | vol = str(self.service_rendering.volume() - self.volume_step) 169 | 170 | return SamsungTVAction( 171 | "Decr Volime to %s" % (vol), 172 | self.service_rendering.volume(vol) 173 | ) 174 | 175 | def mute(self, arg): 176 | return SamsungTVAction( 177 | "Set Mute On", 178 | self.service_rendering.mute(True) 179 | ) 180 | 181 | def unmute(self, arg): 182 | return SamsungTVAction( 183 | "Set Mute Off", 184 | self.service_rendering.mute(False) 185 | ) 186 | 187 | def app_on(self, arg): 188 | return SamsungTVAction( 189 | "Start App %s " % (arg), 190 | self.service_dial.start(arg) 191 | ) 192 | 193 | def app_off(self, arg): 194 | return SamsungTVAction( 195 | "Stop App %s " % (arg), 196 | self.service_dial.stop(arg) 197 | ) 198 | 199 | def app(self, arg): 200 | print "App %s " % (arg) 201 | print self.service_dial.get(arg) 202 | return "" 203 | 204 | def app_install(self, arg): 205 | return SamsungTVAction( 206 | "Install App %s " % (arg), 207 | self.service_dial.install(arg) 208 | ) 209 | 210 | def keys(self, arg): 211 | 212 | return SamsungTVActionList( 213 | "Keys \n", 214 | RemoteControl.KEY_CODES 215 | ) 216 | 217 | def key(self, arg): 218 | """ 219 | 220 | :type arg: str 221 | """ 222 | 223 | if arg.find(",") > 0: 224 | key_codes = arg.split(",") 225 | 226 | self.remote_control.connect() 227 | for key_code in key_codes: 228 | # self.key(key_code) 229 | self.remote_control.command(key_code) 230 | self.remote_control.close() 231 | 232 | return SamsungTVAction( 233 | "Keys Pressed \n " + 234 | "\n ".join(key_codes) 235 | ) 236 | else: 237 | self.remote_control.connect() 238 | result = self.remote_control.command(arg) 239 | self.remote_control.close() 240 | 241 | return SamsungTVAction( 242 | "Key Pressed %s" % arg, 243 | result 244 | ) 245 | 246 | def launch(self, arg): 247 | 248 | self.remote_control.connect() 249 | result = self.remote_control.launch(arg) 250 | self.remote_control.close() 251 | 252 | return SamsungTVAction( 253 | "App Launched %s" % arg, 254 | result 255 | ) 256 | 257 | def apps(self, arg=None): 258 | 259 | self.remote_control.connect() 260 | app_list = self.remote_control.apps() 261 | self.remote_control.close() 262 | 263 | return SamsungTVActionList("Apps \n", app_list, ["appId", "name"]) 264 | 265 | def run(self, method, arg): 266 | return self.__getattribute__(method)(arg) 267 | -------------------------------------------------------------------------------- /samsungtv/dlna/__init__.py: -------------------------------------------------------------------------------- 1 | """DLNA Services""" 2 | 3 | from .device import DlnaDevice 4 | from .device_services import DlnaDeviceServices 5 | from .devices import DlnaDevices 6 | from .utils import Cache 7 | 8 | __all__ = [ 9 | 'DlnaDevice', 10 | 'DlnaDevices', 11 | 'DlnaDeviceServices', 12 | 'Cache' 13 | ] 14 | -------------------------------------------------------------------------------- /samsungtv/dlna/device.py: -------------------------------------------------------------------------------- 1 | import xml.etree.cElementTree as XML 2 | import requests 3 | import re 4 | import urlparse 5 | 6 | # from dlna import utils 7 | from samsungtv.dlna import utils 8 | from utils import etree_to_dict 9 | 10 | 11 | class DlnaDevice(object): 12 | 13 | def __init__(self, location=""): 14 | parsed = urlparse.urlparse(location) 15 | 16 | self.ip = parsed.hostname 17 | self.port = parsed.port 18 | self.path = parsed.path 19 | self.location = location 20 | self.name = "N/A" 21 | self.applicationUrl = None 22 | 23 | data = self.__get_data(location) 24 | 25 | self.info = data['root']['device'] 26 | self.name = self.info['friendlyName'] 27 | self.applicationUrl = data['headers'].get('application-url') 28 | 29 | self.services = {} 30 | 31 | if type(data['root']['device']['serviceList']['service']) is list: 32 | for service in data['root']['device']['serviceList']['service']: 33 | self.services[service["serviceType"]] = service 34 | else: 35 | service = data['root']['device']['serviceList']['service'] 36 | self.services[service["serviceType"]] = service 37 | 38 | del self.info['serviceList'] 39 | del data 40 | 41 | self.info = utils.clean_ns_from_list(self.info) 42 | 43 | def __get_data(self, location): 44 | r = requests.get(location) 45 | 46 | xml_string = r.content 47 | xml_string = re.sub(' xmlns="[^"]+"', '', xml_string, count=1) 48 | 49 | xml = XML.fromstring(xml_string) 50 | data = etree_to_dict(xml) 51 | data['headers'] = r.headers 52 | 53 | return data 54 | 55 | 56 | def __repr__(self): 57 | res = "" 58 | res += self.name + " [ " + str(self.info["modelName"]) + ", " \ 59 | + str(self.info["modelDescription"]) + " ] @ " + self.location + "\n" 60 | 61 | return res 62 | -------------------------------------------------------------------------------- /samsungtv/dlna/device_services.py: -------------------------------------------------------------------------------- 1 | from samsungtv.services.dial import DialService 2 | from samsungtv.upnpevents import EventSubscriber 3 | from samsungtv.upnpservice import UPnPServiceAVTransport, UPnPServiceRendering, UPnPServiceConnectionManager 4 | 5 | 6 | class DlnaDeviceServices: 7 | SERVICE_AV = "urn:schemas-upnp-org:service:AVTransport:1" 8 | SERVICE_RC = "urn:schemas-upnp-org:service:RenderingControl:1" 9 | SERVICE_CM = "urn:schemas-upnp-org:service:ConnectionManager:1" 10 | SERVICE_DIAL = "dial" 11 | 12 | @staticmethod 13 | def get_service(device, type): 14 | 15 | if device.services.get(type) is None and type is not DlnaDeviceServices.SERVICE_DIAL: 16 | raise Exception("Unsupported service {}".format(type)) 17 | 18 | if type == DlnaDeviceServices.SERVICE_AV: 19 | 20 | return UPnPServiceAVTransport(device.ip, device.port, config=device.services[type]) 21 | 22 | elif type == DlnaDeviceServices.SERVICE_RC: 23 | 24 | return UPnPServiceRendering(device.ip, device.port, config=device.services[type]) 25 | elif type == DlnaDeviceServices.SERVICE_CM: 26 | 27 | return UPnPServiceConnectionManager(device.ip, device.port, config=device.services[type]) 28 | elif type == DlnaDeviceServices.SERVICE_DIAL: 29 | 30 | if device.applicationUrl is not None and device.applicationUrl != "": 31 | return DialService(device.applicationUrl) 32 | else: 33 | raise Exception("Device does not support service ()".format(type)) 34 | 35 | else: 36 | raise Exception("Unsupported service ()".format(type)) 37 | 38 | @staticmethod 39 | def get_event_subscriber(device, type, callback=""): 40 | 41 | if device.services.get(type): 42 | url = "http://{}:{}{}".format(device.ip, device.port, device.services[type]['eventSubURL']) 43 | 44 | print "========" 45 | print device.services[type] 46 | print url 47 | 48 | return EventSubscriber(url, callback) 49 | 50 | else: 51 | raise Exception("Unsupported event service ()".format(type)) 52 | 53 | pass 54 | 55 | @staticmethod 56 | def get_event_subscribers(device, callback=""): 57 | 58 | subscribers = {} 59 | 60 | print device.info 61 | 62 | for stype, service in device.services.items(): 63 | if service.get('eventSubURL'): 64 | subscribers[stype] = DlnaDeviceServices.get_event_subscriber(device, stype, callback) 65 | 66 | return subscribers 67 | 68 | @staticmethod 69 | def subscribe(device, type, callback=""): 70 | subscriber = DlnaDeviceServices.get_event_subscriber(device, type, callback) 71 | subscriber.subscribe() 72 | 73 | @staticmethod 74 | def subscribe_to_all(device, callback=""): 75 | 76 | for stype, subscriber in DlnaDeviceServices.get_event_subscribers(device, callback).items(): 77 | print subscriber 78 | subscriber.subscribe() 79 | -------------------------------------------------------------------------------- /samsungtv/dlna/devices.py: -------------------------------------------------------------------------------- 1 | from utils import Cache 2 | from samsungtv.services.ssdp import SSDPDiscovery 3 | from device import DlnaDevice 4 | 5 | 6 | class DlnaDevices(object): 7 | 8 | MEDIA_RENDERER = "urn:schemas-upnp-org:device:MediaRenderer:1" 9 | DIAL_RECEIVER = "urn:dial-multiscreen-org:device:dialreceiver:1" 10 | 11 | def __init__(self, cache=None): 12 | self.cache = cache 13 | 14 | pass 15 | 16 | def _get_devices(self): 17 | discovery = SSDPDiscovery() 18 | result = discovery.discover(SSDPDiscovery.ST_ALL) 19 | devices = {} 20 | 21 | for headers in result: 22 | devices[headers['location']] = DlnaDevice(headers['location']) 23 | 24 | return devices 25 | 26 | def get_devices(self, refresh=False): 27 | 28 | if self.cache: 29 | if refresh is True or not Cache.get(self.cache): 30 | result = self._get_devices() 31 | 32 | Cache.set(self.cache, result) 33 | else: 34 | result = Cache.get(self.cache) 35 | else: 36 | result = self._get_devices() 37 | 38 | return result 39 | 40 | def get_device_by_type(self, dtype=""): 41 | # type: (dev) -> DlnaDevice 42 | """ 43 | 44 | :param dtype: 45 | :return DlnaDevice: Device obj 46 | """ 47 | for key, dev in self.get_devices().items(): 48 | if dev.info['deviceType'] == dtype: 49 | return dev 50 | return None 51 | 52 | def clean(self): 53 | if self.cache: 54 | Cache.clear(self.cache) 55 | 56 | -------------------------------------------------------------------------------- /samsungtv/dlna/utils.py: -------------------------------------------------------------------------------- 1 | import os 2 | import pickle 3 | import re 4 | import socket 5 | from collections import defaultdict 6 | 7 | 8 | class Cache(object): 9 | path = './cache' 10 | 11 | @staticmethod 12 | def set(name, value): 13 | file = '%s/%s.pickle' % (Cache.path, name) 14 | 15 | with open(file, 'wb') as handle: 16 | pickle.dump(value, handle, protocol=pickle.HIGHEST_PROTOCOL) 17 | 18 | pass 19 | 20 | @staticmethod 21 | def get(name): 22 | value = None 23 | file = '%s/%s.pickle' % (Cache.path, name) 24 | 25 | if os.path.isfile(file): 26 | with open(file, 'rb') as handle: 27 | value = pickle.load(handle) 28 | 29 | return value 30 | 31 | @staticmethod 32 | def clear(name): 33 | file = '%s/%s.pickle' % (Cache.path, name) 34 | 35 | if os.path.isfile(file): 36 | os.remove(file) 37 | 38 | 39 | def detect_ip_address(): 40 | """Return the local ip-address""" 41 | # https://stackoverflow.com/a/166589 42 | 43 | s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) 44 | s.connect(("8.8.8.8", 80)) 45 | ip_address = s.getsockname()[0] 46 | s.close() 47 | 48 | return ip_address 49 | 50 | 51 | def clean_ns_from_list(data=None): 52 | if data is None: 53 | data = {} 54 | 55 | result = {} 56 | if data: 57 | for key, el in data.items(): 58 | key = re.sub('{[^{}]+}', '', key, count=1) 59 | result[key] = el 60 | 61 | return result 62 | 63 | 64 | def etree_to_dict(t): 65 | d = {t.tag: {} if t.attrib else None} 66 | children = list(t) 67 | if children: 68 | dd = defaultdict(list) 69 | for dc in map(etree_to_dict, children): 70 | for k, v in dc.items(): 71 | dd[k].append(v) 72 | d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}} 73 | if t.attrib: 74 | d[t.tag].update(('@' + k, v) for k, v in t.attrib.items()) 75 | if t.text: 76 | text = t.text.strip() 77 | if children or t.attrib: 78 | if text: 79 | d[t.tag]['#text'] = text 80 | else: 81 | d[t.tag] = text 82 | return d 83 | -------------------------------------------------------------------------------- /samsungtv/httpd/__init__.py: -------------------------------------------------------------------------------- 1 | """UPnP Services""" 2 | from serverctrl import HttpProxyServerCtrl 3 | 4 | __all__ = [ 5 | 'HttpProxyServerCtrl' 6 | ] 7 | -------------------------------------------------------------------------------- /samsungtv/httpd/composite_handler.py: -------------------------------------------------------------------------------- 1 | from BaseHTTPServer import HTTPServer 2 | 3 | import sys 4 | import os 5 | 6 | from proxy_handler import ProxyHttpRequestHandler 7 | from subscribe_handler import SubscribeHttpRequestHandler 8 | 9 | 10 | class CompositeHttpRequestHandler(ProxyHttpRequestHandler, SubscribeHttpRequestHandler): 11 | pass 12 | 13 | 14 | if __name__ == "__main__": 15 | 16 | host = "" 17 | port = 8008 18 | 19 | try: 20 | CompositeHttpRequestHandler.dir_path = os.path.dirname(os.path.realpath(__file__)) + "/../media" 21 | httpd = HTTPServer((host, port), CompositeHttpRequestHandler) 22 | 23 | except Exception as e: 24 | sys.stderr.write(str(e)) 25 | sys.exit(-1) 26 | 27 | print "Serving on " + host + ":" + str(port) + " ... " 28 | 29 | while True: 30 | try: 31 | httpd.handle_request() 32 | except KeyboardInterrupt as e1: 33 | print "Bye" 34 | sys.exit(0) 35 | -------------------------------------------------------------------------------- /samsungtv/httpd/proxy_handler.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | import os 3 | import shutil 4 | from BaseHTTPServer import BaseHTTPRequestHandler 5 | from urllib2 import urlopen 6 | 7 | 8 | class ProxyHttpRequestHandler(BaseHTTPRequestHandler): 9 | 10 | dir_path = os.path.dirname(os.path.realpath(__file__)) + "/" 11 | 12 | def _send_ok(self): 13 | url = self.path[1:] # replace '/' 14 | 15 | file = self.dir_path + "/" + url 16 | 17 | if os.path.exists(file) and os.path.isfile(file): 18 | f = open(file) 19 | content_type = mimetypes.guess_type(file)[0] 20 | 21 | f.close() 22 | elif url.startswith("http"): 23 | f = urlopen(url=url) 24 | content_type = f.info().getheaders("Content-Type")[0] 25 | 26 | f.close() 27 | else: 28 | self.send_response(404, "Not Found") 29 | self.send_header("X-App-Url", url) 30 | self.send_header("X-App-File", file) 31 | self.end_headers() 32 | return None 33 | 34 | self.send_response(200, "ok") 35 | self.send_header('Access-Control-Allow-Origin', '*') 36 | self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') 37 | self.send_header("Access-Control-Allow-Headers", "X-Requested-With") 38 | self.send_header("Access-Control-Allow-Headers", "Content-Type") 39 | self.send_header("Content-Type", content_type) 40 | self.send_header("X-App-Url", url) 41 | self.send_header("X-App-File", file) 42 | self.end_headers() 43 | 44 | def do_OPTIONS(self): 45 | self._send_ok() 46 | 47 | def do_HEAD(self): 48 | self._send_ok() 49 | 50 | def do_GET(self): 51 | url = self.path[1:] # replace '/' 52 | 53 | file = self.dir_path + "/" + url 54 | 55 | if os.path.exists(file) and os.path.isfile(file): 56 | f = open(file) 57 | content_type = mimetypes.guess_type(file)[0] 58 | size = os.path.getsize(file) 59 | name = os.path.basename(file) 60 | elif url.startswith("http"): 61 | f = urlopen(url=url) 62 | content_type = f.info().getheaders("Content-Type")[0] 63 | size = f.info().getheaders("Content-Length")[0] 64 | name = os.path.basename(url) 65 | else : 66 | self.send_response(404, "Not Found") 67 | self.end_headers() 68 | return None 69 | 70 | try: 71 | self.send_response(200) 72 | self.send_header('Access-Control-Allow-Origin', '*') 73 | self.send_header("Content-Type", content_type) 74 | self.send_header("Content-Disposition", 'attachment; filename="{}"'.format(name)) 75 | self.send_header("Content-Length", str(size)) 76 | self.end_headers() 77 | 78 | shutil.copyfileobj(f, self.wfile) 79 | finally: 80 | f.close() -------------------------------------------------------------------------------- /samsungtv/httpd/server.py: -------------------------------------------------------------------------------- 1 | import getopt 2 | import os 3 | import sys 4 | from BaseHTTPServer import HTTPServer 5 | 6 | from composite_handler import CompositeHttpRequestHandler 7 | 8 | def usage(): 9 | print "" 10 | print "Usage: " + sys.argv[0] + " [OPTIONS]" 11 | print " -h HOST" 12 | print " -p Port" 13 | print "" 14 | 15 | 16 | if __name__ == "__main__": 17 | 18 | host = "" 19 | port = 8000 20 | 21 | try: 22 | opts, args = getopt.getopt(sys.argv[1:], "h:p:", ["host", "port"]) 23 | except getopt.GetoptError, err: 24 | print(err) 25 | sys.exit(-1) 26 | 27 | for o, arg in opts: 28 | if o in ("-h", "--host"): 29 | host = arg 30 | elif o in ("-p", "--port"): 31 | port = int(arg) 32 | 33 | else: 34 | print "Unknown Options" 35 | 36 | try: 37 | CompositeHttpRequestHandler.dir_path = os.curdir + "/media" 38 | # CompositeHttpRequestHandler.dir_path = os.path.dirname(os.path.realpath(__file__)) + "/../media" 39 | httpd = HTTPServer((host, port), CompositeHttpRequestHandler) 40 | 41 | except Exception as e: 42 | sys.stderr.write(str(e)) 43 | sys.exit(-1) 44 | 45 | print "Serving on " + host + ":" + str(port) + " ... " 46 | 47 | while True: 48 | httpd.handle_request() 49 | -------------------------------------------------------------------------------- /samsungtv/httpd/serverctrl.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | 4 | logger = logging.getLogger(__name__) 5 | 6 | 7 | class HttpProxyServerCtrl(object): 8 | 9 | def __init__(self, host="", port=8000, pid_file=""): 10 | """ 11 | Httpd Control 12 | :type port: int 13 | :type host: str 14 | """ 15 | 16 | dir_path = os.path.dirname(os.path.realpath(__file__)) 17 | 18 | self.host = host 19 | self.port = port 20 | self.pid_file = pid_file if pid_file is not "" else "./stvpid" 21 | self.server = "{0}/server.py".format(dir_path) 22 | 23 | logger.info('pid {}'.format(self.pid_file)) 24 | logger.info('server {}'.format(self.server)) 25 | 26 | def stop(self): 27 | if os.path.isfile(self.pid_file): 28 | cmd = "cat {} | xargs kill && rm {}".format(self.pid_file, self.pid_file) 29 | os.system(cmd) 30 | 31 | pass 32 | 33 | def start(self): 34 | self.stop() 35 | 36 | server_cmd = self.server 37 | 38 | if self.host is not "" : 39 | server_cmd += " -h " + self.host 40 | 41 | if self.port : 42 | server_cmd += " -p " + str(self.port) 43 | 44 | cmd = "nohup python {} & echo $! > {}".format(server_cmd, self.pid_file) 45 | 46 | os.system(cmd) 47 | 48 | logger.debug('cmd {}'.format(cmd)) 49 | logger.info("Http Server Started @ {0}:{1}".format(self.host, str(self.port))) 50 | 51 | def get_pid(self): 52 | pid = "" 53 | 54 | if os.path.isfile(self.pid_file): 55 | with open(self.pid_file, 'r') as handle: 56 | pid = handle.read(1024) 57 | 58 | handle.close() 59 | 60 | return pid 61 | 62 | 63 | if __name__ == "__main__": 64 | 65 | logging.basicConfig(level=logging.DEBUG) 66 | 67 | httpdctrl = HttpProxyServerCtrl() 68 | 69 | httpdctrl.get_pid() 70 | httpdctrl.start() 71 | print httpdctrl.get_pid() 72 | httpdctrl.stop() 73 | -------------------------------------------------------------------------------- /samsungtv/httpd/subscribe_handler.py: -------------------------------------------------------------------------------- 1 | import re 2 | import sys 3 | from BaseHTTPServer import BaseHTTPRequestHandler, HTTPServer 4 | from xml.etree import cElementTree 5 | 6 | from collections import defaultdict 7 | 8 | 9 | def etree_to_dict(t): 10 | d = {t.tag: {} if t.attrib else None} 11 | children = list(t) 12 | if children: 13 | dd = defaultdict(list) 14 | for dc in map(etree_to_dict, children): 15 | for k, v in dc.items(): 16 | dd[k].append(v) 17 | d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}} 18 | if t.attrib: 19 | d[t.tag].update(('@' + k, v) for k, v in t.attrib.items()) 20 | if t.text: 21 | text = t.text.strip() 22 | if children or t.attrib: 23 | if text: 24 | d[t.tag]['#text'] = text 25 | else: 26 | d[t.tag] = text 27 | return d 28 | 29 | 30 | class SubscribeHttpRequestHandler(BaseHTTPRequestHandler): 31 | NS = "{urn:schemas-upnp-org:event-1-0}" 32 | 33 | def do_NOTIFY(self): 34 | 35 | result = {} 36 | ip, _ = self.client_address 37 | 38 | content_len = int(self.headers.get('content-length', 0)) 39 | data = self.rfile.read(content_len) 40 | 41 | # print "RAWWWWWWWWWWWWWWWWWWWWWWWWWWW" 42 | # print data 43 | 44 | properties = {} 45 | event = {} 46 | if data: 47 | doc = cElementTree.fromstring(data) 48 | 49 | for propnode in doc.findall('./{0}property'.format(self.NS)): 50 | for prop in propnode.getchildren(): 51 | # "Raw" Properties 52 | properties[prop.tag] = prop.text 53 | 54 | if prop.text is not None and prop.text.startswith("<"): 55 | # // Extract Event 56 | xml_string = re.sub(' xmlns="[^"]+"', '', prop.text, count=1) 57 | pxml = cElementTree.fromstring(xml_string) 58 | # pp = pxml.findall('./InstanceID/*') 59 | # event = etree_to_dict(pp) 60 | 61 | event = {} 62 | for node in pxml.findall('./InstanceID/*'): 63 | event[node.tag] = node.attrib 64 | 65 | # print "--------------------------------------" 66 | # print cElementTree.tostring(pxml) 67 | # print cElementTree.tostring(pp) 68 | # print etree_to_dict(pxml) 69 | # print "--------------------------------------" 70 | 71 | result["ip"] = ip 72 | result["sid"] = self.headers.getheader("sid") 73 | result["nt"] = self.headers.getheader("nt") 74 | result["nts"] = self.headers.getheader("nts") 75 | result["event"] = event 76 | 77 | # result["raw"] = { 78 | # result["properties"] = properties 79 | # "data": data, 80 | # "headers": self.headers.items() 81 | # } 82 | 83 | self._log(result) 84 | 85 | response = "

200 OK

" 86 | 87 | self.send_response(200) 88 | self.send_header('Content-Type', 'text/html') 89 | self.send_header('Content-Length', len(response)) 90 | self.send_header('Connection', 'close') 91 | self.end_headers() 92 | 93 | self.wfile.write(response.encode("UTF-8")) 94 | 95 | def do_OPTIONS(self): 96 | print "OPTIONS" 97 | self._success() 98 | 99 | def do_HEAD(self): 100 | print "HEAD" 101 | self._success() 102 | 103 | def do_GET(self): 104 | print "GET" 105 | self._success() 106 | 107 | def do_DELETE(self): 108 | print " DELETE" 109 | self._success() 110 | 111 | def do_PUT(self): 112 | print "PUT" 113 | self._success() 114 | 115 | def do_POST(self): 116 | print "POST" 117 | self._success() 118 | 119 | def _success(self): 120 | 121 | self.send_response(200, "ok") 122 | self.send_header('Access-Control-Allow-Origin', '*') 123 | self.send_header('Access-Control-Allow-Methods', 'GET, OPTIONS') 124 | self.send_header("Access-Control-Allow-Headers", "*") 125 | self.end_headers() 126 | 127 | @staticmethod 128 | def _log(result): 129 | import pprint 130 | 131 | print " # ===================================" 132 | pprint.pprint(result) 133 | print "" 134 | 135 | -------------------------------------------------------------------------------- /samsungtv/services/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svilborg/samsungtv/007d8cbf4326ae13d74ab138eb77798d11aae108/samsungtv/services/__init__.py -------------------------------------------------------------------------------- /samsungtv/services/dial.py: -------------------------------------------------------------------------------- 1 | import re 2 | import requests 3 | from xml.etree import cElementTree as XML 4 | 5 | 6 | class DialService(object): 7 | 8 | def __init__(self, url): 9 | self.url = url 10 | pass 11 | 12 | def start(self, name): 13 | r = requests.post(self.url + name) 14 | 15 | return r.content 16 | 17 | def get(self, name): 18 | r = requests.get(self.url + name) 19 | 20 | if r.status_code == 200: 21 | xmlstring = r.content 22 | 23 | try: 24 | xml = XML.fromstring(xmlstring) 25 | except: 26 | raise Exception("XML Parsing Error") 27 | 28 | ns = '{urn:dial-multiscreen-org:schemas:dial}' 29 | 30 | name = xml.findtext('.//' + ns + 'name') 31 | state = xml.findtext('.//' + ns + 'state') 32 | version = xml.findtext('.//' + ns + 'version') 33 | options = xml.find('.//' + ns + 'options') 34 | additional_data = xml.find('.//' + ns + 'additionalData') 35 | atom = xml.find('.//{http://www.w3.org/2005/Atom}link') 36 | install_url = None 37 | 38 | data = {} 39 | 40 | m = re.search('installable=(.*)', state) 41 | if m: 42 | state = 'installable' 43 | install_url = m.group(1) 44 | 45 | data['name'] = name 46 | data['state'] = state 47 | data['install_url'] = install_url 48 | data['version'] = version 49 | data['options'] = options.attrib if options is not None else None 50 | data['links'] = atom.attrib if atom is not None else None 51 | data['additional_data'] = {} 52 | 53 | if additional_data: 54 | for el in additional_data: 55 | tag = re.sub('{[^{}]+}', '', el.tag, count=1) 56 | data['additional_data'][tag] = el.text 57 | 58 | return data 59 | 60 | else : 61 | raise Exception("App Error - {}".format(r.status_code)) 62 | 63 | 64 | def install(self, name): 65 | app = self.get(name) 66 | 67 | if app['state'] == "installable" and app['install_url']: 68 | requests.get(app['install_url']) 69 | 70 | def stop(self, name): 71 | app = self.get(name) 72 | 73 | if app and app['state'] == "running": 74 | if app['links'] is not None: 75 | requests.delete(self.url + name + "/" + app['links']['rel']) 76 | 77 | -------------------------------------------------------------------------------- /samsungtv/services/remote_control.py: -------------------------------------------------------------------------------- 1 | import json 2 | import time 3 | import base64 4 | import websocket 5 | 6 | 7 | class RemoteControl(): 8 | URL = "ws://{}:{}/api/v2/channels/samsung.remote.control?name={}" 9 | 10 | KEY_CODES = [ 11 | "KEY_POWEROFF", "KEY_POWER", 12 | "KEY_UP", "KEY_DOWN", "KEY_LEFT", "KEY_RIGHT", "KEY_CHUP", "KEY_CHDOWN", "KEY_ENTER", 13 | "KEY_RETURN", "KEY_EXIT", 14 | "KEY_INFO", "KEY_CONTENTS", "KEY_CH_LIST", "KEY_MENU", "KEY_SOURCE", "KEY_GUIDE", "KEY_TOOLS", 15 | "KEY_RED", "KEY_GREEN", "KEY_YELLOW", "KEY_BLUE", 16 | "KEY_PANNEL_CHDOWN", 17 | "KEY_VOLUP", "KEY_VOLDOWN", "KEY_MUTE", 18 | "KEY_DTV", "KEY_HDMI", 19 | "KEY_0", "KEY_1", "KEY_2", "KEY_3", "KEY_4", 20 | "KEY_5", "KEY_6", "KEY_7", "KEY_8", "KEY_9", 21 | "KEY_PIP_ONOFF", "KEY_EXTRA " 22 | ] 23 | 24 | def __init__(self, host, port=8001, name=u'RemoteControl', timeout=20, key_delay=1): 25 | 26 | self.id = None 27 | 28 | self.name = name 29 | self.host = host 30 | self.port = port 31 | self.timeout = timeout 32 | self.connection = None 33 | self.key_delay = key_delay 34 | 35 | def connect(self): 36 | 37 | url = RemoteControl.URL.format(self.host, self.port, self._encode(self.name)) 38 | 39 | self.connection = websocket.create_connection(url, self.timeout) 40 | 41 | response = self._receive() 42 | 43 | if response.get("data") is None or response.get("event") != "ms.channel.connect": 44 | self.close() 45 | 46 | self.id = response["data"]["id"] 47 | 48 | return self.id 49 | 50 | def close(self): 51 | if self.is_connected(): 52 | self.connection.close() 53 | 54 | def is_connected(self): 55 | 56 | if self.connection and self.connection.connected is True: 57 | return True 58 | 59 | return False 60 | 61 | def test(self, d): 62 | 63 | msg = { 64 | "method": "ms.channel.emit", 65 | "params": { 66 | "event": "ed.apps.search", 67 | # "event": "seek", 68 | "data": 100, 69 | "to": "host" 70 | } 71 | } 72 | 73 | response = self._send_and_receive(msg) 74 | return response 75 | 76 | def apps(self): 77 | 78 | msg = { 79 | "method": "ms.channel.emit", 80 | "params": { 81 | "event": "ed.installedApp.get", 82 | "to": "host" 83 | }} 84 | 85 | response = self._send_and_receive(msg) 86 | 87 | return response['data']['data'] 88 | 89 | def launch(self, app_id=""): 90 | 91 | msg = { 92 | "method": "ms.channel.emit", 93 | "params": { 94 | "event": "ed.apps.launch", 95 | "to": "host", 96 | "data": { 97 | "appId": app_id, 98 | "action_type": "NATIVE_LAUNCH" 99 | } 100 | }} 101 | 102 | response = self._send_and_receive(msg) 103 | time.sleep(self.key_delay) 104 | 105 | return response 106 | 107 | def command(self, key_code): 108 | 109 | msg = { 110 | "method": "ms.remote.control", 111 | "params": { 112 | "Cmd": "Click", 113 | "DataOfCmd": key_code, 114 | "Option": "false", 115 | "TypeOfRemote": "SendRemoteKey" 116 | } 117 | } 118 | 119 | result = self._send(msg) 120 | 121 | time.sleep(self.key_delay) 122 | 123 | return result 124 | 125 | def _send(self, msg): 126 | if not self.is_connected(): 127 | raise Exception("No Websocket Connection") 128 | 129 | data = json.dumps(msg) 130 | 131 | return self.connection.send(data) 132 | 133 | def _receive(self): 134 | response = self.connection.recv() 135 | 136 | response = json.loads(response) 137 | return response 138 | 139 | def _send_and_receive(self, msg): 140 | 141 | self._send(msg) 142 | 143 | response = self._receive() 144 | 145 | return response 146 | 147 | def _encode(self, input): 148 | if type(input) != unicode: 149 | input = input.decode('utf-8') 150 | 151 | return base64.b64encode(input).decode("utf-8") 152 | -------------------------------------------------------------------------------- /samsungtv/services/ssdp.py: -------------------------------------------------------------------------------- 1 | import socket 2 | 3 | 4 | class SSDPDiscovery(object): 5 | ip = "239.255.255.250" 6 | port = 1900 7 | 8 | ST_ALL = "ssdp:all" 9 | ST_ROOT = "upnp:rootdevice" 10 | 11 | def discover(self, service, timeout=10.0, retries=3, mx=3): 12 | 13 | message = "\r\n".join([ 14 | 'M-SEARCH * HTTP/1.1', 15 | 'HOST: {0.ip}:{0.port}', 16 | 'Accept: */*', 17 | 'MAN: "ssdp:discover"', 18 | 'ST: {st}', 19 | 'MX: {mx}', '', '']) 20 | 21 | message = message.format(self, st=service, mx=mx) 22 | 23 | socket.setdefaulttimeout(timeout) 24 | responses = {} 25 | 26 | for _ in range(retries): 27 | 28 | sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, socket.IPPROTO_UDP) 29 | 30 | sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) 31 | sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 2) 32 | sock.sendto(message, (self.ip, self.port)) 33 | 34 | while True: 35 | try: 36 | response = self._get_response(sock.recvfrom(1024)) 37 | responses[response['location']] = response 38 | 39 | except socket.timeout: 40 | break 41 | 42 | return responses.values() 43 | 44 | def _get_response(self, response): 45 | headers, addr = response 46 | 47 | result = { 48 | 'ip': addr[0], 49 | 'port': addr[1] 50 | } 51 | 52 | for s in headers.splitlines(): 53 | x = s.split(": ") 54 | if len(x) == 2: 55 | result[x[0].lower()] = x[1] 56 | return result 57 | -------------------------------------------------------------------------------- /samsungtv/upnpevents/__init__.py: -------------------------------------------------------------------------------- 1 | """UPnP Services""" 2 | 3 | from .subscriber import EventSubscriber 4 | 5 | __all__ = [ 6 | 'EventSubscriber' 7 | ] 8 | -------------------------------------------------------------------------------- /samsungtv/upnpevents/subscriber.py: -------------------------------------------------------------------------------- 1 | import requests 2 | 3 | 4 | class EventSubscriber: 5 | 6 | def __init__(self, url="", callback=""): 7 | 8 | self.url = url 9 | self.callback = callback 10 | pass 11 | 12 | def subscribe(self, timeout=1000): 13 | headers = { 14 | u'TIMEOUT': '5000', 15 | u'NT': 'upnp:event', 16 | u'CALLBACK': '<{}/>'.format(self.callback), 17 | u'User-Agent': 'HTTPSamsungCtrlCli' 18 | } 19 | 20 | result = {} 21 | 22 | response = requests.request(method='subscribe', 23 | url=self.url, 24 | headers=headers, 25 | verify=True, 26 | stream=True, 27 | timeout=30, 28 | allow_redirects=False, 29 | auth=None, 30 | cert=None 31 | ) 32 | 33 | if response.status_code == 200: 34 | result['date'] = response.headers['date'] 35 | result['sid'] = response.headers['sid'] 36 | else: 37 | raise Exception('Error', response.status_code) 38 | 39 | return result 40 | 41 | def renew(self, sid, timeout=1000): 42 | headers = { 43 | u'TIMEOUT': str(timeout), 44 | u'SID': sid, 45 | u'User-Agent': 'HTTPSamsungCtrlCli' 46 | } 47 | 48 | result = {} 49 | 50 | response = requests.request(method='subscribe', 51 | url=self.url, 52 | headers=headers, 53 | verify=True, 54 | stream=True, 55 | timeout=30, 56 | allow_redirects=False, 57 | auth=None, 58 | cert=None 59 | ) 60 | 61 | if response.status_code == 200: 62 | result['date'] = response.headers['date'] 63 | result['sid'] = response.headers['sid'] 64 | else: 65 | raise Exception('Error', response.status_code) 66 | 67 | return result 68 | 69 | def cancel(self, sid): 70 | headers = { 71 | 'SID': sid 72 | } 73 | 74 | response = requests.request(method='UNSUBSCRIBE', 75 | url=self.url, 76 | headers=headers) 77 | if response.status_code == 200: 78 | return True 79 | else: 80 | raise Exception('Error', response.status_code) 81 | 82 | pass 83 | -------------------------------------------------------------------------------- /samsungtv/upnpservice/__init__.py: -------------------------------------------------------------------------------- 1 | """UPnP Services""" 2 | 3 | from .base import UPnPServiceBase 4 | from .avtransport import UPnPServiceAVTransport 5 | from .connectionmanager import UPnPServiceConnectionManager 6 | from .rendering import UPnPServiceRendering 7 | from .wfaconfig import UPnPServiceWfaConfig 8 | 9 | __all__ = [ 10 | 'UPnPServiceBase', 11 | 'UPnPServiceAVTransport', 12 | 'UPnPServiceConnectionManager', 13 | 'UPnPServiceRendering', 14 | 'UPnPServiceWfaConfig' 15 | ] 16 | -------------------------------------------------------------------------------- /samsungtv/upnpservice/avtransport.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import xml.etree.cElementTree as XML 3 | from base import UPnPServiceBase 4 | 5 | 6 | class UPnPServiceAVTransport(UPnPServiceBase): 7 | 8 | SEEK_UNITS = [ 9 | "TRACK_NR", 10 | "REL_TIME", 11 | "ABS_TIME", 12 | "ABS_COUNT", 13 | "REL_COUNT", 14 | "X_DLNA_REL_BYTE", 15 | "FRAME", 16 | ] 17 | 18 | def __init__(self, ip, port="9197", id='0', config=None): 19 | super(UPnPServiceAVTransport, self).__init__(ip, port) 20 | 21 | self.id = id 22 | self.stype = 'AVTransport' 23 | self.endpoint = '' 24 | 25 | if config is not None: 26 | self.endpoint = config['controlURL'] 27 | 28 | def url(self, uri): 29 | action = 'SetAVTransportURI' 30 | args = [('InstanceID', self.id), 31 | ('CurrentURI', uri), 32 | ('CurrentURIMetaData', '') 33 | # ('CurrentURIMetaData', '<DIDL-Lite xmlns="urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/" xmlns:upnp="urn:schemas-upnp-org:metadata-1-0/upnp/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:sec="http://www.sec.co.kr/"><item id="f-0" parentID="0" restricted="0"><dc:title>Video</dc:title><dc:creator>vGet</dc:creator><upnp:class>object.item.videoItem</upnp:class><res protocolInfo="http-get:*:video/mp4:DLNA.ORG_OP=01;DLNA.ORG_CI=0;DLNA.ORG_FLAGS=01700000000000000000000000000000" sec:URIType="public">$URI</res></item></DIDL-Lite>') 34 | ] 35 | 36 | response = self._send_cmd(action, args) 37 | 38 | return self._get_result(response) 39 | 40 | def set_url(self, uri): 41 | action = 'SetAVTransportURI' 42 | args = [ 43 | ('InstanceID', self.id), 44 | ('CurrentURI', uri), 45 | ('CurrentURIMetaData', '') 46 | ] 47 | 48 | response = self._send_cmd(action, args) 49 | 50 | return self._get_result(response) 51 | 52 | def set_next_url(self, uri): 53 | action = 'SetNextAVTransportURI' 54 | args = [ 55 | ('InstanceID', self.id), 56 | ('NextURI', uri), 57 | ('NextURIMetaData', '') 58 | ] 59 | 60 | response = self._send_cmd(action, args) 61 | 62 | return self._get_result(response) 63 | 64 | def prefetch_url(self, uri): 65 | action = 'X_PrefetchURI' 66 | args = [ 67 | ('InstanceID', self.id), 68 | ('PrefetchURI', uri), 69 | ('PrefetchURIMetaData', '') 70 | ] 71 | 72 | response = self._send_cmd(action, args) 73 | # print response 74 | return self._get_result(response) 75 | 76 | def get_transport_settings(self): 77 | action = 'GetTransporget_transport_settings' 78 | args = [('InstanceID', self.id)] 79 | 80 | response = self._send_cmd(action, args) 81 | 82 | return self._get_result(response) 83 | 84 | def device_cap(self): 85 | action = 'GetDeviceCapabilities' 86 | args = [('InstanceID', self.id)] 87 | 88 | response = self._send_cmd(action, args) 89 | 90 | return self._get_result(response) 91 | 92 | def player_app_hint(self): 93 | action = 'X_PlayerAppHint' 94 | args = [('InstanceID', self.id)] 95 | 96 | response = self._send_cmd(action, args) 97 | 98 | return self._get_result(response) 99 | 100 | def get_transport_info(self): 101 | action = 'GetTransportInfo' 102 | args = [('InstanceID', self.id)] 103 | 104 | response = self._send_cmd(action, args) 105 | 106 | return self._get_result(response) 107 | 108 | def media_info(self): 109 | action = 'GetMediaInfo' 110 | args = [('InstanceID', self.id)] 111 | 112 | response = self._send_cmd(action, args) 113 | 114 | return self._get_result(response) 115 | 116 | def play(self, speed="1"): 117 | action = 'Play' 118 | args = [('InstanceID', self.id), ('Speed', speed)] 119 | 120 | response = self._send_cmd(action, args) 121 | 122 | return self._get_result(response) 123 | 124 | def pause(self, speed="1"): 125 | action = 'Pause' 126 | args = [('InstanceID', self.id), ('Speed', speed)] 127 | 128 | response = self._send_cmd(action, args) 129 | 130 | return self._get_result(response) 131 | 132 | def stop(self): 133 | action = 'Stop' 134 | args = [('InstanceID', self.id)] 135 | 136 | response = self._send_cmd(action, args) 137 | 138 | return self._get_result(response) 139 | 140 | def next(self): 141 | action = 'Next' 142 | args = [('InstanceID', self.id)] 143 | 144 | response = self._send_cmd(action, args) 145 | 146 | return self._get_result(response) 147 | 148 | def previous(self): 149 | action = 'Previous' 150 | args = [('InstanceID', self.id)] 151 | 152 | response = self._send_cmd(action, args) 153 | 154 | return self._get_result(response) 155 | 156 | def get_position_info(self): 157 | action = 'GetPositionInfo' 158 | args = [('InstanceID', self.id)] 159 | 160 | response = self._send_cmd(action, args) 161 | 162 | result = self._get_result(response) 163 | 164 | metadata = result['TrackMetaData'] 165 | 166 | if metadata and metadata != '': 167 | xml = XML.fromstring(metadata) 168 | title = xml.findtext('.//{http://purl.org/dc/elements/1.1/}title') 169 | artist = xml.findtext('.//{http://purl.org/dc/elements/1.1/}creator') 170 | # uri = xml.findtext('.//{urn:schemas-upnp-org:metadata-1-0/DIDL-Lite/}res') 171 | 172 | result['metadata'] = {'title': title, 'artist': artist} 173 | 174 | return result 175 | 176 | def seek(self, unit='TRACK_NR', target=1): 177 | 178 | action = 'Seek' 179 | args = [('InstanceID', self.id), ('Unit', unit), ('Target', target)] 180 | 181 | response = self._send_cmd(action, args) 182 | 183 | return self._get_result(response) 184 | 185 | def get_stopped_reason(self): 186 | action = 'X_GetStoppedReason' 187 | args = [('InstanceID', self.id)] 188 | 189 | response = self._send_cmd(action, args) 190 | 191 | return self._get_result(response) 192 | 193 | def get_current_transport_actions(self): 194 | action = 'GetCurrentTransportActions' 195 | args = [('InstanceID', self.id)] 196 | 197 | response = self._send_cmd(action, args) 198 | 199 | return self._get_result(response) 200 | -------------------------------------------------------------------------------- /samsungtv/upnpservice/base.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import re 3 | import requests 4 | import xml.etree.cElementTree as XML 5 | 6 | 7 | class UPnPServiceBase(object): 8 | soap_body_template = ( 9 | '' 10 | '' 12 | '' 13 | '' 15 | '{arguments}' 16 | '' 17 | '' 18 | '') 19 | 20 | def __init__(self, ip, port="9197"): 21 | 22 | self.ip = ip 23 | self.port = port 24 | 25 | self.endpoint = "" 26 | self.stype = "" 27 | self.sns = "schemas-upnp-org" 28 | 29 | def _is_error(self, response): 30 | m = re.match(r".*errorCode.*", response) 31 | 32 | return True if m else False 33 | 34 | def _get_result(self, response): 35 | if self._is_error(response): 36 | return self._parse_error(response) 37 | 38 | tree = XML.fromstring(response) 39 | body = tree.find("{http://schemas.xmlsoap.org/soap/envelope/}Body")[0] 40 | 41 | res = {} 42 | for i in body: 43 | res[i.tag] = i.text 44 | 45 | return res 46 | 47 | def _send_cmd(self, action, arguments): 48 | 49 | args = '' 50 | for tag, value in arguments: 51 | args += '<{tag}>{value}'.format(tag=tag, value=value) 52 | 53 | soap_action = '"urn:{sns}:service:{stype}:{version}#{action}"'.format( 54 | sns=self.sns, 55 | stype=self.stype, 56 | version="1", 57 | action=action) 58 | 59 | headers = { 60 | 'Content-Type': 'text/xml', 61 | 'SOAPAction': soap_action 62 | } 63 | 64 | data = self.soap_body_template.format( 65 | sns=self.sns, 66 | arguments=args, 67 | action=action, 68 | stype=self.stype, 69 | version=1) 70 | 71 | # print "==================================" 72 | # print headers 73 | # print "==================================" 74 | # print data 75 | # print "==================================" 76 | 77 | r = requests.post('http://' + self.ip + ':' + str(self.port) + self.endpoint, data=str(data), headers=headers) 78 | return r.content 79 | 80 | def _parse_error(self, response): 81 | """ Parse an error returned from the Sonos speaker. 82 | """ 83 | error = XML.fromstring(response) 84 | error_code = error.findtext('.//{urn:schemas-upnp-org:control-1-0}errorCode') 85 | error_message = error.findtext('.//{urn:schemas-upnp-org:control-1-0}errorDescription') 86 | 87 | if error_code is not None: 88 | raise Exception(error_message, error_code) 89 | else: 90 | raise Exception('Unknown Error ' + response, 0) 91 | -------------------------------------------------------------------------------- /samsungtv/upnpservice/connectionmanager.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from base import UPnPServiceBase 4 | 5 | 6 | class UPnPServiceConnectionManager(UPnPServiceBase): 7 | 8 | def __init__(self, ip, port="9197", config=None): 9 | super(UPnPServiceConnectionManager, self).__init__(ip, port) 10 | 11 | self.id = '0' 12 | self.stype = 'ConnectionManager' 13 | self.endpoint = '/dmr/upnp/control/ConnectionManager1' 14 | 15 | if config is not None: 16 | self.endpoint = config['controlURL'] 17 | 18 | def protocol_info(self): 19 | action = 'GetProtocolInfo' 20 | args = [('InstanceID', self.id)] 21 | 22 | response = self._send_cmd(action, args) 23 | 24 | result = self._get_result(response) 25 | 26 | result['Sink'] = result['Sink'].split(",") 27 | 28 | return result 29 | 30 | def connection_info(self): 31 | action = 'GetCurrentConnectionInfo' 32 | args = [('ConnectionID', '0')] 33 | 34 | response = self._send_cmd(action, args) 35 | 36 | return self._get_result(response) 37 | 38 | def connections(self): 39 | action = 'GetCurrentConnectionIDs' 40 | args = [('InstanceID', self.id)] 41 | 42 | response = self._send_cmd(action, args) 43 | 44 | return self._get_result(response) 45 | 46 | def prepare_for_connection(self, proto="", pcm="", pc_id="-1", d="Input"): 47 | action = 'PrepareForConnection' 48 | args = [ 49 | ('RemoteProtocolInfo', proto), 50 | ('PeerConnectionManager', pcm), 51 | ('PeerConnectionID', pc_id), 52 | ('Direction', d) 53 | ] 54 | 55 | response = self._send_cmd(action, args) 56 | 57 | return self._get_result(response) 58 | 59 | def connection_complete(self, con_id="0"): 60 | action = 'ConnectionComplete' 61 | args = [ 62 | ('ConnectionID', con_id) 63 | ] 64 | 65 | response = self._send_cmd(action, args) 66 | 67 | return self._get_result(response) 68 | 69 | -------------------------------------------------------------------------------- /samsungtv/upnpservice/dialreceiver.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from base import UPnPServiceBase 4 | 5 | 6 | class UPnPServiceDialReceiver(UPnPServiceBase): 7 | 8 | def __init__(self, ip, port="7678", config=None): 9 | super(UPnPServiceDialReceiver, self).__init__(ip, port) 10 | 11 | self.endpoint = '/RCR/control/dial' 12 | self.sns = "dial-multiscreen-org" 13 | self.stype = 'dial' 14 | 15 | if config is not None: 16 | self.endpoint = config['controlURL'] 17 | 18 | def key_send(self, key_code, key_description = ''): 19 | action = 'SendKeyCode' 20 | args = [('KeyCode', key_code), ('KeyDescription', key_description)] 21 | 22 | response = self._send_cmd(action, args) 23 | 24 | result = self._get_result(response) 25 | 26 | return result 27 | 28 | if __name__ == "__main__": 29 | s = UPnPServiceDialReceiver("192.168.0.100", "7678") 30 | 31 | print s.key_send('10146', '') -------------------------------------------------------------------------------- /samsungtv/upnpservice/rendering.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from base import UPnPServiceBase 3 | 4 | 5 | class UPnPServiceRendering(UPnPServiceBase): 6 | 7 | def __init__(self, ip, port="9197", config=None): 8 | super(UPnPServiceRendering, self).__init__(ip, port) 9 | 10 | self.id = '0' 11 | self.endpoint = '/dmr/upnp/control/RenderingControl1' 12 | self.stype = 'RenderingControl' 13 | 14 | if config is not None : 15 | self.endpoint = config['controlURL'] 16 | 17 | 18 | def mute(self, mute): 19 | mute_value = '1' if mute is True else '0' 20 | 21 | action = 'SetMute' 22 | args = [('InstanceID', self.id), ('Channel', 'Master'), ('DesiredMute', mute_value)] 23 | 24 | response = self._send_cmd(action, args) 25 | 26 | return self._get_result(response) 27 | 28 | def tv_slide_show(self): 29 | 30 | action = 'X_GetTVSlideShow' 31 | args = [('InstanceID', self.id)] 32 | 33 | response = self._send_cmd(action, args) 34 | 35 | return self._get_result(response) 36 | 37 | def set_tv_slide_show(self, state, theme): 38 | 39 | action = 'X_SetTVSlideShow' 40 | args = [('InstanceID', self.id), ('CurrentShowState', state), ('CurrentShowTheme', theme)] 41 | 42 | response = self._send_cmd(action, args) 43 | 44 | return self._get_result(response) 45 | 46 | def zoom(self, x, y, w=0, h=0): 47 | 48 | action = 'X_SetZoom' 49 | args = [('InstanceID', self.id), ('x', x), ('y', y), ('w', w), ('h', h)] 50 | 51 | response = self._send_cmd(action, args) 52 | print response 53 | return self._get_result(response) 54 | 55 | def audio_selection(self): 56 | 57 | action = 'X_GetAudioSelection' 58 | args = [('InstanceID', self.id)] 59 | 60 | response = self._send_cmd(action, args) 61 | 62 | return self._get_result(response) 63 | 64 | def video_selection(self): 65 | 66 | action = 'X_GetVideoSelection' 67 | args = [('InstanceID', self.id)] 68 | 69 | response = self._send_cmd(action, args) 70 | 71 | return self._get_result(response) 72 | 73 | def presets(self): 74 | 75 | action = 'ListPresets' 76 | args = [('InstanceID', self.id)] 77 | 78 | response = self._send_cmd(action, args) 79 | 80 | return self._get_result(response) 81 | 82 | def select_preset(self, name=""): 83 | 84 | action = 'SelectPreset' 85 | args = [('InstanceID', self.id), ("PresetName", name)] 86 | 87 | response = self._send_cmd(action, args) 88 | 89 | return self._get_result(response) 90 | 91 | def volume(self, volume=False): 92 | if volume: 93 | 94 | action = 'SetVolume' 95 | args = [('InstanceID', self.id), ('Channel', 'Master'), ('DesiredVolume', volume)] 96 | 97 | response = self._send_cmd(action, args) 98 | 99 | return self._get_result(response) 100 | 101 | else: 102 | 103 | action = 'GetVolume' 104 | args = [('InstanceID', self.id), ('Channel', 'Master')] 105 | 106 | response = self._send_cmd(action, args) 107 | 108 | result = self._get_result(response) 109 | 110 | return int(result['CurrentVolume']) 111 | 112 | -------------------------------------------------------------------------------- /samsungtv/upnpservice/wfaconfig.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | import base64 4 | from base import UPnPServiceBase 5 | 6 | 7 | class UPnPServiceWfaConfig(UPnPServiceBase): 8 | 9 | def __init__(self, ip, port="49152"): 10 | super(UPnPServiceWfaConfig, self).__init__(ip, port) 11 | 12 | self.endpoint = '/wps_control' 13 | self.stype = 'WFAWLANConfig' 14 | self.sns = "schemas-wifialliance-org" 15 | 16 | def get_device_info(self): 17 | action = 'GetDeviceInfo' 18 | args = [] 19 | 20 | response = self._send_cmd(action, args) 21 | 22 | res = self._get_result(response) 23 | 24 | if res.get("faultcode"): 25 | raise Exception("Error {}".format(res.get("faultstring")), res.get("faultstring")) 26 | 27 | if not res.get("NewDeviceInfo"): 28 | raise Exception("Missing Device Info") 29 | 30 | # print res 31 | 32 | return base64.b64decode(res['NewDeviceInfo']) 33 | 34 | def get_sta_settings(self): 35 | action = 'GetSTASettings' 36 | args = [('NewMessage', 'll')] 37 | 38 | response = self._send_cmd(action, args) 39 | 40 | res = self._get_result(response) 41 | print response 42 | 43 | if res.get("faultcode"): 44 | raise Exception("Error {}".format(res.get("faultstring")), res.get("faultstring")) 45 | 46 | # if not res.get("NewDeviceInfo"): 47 | # raise Exception("Missing Device Info") 48 | 49 | 50 | # return base64.b64decode(res['NewDeviceInfo']) 51 | 52 | 53 | if __name__ == "__main__": 54 | s = UPnPServiceWfaConfig("192.168.0.1") 55 | 56 | print s.get_device_info() 57 | print s.get_sta_settings() -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.1.0 3 | commit = True 4 | tag = True 5 | 6 | [bumpversion:file:setup.py] 7 | search = version='{current_version}' 8 | replace = version='{new_version}' 9 | 10 | [bumpversion:file:samsungtv/__init__.py] 11 | search = __version__ = '{current_version}' 12 | replace = __version__ = '{new_version}' 13 | 14 | [flake8] 15 | exclude = docs 16 | 17 | [aliases] 18 | # Define setup.py command aliases here 19 | 20 | [nosetests] 21 | detailed-errors=1 22 | ;with-coverage3=1 23 | ;cover3-package=samsungtv 24 | ;cover3-erase=1 25 | verbosity=1 26 | 27 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: utf-8 -*- 3 | 4 | """The setup script.""" 5 | 6 | from setuptools import setup, find_packages 7 | 8 | with open('README.rst') as readme_file: 9 | readme = readme_file.read() 10 | 11 | requirements = ['websocket-client', ] 12 | 13 | setup_requirements = [] 14 | 15 | test_requirements = [] 16 | 17 | setup( 18 | author="S.Simeonov", 19 | author_email='svilborg@gmail.com', 20 | classifiers=[ 21 | 'Development Status :: 2 - Pre-Alpha', 22 | 'Intended Audience :: Developers', 23 | 'License :: OSI Approved :: MIT License', 24 | 'Natural Language :: English', 25 | "Programming Language :: Python :: 2", 26 | 'Programming Language :: Python :: 2.7', 27 | 'Programming Language :: Python :: 3', 28 | 'Programming Language :: Python :: 3.4', 29 | 'Programming Language :: Python :: 3.5', 30 | 'Programming Language :: Python :: 3.6', 31 | ], 32 | description="Samsung Tv UPnP/DIAL/Remote Automations", 33 | install_requires=requirements, 34 | license="MIT license", 35 | long_description=readme + '\n\n', 36 | include_package_data=True, 37 | keywords='samsungtv', 38 | name='samsungtv', 39 | entry_points={ 40 | 'console_scripts': [ 41 | 'samsungtv-cli=samsungtv.__main__:main', 42 | ], 43 | }, 44 | packages=find_packages(include=['samsungtv*']), 45 | setup_requires=setup_requirements, 46 | test_suite='tests', 47 | tests_require=test_requirements, 48 | url='https://github.com/svilborg/samsungtv', 49 | version='0.1.0', 50 | zip_safe=False, 51 | ) 52 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/svilborg/samsungtv/007d8cbf4326ae13d74ab138eb77798d11aae108/tests/__init__.py -------------------------------------------------------------------------------- /tests/mocks.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import mock 4 | 5 | 6 | def mock_response(status=200, content=""): 7 | resp = mock.Mock() 8 | 9 | resp.status_code = status 10 | resp.content = content 11 | 12 | # resp.json = mock.Mock( 13 | # return_value=json_data 14 | # ) 15 | return resp 16 | 17 | 18 | def mock_ws_conn(data=None, connected=True): 19 | j = json.dumps(data) 20 | 21 | websocket_conn = mock.MagicMock() 22 | websocket_conn.close.return_value = True 23 | websocket_conn.connected = connected 24 | websocket_conn.recv.return_value = j 25 | 26 | return websocket_conn 27 | -------------------------------------------------------------------------------- /tests/test_dial_service.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from unittest import TestCase 3 | import mock 4 | from tests.mocks import mock_response 5 | 6 | from samsungtv.services.dial import DialService 7 | 8 | 9 | class TestDialService(TestCase): 10 | 11 | @mock.patch('requests.post') 12 | def test_start(self, mock_post): 13 | mock_post.return_value = mock_response(200, "TEST") 14 | 15 | service = DialService("httrp://fake.me/test") 16 | result = service.start("Netflix") 17 | 18 | self.assertEquals(result, "TEST") 19 | 20 | # @mock.patch('requests.get') 21 | # def test_get(self, mock_get): 22 | # mock_get.return_value = mock_response(200) 23 | # 24 | # service = DialService("httrp://fake.me/test") 25 | # result = service.get("Netflix") 26 | 27 | # def test_real(self): 28 | # service = DialService("http://192.168.0.100:8080/ws/app/") 29 | # 30 | # name = "YouTube" 31 | # print "==================" 32 | # print service.start(name) 33 | # 34 | # print "==================" 35 | # print service.get(name) 36 | # 37 | # print "==================" 38 | # print service.get("Netflix") 39 | # 40 | # print "==================" 41 | # print service.get("NetflixNope") 42 | # 43 | # print "==================" 44 | # print service.get("uk.co.bbc.iPlayer") 45 | # 46 | # import time 47 | # time.sleep(2) 48 | # 49 | # print "==================" 50 | # print service.stop(name) 51 | # # print service.install("uk.co.bbc.iPlayer") 52 | -------------------------------------------------------------------------------- /tests/test_dlna_device.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from samsungtv.dlna import DlnaDevice 4 | 5 | 6 | class TestDlnaDevice(TestCase): 7 | 8 | def test_device(self): 9 | # d = DlnaDevice("http://192.168.0.100:9197/dmr") 10 | 11 | self.skipTest("Todo") 12 | pass 13 | 14 | # def test_real(self): 15 | # d = DlnaDevice("http://192.168.0.100:9197/dmr") 16 | # print d 17 | -------------------------------------------------------------------------------- /tests/test_dlna_devices.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from samsungtv.dlna import DlnaDevice, DlnaDevices 4 | 5 | 6 | class TestDlnaDevices(TestCase): 7 | def test_get_devices(self): 8 | self.skipTest("Todo") 9 | 10 | # def test_real(self): 11 | # d = DlnaDevices("d_test_cache") 12 | # devices = d.get_devices() 13 | # 14 | # for location, device in devices.items(): 15 | # print str(device) + " - " + device.info['deviceType'] 16 | # # print device.info 17 | # print "" 18 | # 19 | # # print d.get_device_by_type("urn:schemas-upnp-org:device:MediaRenderer:1") 20 | # device = d.get_device_by_type("urn:dial-multiscreen-org:device:dialreceiver:1") 21 | # print device 22 | # print device.applicationUrl 23 | -------------------------------------------------------------------------------- /tests/test_event_subscriber.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from samsungtv.upnpevents import EventSubscriber 4 | 5 | 6 | class TestEventSubscriber(TestCase): 7 | def test_subscribe(self): 8 | self.skipTest("Todo") 9 | 10 | # def test_real(self): 11 | # # http SUBSCRIBE http://192.168.0.100:9197/dmr/upnp/event/RenderingControl1 TIMEOUT:1000 NT:'upnp:event' 12 | # 13 | # s = EventSubscriber(u'http://192.168.0.100:9197/upnp/event/RenderingControl1') 14 | # 15 | # result = s.subscribe() 16 | # print result 17 | # 18 | # # print s.renew(result['sid']) 19 | # # print s.cancel(result['sid']) 20 | # 21 | # pass 22 | -------------------------------------------------------------------------------- /tests/test_proxy_http_handler.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from BaseHTTPServer import HTTPServer 3 | from unittest import TestCase 4 | 5 | from samsungtv.httpd.proxy_handler import ProxyHttpRequestHandler 6 | 7 | 8 | class TestProxyHttpRequestHandler(TestCase): 9 | def test__log(self): 10 | self.skipTest("Todo") 11 | 12 | # def test_real(self): 13 | # host = "" 14 | # port = 8007 15 | # 16 | # try: 17 | # # SubscribeRequestHandler.protocol_version = "HTTP/1.0" 18 | # 19 | # httpd = HTTPServer((host, port), ProxyHttpRequestHandler) 20 | # 21 | # except Exception as e: 22 | # sys.stderr.write(str(e)) 23 | # sys.exit(-1) 24 | # 25 | # print "Serving on " + host + ":" + str(port) + " ... " 26 | # 27 | # while True: 28 | # httpd.handle_request() 29 | -------------------------------------------------------------------------------- /tests/test_remote_control.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import mock 4 | from unittest import TestCase 5 | from samsungtv.services.remote_control import RemoteControl 6 | from tests.mocks import mock_ws_conn 7 | 8 | 9 | class TestRemoteControl(TestCase): 10 | 11 | def setUp(self): 12 | self.rc = RemoteControl("192.168.0.100") 13 | 14 | @mock.patch('samsungtv.services.remote_control.websocket') 15 | def test_connect(self, mock_ws): 16 | mock_ws.create_connection.return_value = mock_ws_conn({'data': {'id': 'ID_OK'}}) 17 | 18 | id = self.rc.connect() 19 | self.rc.close() 20 | 21 | self.assertIsNotNone(id) 22 | self.assertEquals(id, u'ID_OK') 23 | 24 | @mock.patch('samsungtv.services.remote_control.websocket') 25 | def test_command(self, mock_ws): 26 | mock_ws.create_connection.return_value = mock_ws_conn({'data': {'id': 'ID_OK'}}) 27 | 28 | id = self.rc.connect() 29 | r = self.rc.command("KEY_2") 30 | self.rc.close() 31 | 32 | self.assertIsNotNone(id) 33 | 34 | @mock.patch('samsungtv.services.remote_control.websocket') 35 | def test_launch(self, mock_ws): 36 | mock_ws.create_connection.return_value = mock_ws_conn({'data': {'id': 'ID_OK'}}) 37 | id = self.rc.connect() 38 | r = self.rc.launch("org.tizen.browser") 39 | 40 | self.rc.close() 41 | 42 | # def test_real(self): 43 | # rc = RemoteControl("192.168.0.100") 44 | # rc.connect() 45 | # rc.command("KEY_0") 46 | # print rc.test("com.tizen.browser") 47 | # print rc.apps() 48 | # print rc.launch(20162100002) 49 | # print rc.launch("org.tizen.browser") 50 | # rc.launch("com.netflix.samsung") 51 | # rc.launch("com.netflix.tizen") 52 | # rc.close() 53 | -------------------------------------------------------------------------------- /tests/test_ssdp_discovery.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | 4 | class TestSSDPDiscovery(TestCase): 5 | def test_discover(self): 6 | self.skipTest("Todo") 7 | -------------------------------------------------------------------------------- /tests/test_subscribe_http_handler.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from BaseHTTPServer import HTTPServer 3 | from unittest import TestCase 4 | 5 | from samsungtv.httpd.subscribe_handler import SubscribeHttpRequestHandler 6 | 7 | 8 | class TestSubscribeHttpRequestHandler(TestCase): 9 | def test__log(self): 10 | self.skipTest("Todo") 11 | 12 | # def test_real(self): 13 | # host = "" 14 | # port = 8007 15 | # 16 | # try: 17 | # # SubscribeRequestHandler.protocol_version = "HTTP/1.0" 18 | # 19 | # httpd = HTTPServer((host, port), SubscribeHttpRequestHandler) 20 | # 21 | # except Exception as e: 22 | # sys.stderr.write(str(e)) 23 | # sys.exit(-1) 24 | # 25 | # print "Serving on " + host + ":" + str(port) + " ... " 26 | # 27 | # while True: 28 | # httpd.handle_request() 29 | -------------------------------------------------------------------------------- /tests/test_upnpservice_av_transport.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | from samsungtv.upnpservice import UPnPServiceAVTransport 3 | 4 | class TestUPnPServiceAVTransport(TestCase): 5 | 6 | def test_init(self): 7 | t = UPnPServiceAVTransport('192.168.0.1') 8 | self.skipTest("Todo") 9 | 10 | # def test_real(self): 11 | # print "UPnPServiceAVTransport \n" 12 | # 13 | # import pprint 14 | # import time 15 | # 16 | # t = UPnPServiceAVTransport('192.168.0.100', '9197', config={'controlURL': '/dmr/upnp/control/AVTransport1'}) 17 | 18 | # print "====================" 19 | # pprint.pprint(t.device_cap()) 20 | 21 | # print "====================" 22 | # pprint.pprint(t.get_transport_info()) 23 | # print "====================" 24 | # pprint.pprint(t.get_transport_settings()) 25 | 26 | 27 | # print t.set_url("http://192.168.0.103:8000/media/t.mp4") 28 | # print t.set_url("http://192.168.0.103:8000/media/test.jpg#1") 29 | # print t.set_next_url("http://192.168.0.103:8000/media/test2.jpg#2") 30 | # time.sleep(1) 31 | 32 | # print "====================" 33 | # print t.play() 34 | 35 | # print "====================" 36 | # pprint.pprint (t.get_position_info()) 37 | 38 | # time.sleep(2) 39 | # print "====================" 40 | 41 | # time.sleep(4) 42 | # print "====================" 43 | # print t.pause() 44 | 45 | # print "====================" 46 | # pprint.pprint(t.media_info()) 47 | 48 | # time.sleep(4) 49 | # print t.next() 50 | 51 | # print "====================" 52 | # pprint.pprint(t.media_info()) 53 | 54 | # exit(1) 55 | 56 | # # print t.set_url("http://192.168.0.103:8000/media/test.jpg") 57 | # # print t.set_next_url("http://192.168.0.103:8000/media/test3.jpg") 58 | # # print t.seek('REL_TIME', '00:00:05') 59 | 60 | # # print t.prefetch_url("http://192.168.0.103:8000/media/test3.jpg") 61 | # # print t.stop() 62 | 63 | # def test_real_pic_rotate (self) : 64 | # l = [ 65 | # 'http://i.imgur.com/6yHmlwT.jpg', 66 | # 'http://i.imgur.com/qCoybZR.jpg', 67 | # 'http://i.imgur.com/hl4mfZf.jpg', 68 | # ] 69 | # 70 | # is_play=False 71 | # 72 | # for img in l: 73 | # 74 | # print img 75 | # 76 | # if not is_play: 77 | # print t.set_url(img) 78 | # print t.play() 79 | # 80 | # is_play=True 81 | # 82 | # time.sleep(1) 83 | # 84 | # else: 85 | # print t.set_next_url(img) 86 | # print t.next() 87 | # 88 | # time.sleep(5) 89 | # pprint.pprint (t.get_transport_info()) 90 | # exit(1) 91 | -------------------------------------------------------------------------------- /tests/test_upnpservice_connection_manager.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from samsungtv.upnpservice import UPnPServiceConnectionManager 4 | 5 | 6 | class TestUPnPServiceRendering(TestCase): 7 | def test_init(self): 8 | t = UPnPServiceConnectionManager('192.168.0.1') 9 | self.skipTest("Todo") 10 | 11 | # def test_real(self): 12 | # import pprint 13 | # 14 | # pprint.pprint("UPnPServiceConnectionManager") 15 | # t = UPnPServiceConnectionManager('192.168.0.100', '9197') 16 | # 17 | # conn = t.prepare_for_connection('http-get:*:image/jpeg:*') 18 | # 19 | # print conn 20 | # 21 | # # pprint.pprint(t.protocol_info()) 22 | # print t.connection_info() 23 | # print t.connections() 24 | # 25 | # # from avtransport import UPnPServiceAVTransport 26 | # # av = UPnPServiceAVTransport('192.168.0.100', '9197', config={"controlURL":"/upnp/control/AVTransport1"}) 27 | # # print av.get_transport_info() 28 | # # print av.get_stopped_reason() 29 | # # print av.media_info() 30 | # # print av.play() 31 | # # print av.stop() 32 | # 33 | # print t.connection_complete(conn["ConnectionID"]) 34 | -------------------------------------------------------------------------------- /tests/test_upnpservice_rendering.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from samsungtv.upnpservice import UPnPServiceRendering 4 | 5 | 6 | class TestUPnPServiceRendering(TestCase): 7 | def test_init(self): 8 | t = UPnPServiceRendering('192.168.0.1') 9 | self.skipTest("Todo") 10 | 11 | 12 | # def test_real(self): 13 | # print "UPnPServiceRendering \n" 14 | # 15 | # t = UPnPServiceRendering('192.168.0.100', '9197') 16 | # # print t.audio_selection() 17 | # # print t.video_selection() 18 | # print t.volume() 19 | # # print t.tv_slide_show() 20 | # # print t.set_tv_slide_show(0,'2') 21 | # # print t.zoom(0, 0 , 350, 620) 22 | # # print t.presets() 23 | # # print t.select_preset() 24 | 25 | 26 | # def test_real_moving_pic(self): 27 | # import time 28 | # 29 | # # https://i.imgur.com/BMvDxow.jpg 30 | # # 3847x808 31 | # t = UPnPServiceRendering('192.168.0.100', '9197') 32 | # 33 | # x = 0 34 | # y = 20 35 | # w = 800 36 | # h = 360 37 | # 38 | # while True: 39 | # time.sleep(0.1) 40 | # print t.zoom(x, y, w, h) 41 | # 42 | # # start +=10 43 | # x += 3 44 | # print (x, y, w, h) 45 | # # y +=10 46 | # pass 47 | -------------------------------------------------------------------------------- /tests/test_upnpservice_wfa_config.py: -------------------------------------------------------------------------------- 1 | from unittest import TestCase 2 | 3 | from samsungtv.upnpservice import UPnPServiceWfaConfig 4 | 5 | 6 | class TestUPnPServiceWfaConfig(TestCase): 7 | def test_get_device_info(self): 8 | t = UPnPServiceWfaConfig('192.168.0.1') 9 | self.skipTest("Todo") 10 | 11 | # def test_real(self): 12 | # print "UPnPServiceWfaConfig \n" 13 | # t = UPnPServiceWfaConfig('192.168.0.1') 14 | # 15 | # print t.get_device_info() 16 | 17 | 18 | # 19 | # class TestUPnPServiceAVTransport(TestCase): 20 | # def test_init(self): 21 | # t = UPnPServiceAVTransport('192.168.0.1') 22 | # self.skipTest("Todo") 23 | # 24 | # # def test_real(self): 25 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27, py34, py35, py36, flake8 3 | 4 | [travis] 5 | python = 6 | 3.6: py36 7 | 3.5: py35 8 | 3.4: py34 9 | 2.7: py27 10 | 11 | [testenv:flake8] 12 | basepython = python 13 | deps = flake8 14 | commands = flake8 samsungtv 15 | 16 | [testenv] 17 | setenv = 18 | PYTHONPATH = {toxinidir} 19 | 20 | commands = python setup.py test 21 | 22 | --------------------------------------------------------------------------------