diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 00000000..4b688edd --- /dev/null +++ b/.coveragerc @@ -0,0 +1,7 @@ +[report] +exclude_lines = + # Don't complain if tests don't hit defensive assertion code: + raise NotImplementedError + +[run] +source = teslajsonpy diff --git a/.gitignore b/.gitignore index e7c89777..709659d8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1,108 @@ -*.pyc -*.pyo -app.db -search.db -flask -/.idea/ -/tmp/ -SeedUpload.db -/db_repository/ -*log -/storage/ -env/ -env +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python build/ +develop-eggs/ dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ *.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +.hypothesis/ +.pytest_cache/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ + +# macOS files +._* +.DS_Store diff --git a/AUTHORS.md b/AUTHORS.md new file mode 100644 index 00000000..00987455 --- /dev/null +++ b/AUTHORS.md @@ -0,0 +1,14 @@ +# Contributions to `teslajsonpy` + +## Owners + +- Sergey Isachenko [GitHub](https://github.com/zabuldon) + +## Maintainers + +- Alan Tse [GitLab](https://gitlab.com/alandtse) [GitHub](https://github.com/alandtse) + +## Contributors + +- ultratoto14 [GitHub](https://github.com/ultratoto14) +- johanjongsma [GitHub](https://github.com/johanjongsma) diff --git a/LICENSE b/LICENSE index 5a8e3325..d6456956 100644 --- a/LICENSE +++ b/LICENSE @@ -1,14 +1,202 @@ - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - Version 2, December 2004 - Copyright (C) 2004 Sam Hocevar + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - Everyone is permitted to copy and distribute verbatim or modified - copies of this license document, and changing it is allowed as long - as the name is changed. + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE - TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + 1. Definitions. - 0. You just DO WHAT THE FUCK YOU WANT TO. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..9420ba48 --- /dev/null +++ b/Makefile @@ -0,0 +1,27 @@ +# SPDX-License-Identifier: WTFPL +# Based on code from https://github.com/bachya/simplisafe-python/blob/dev/Makefile +coverage: + #Not implemented yet + #pipenv run py.test -s --verbose --cov-report term-missing --cov-report xml --cov=teslajsonpy tests +clean: + rm -rf dist/ build/ .egg teslajsonpy.egg-info/ +init: + pip3 install --upgrade pip pipenv + pipenv lock + pipenv install --three --dev +lint: flake8 docstyle pylint +flake8: + pipenv run flake8 teslajsonpy +docstyle: + pipenv run pydocstyle teslajsonpy +pylint: + pipenv run pylint teslajsonpy +publish: + pipenv run python setup.py sdist bdist_wheel + pipenv run twine upload dist/* + rm -rf dist/ build/ .egg teslajsonpy.egg-info/ +test: + #Not implemented yet + #pipenv run py.test +typing: + pipenv run mypy --ignore-missing-imports teslajsonpy diff --git a/Pipfile b/Pipfile new file mode 100644 index 00000000..6ab9ad5c --- /dev/null +++ b/Pipfile @@ -0,0 +1,16 @@ +[[source]] +url = "https://pypi.python.org/simple" +verify_ssl = true + +[dev-packages] +"flake8" = "*" +detox = "*" +mypy = "*" +pydocstyle = "*" +pylint = "*" +pytest-cov = "*" +tox = "*" +twine = "*" + +[packages] +requests = "*" diff --git a/Pipfile.lock b/Pipfile.lock new file mode 100644 index 00000000..e620abf8 --- /dev/null +++ b/Pipfile.lock @@ -0,0 +1,476 @@ +{ + "_meta": { + "hash": { + "sha256": "ccc4568747c1f6c73e86326d4589fbf91d4713ea71bfeac5f0a89c73105fd7e2" + }, + "pipfile-spec": 6, + "requires": {}, + "sources": [ + { + "url": "https://pypi.python.org/simple", + "verify_ssl": true + } + ] + }, + "default": { + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "version": "==2.21.0" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + } + }, + "develop": { + "astroid": { + "hashes": [ + "sha256:8a8d8f610ab3646f89f4c1b89385446e2675c3b7139a577116fa966d33a81d6d", + "sha256:91f52b4e4645ee610a82841aacc7ecd0202f695375cc7b34bae43ab4ab359099" + ], + "version": "==2.2.3" + }, + "atomicwrites": { + "hashes": [ + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + ], + "version": "==1.3.0" + }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, + "bleach": { + "hashes": [ + "sha256:213336e49e102af26d9cde77dd2d0397afabc5a6bf2fed985dc35b5d1e285a16", + "sha256:3fdf7f77adcf649c9911387df51254b813185e32b2c6619f690b593a617e19fa" + ], + "version": "==3.1.0" + }, + "certifi": { + "hashes": [ + "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", + "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + ], + "version": "==2018.11.29" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "coverage": { + "hashes": [ + "sha256:09e47c529ff77bf042ecfe858fb55c3e3eb97aac2c87f0349ab5a7efd6b3939f", + "sha256:0a1f9b0eb3aa15c990c328535655847b3420231af299386cfe5efc98f9c250fe", + "sha256:0cc941b37b8c2ececfed341444a456912e740ecf515d560de58b9a76562d966d", + "sha256:10e8af18d1315de936d67775d3a814cc81d0747a1a0312d84e27ae5610e313b0", + "sha256:1b4276550b86caa60606bd3572b52769860a81a70754a54acc8ba789ce74d607", + "sha256:1e8a2627c48266c7b813975335cfdea58c706fe36f607c97d9392e61502dc79d", + "sha256:2b224052bfd801beb7478b03e8a66f3f25ea56ea488922e98903914ac9ac930b", + "sha256:447c450a093766744ab53bf1e7063ec82866f27bcb4f4c907da25ad293bba7e3", + "sha256:46101fc20c6f6568561cdd15a54018bb42980954b79aa46da8ae6f008066a30e", + "sha256:4710dc676bb4b779c4361b54eb308bc84d64a2fa3d78e5f7228921eccce5d815", + "sha256:510986f9a280cd05189b42eee2b69fecdf5bf9651d4cd315ea21d24a964a3c36", + "sha256:5535dda5739257effef56e49a1c51c71f1d37a6e5607bb25a5eee507c59580d1", + "sha256:5a7524042014642b39b1fcae85fb37556c200e64ec90824ae9ecf7b667ccfc14", + "sha256:5f55028169ef85e1fa8e4b8b1b91c0b3b0fa3297c4fb22990d46ff01d22c2d6c", + "sha256:6694d5573e7790a0e8d3d177d7a416ca5f5c150742ee703f3c18df76260de794", + "sha256:6831e1ac20ac52634da606b658b0b2712d26984999c9d93f0c6e59fe62ca741b", + "sha256:77f0d9fa5e10d03aa4528436e33423bfa3718b86c646615f04616294c935f840", + "sha256:828ad813c7cdc2e71dcf141912c685bfe4b548c0e6d9540db6418b807c345ddd", + "sha256:85a06c61598b14b015d4df233d249cd5abfa61084ef5b9f64a48e997fd829a82", + "sha256:8cb4febad0f0b26c6f62e1628f2053954ad2c555d67660f28dfb1b0496711952", + "sha256:a5c58664b23b248b16b96253880b2868fb34358911400a7ba39d7f6399935389", + "sha256:aaa0f296e503cda4bc07566f592cd7a28779d433f3a23c48082af425d6d5a78f", + "sha256:ab235d9fe64833f12d1334d29b558aacedfbca2356dfb9691f2d0d38a8a7bfb4", + "sha256:b3b0c8f660fae65eac74fbf003f3103769b90012ae7a460863010539bb7a80da", + "sha256:bab8e6d510d2ea0f1d14f12642e3f35cefa47a9b2e4c7cea1852b52bc9c49647", + "sha256:c45297bbdbc8bb79b02cf41417d63352b70bcb76f1bbb1ee7d47b3e89e42f95d", + "sha256:d19bca47c8a01b92640c614a9147b081a1974f69168ecd494687c827109e8f42", + "sha256:d64b4340a0c488a9e79b66ec9f9d77d02b99b772c8b8afd46c1294c1d39ca478", + "sha256:da969da069a82bbb5300b59161d8d7c8d423bc4ccd3b410a9b4d8932aeefc14b", + "sha256:ed02c7539705696ecb7dc9d476d861f3904a8d2b7e894bd418994920935d36bb", + "sha256:ee5b8abc35b549012e03a7b1e86c09491457dba6c94112a2482b18589cc2bdb9" + ], + "version": "==4.5.2" + }, + "detox": { + "hashes": [ + "sha256:e650f95f0c7f5858578014b3b193e5dac76c89285c1bbe4bae598fd641bf9cd3", + "sha256:fcad009e2d20ce61176dc826a2c1562bd712fe53953ca603b455171cf819080f" + ], + "version": "==0.19" + }, + "dnspython": { + "hashes": [ + "sha256:36c5e8e38d4369a08b6780b7f27d790a292b2b08eea01607865bf0936c558e01", + "sha256:f69c21288a962f4da86e56c4905b49d11aba7938d3d740e80d9e366ee4f1632d" + ], + "version": "==1.16.0" + }, + "docutils": { + "hashes": [ + "sha256:02aec4bd92ab067f6ff27a38a38a41173bf01bed8f89157768c1573f53e474a6", + "sha256:51e64ef2ebfb29cae1faa133b3710143496eca21c530f3f71424d77687764274", + "sha256:7a4bd47eaf6596e1295ecb11361139febe29b084a87bf005bf899f9a42edc3c6" + ], + "version": "==0.14" + }, + "entrypoints": { + "hashes": [ + "sha256:589f874b313739ad35be6e0cd7efde2a4e9b6fea91edcc34e58ecbb8dbe56d19", + "sha256:c70dd71abe5a8c85e55e12c19bd91ccfeec11a6e99044204511f9ed547d48451" + ], + "version": "==0.3" + }, + "eventlet": { + "hashes": [ + "sha256:c584163e006e613707e224552fafc63e4e0aa31d7de0ab18b481ac0b385254c8", + "sha256:d9d31a3c8dbcedbcce5859a919956d934685b17323fc80e1077cb344a2ffa68d" + ], + "version": "==0.24.1" + }, + "filelock": { + "hashes": [ + "sha256:b8d5ca5ca1c815e1574aee746650ea7301de63d87935b3463d26368b76e31633", + "sha256:d610c1bb404daf85976d7a82eb2ada120f04671007266b708606565dd03b5be6" + ], + "version": "==3.0.10" + }, + "flake8": { + "hashes": [ + "sha256:859996073f341f2670741b51ec1e67a01da142831aa1fdc6242dbf88dffbe661", + "sha256:a796a115208f5c03b18f332f7c11729812c8c3ded6c46319c59b53efd3819da8" + ], + "version": "==3.7.7" + }, + "greenlet": { + "hashes": [ + "sha256:000546ad01e6389e98626c1367be58efa613fa82a1be98b0c6fc24b563acc6d0", + "sha256:0d48200bc50cbf498716712129eef819b1729339e34c3ae71656964dac907c28", + "sha256:23d12eacffa9d0f290c0fe0c4e81ba6d5f3a5b7ac3c30a5eaf0126bf4deda5c8", + "sha256:37c9ba82bd82eb6a23c2e5acc03055c0e45697253b2393c9a50cef76a3985304", + "sha256:51503524dd6f152ab4ad1fbd168fc6c30b5795e8c70be4410a64940b3abb55c0", + "sha256:8041e2de00e745c0e05a502d6e6db310db7faa7c979b3a5877123548a4c0b214", + "sha256:81fcd96a275209ef117e9ec91f75c731fa18dcfd9ffaa1c0adbdaa3616a86043", + "sha256:853da4f9563d982e4121fed8c92eea1a4594a2299037b3034c3c898cb8e933d6", + "sha256:8b4572c334593d449113f9dc8d19b93b7b271bdbe90ba7509eb178923327b625", + "sha256:9416443e219356e3c31f1f918a91badf2e37acf297e2fa13d24d1cc2380f8fbc", + "sha256:9854f612e1b59ec66804931df5add3b2d5ef0067748ea29dc60f0efdcda9a638", + "sha256:99a26afdb82ea83a265137a398f570402aa1f2b5dfb4ac3300c026931817b163", + "sha256:a19bf883b3384957e4a4a13e6bd1ae3d85ae87f4beb5957e35b0be287f12f4e4", + "sha256:a9f145660588187ff835c55a7d2ddf6abfc570c2651c276d3d4be8a2766db490", + "sha256:ac57fcdcfb0b73bb3203b58a14501abb7e5ff9ea5e2edfa06bb03035f0cff248", + "sha256:bcb530089ff24f6458a81ac3fa699e8c00194208a724b644ecc68422e1111939", + "sha256:beeabe25c3b704f7d56b573f7d2ff88fc99f0138e43480cecdfcaa3b87fe4f87", + "sha256:d634a7ea1fc3380ff96f9e44d8d22f38418c1c381d5fac680b272d7d90883720", + "sha256:d97b0661e1aead761f0ded3b769044bb00ed5d33e1ec865e891a8b128bf7c656" + ], + "version": "==0.4.15" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, + "isort": { + "hashes": [ + "sha256:144c4295314c0ed34fb034f838b2b7e242c52dd3eafdd6f5d49078692f582c0c", + "sha256:92a7ddacb0e7e10ed2976e6b5d58496dcda27a3f525c187a3a1a0ae5fa79ff1b" + ], + "version": "==4.3.10" + }, + "lazy-object-proxy": { + "hashes": [ + "sha256:0ce34342b419bd8f018e6666bfef729aec3edf62345a53b537a4dcc115746a33", + "sha256:1b668120716eb7ee21d8a38815e5eb3bb8211117d9a90b0f8e21722c0758cc39", + "sha256:209615b0fe4624d79e50220ce3310ca1a9445fd8e6d3572a896e7f9146bbf019", + "sha256:27bf62cb2b1a2068d443ff7097ee33393f8483b570b475db8ebf7e1cba64f088", + "sha256:27ea6fd1c02dcc78172a82fc37fcc0992a94e4cecf53cb6d73f11749825bd98b", + "sha256:2c1b21b44ac9beb0fc848d3993924147ba45c4ebc24be19825e57aabbe74a99e", + "sha256:2df72ab12046a3496a92476020a1a0abf78b2a7db9ff4dc2036b8dd980203ae6", + "sha256:320ffd3de9699d3892048baee45ebfbbf9388a7d65d832d7e580243ade426d2b", + "sha256:50e3b9a464d5d08cc5227413db0d1c4707b6172e4d4d915c1c70e4de0bbff1f5", + "sha256:5276db7ff62bb7b52f77f1f51ed58850e315154249aceb42e7f4c611f0f847ff", + "sha256:61a6cf00dcb1a7f0c773ed4acc509cb636af2d6337a08f362413c76b2b47a8dd", + "sha256:6ae6c4cb59f199d8827c5a07546b2ab7e85d262acaccaacd49b62f53f7c456f7", + "sha256:7661d401d60d8bf15bb5da39e4dd72f5d764c5aff5a86ef52a042506e3e970ff", + "sha256:7bd527f36a605c914efca5d3d014170b2cb184723e423d26b1fb2fd9108e264d", + "sha256:7cb54db3535c8686ea12e9535eb087d32421184eacc6939ef15ef50f83a5e7e2", + "sha256:7f3a2d740291f7f2c111d86a1c4851b70fb000a6c8883a59660d95ad57b9df35", + "sha256:81304b7d8e9c824d058087dcb89144842c8e0dea6d281c031f59f0acf66963d4", + "sha256:933947e8b4fbe617a51528b09851685138b49d511af0b6c0da2539115d6d4514", + "sha256:94223d7f060301b3a8c09c9b3bc3294b56b2188e7d8179c762a1cda72c979252", + "sha256:ab3ca49afcb47058393b0122428358d2fbe0408cf99f1b58b295cfeb4ed39109", + "sha256:bd6292f565ca46dee4e737ebcc20742e3b5be2b01556dafe169f6c65d088875f", + "sha256:cb924aa3e4a3fb644d0c463cad5bc2572649a6a3f68a7f8e4fbe44aaa6d77e4c", + "sha256:d0fc7a286feac9077ec52a927fc9fe8fe2fabab95426722be4c953c9a8bede92", + "sha256:ddc34786490a6e4ec0a855d401034cbd1242ef186c20d79d2166d6a4bd449577", + "sha256:e34b155e36fa9da7e1b7c738ed7767fc9491a62ec6af70fe9da4a057759edc2d", + "sha256:e5b9e8f6bda48460b7b143c3821b21b452cb3a835e6bbd5dd33aa0c8d3f5137d", + "sha256:e81ebf6c5ee9684be8f2c87563880f93eedd56dd2b6146d8a725b50b7e5adb0f", + "sha256:eb91be369f945f10d3a49f5f9be8b3d0b93a4c2be8f8a5b83b0571b8123e0a7a", + "sha256:f460d1ceb0e4a5dcb2a652db0904224f367c9b3c1470d5a7683c0480e582468b" + ], + "version": "==1.3.1" + }, + "mccabe": { + "hashes": [ + "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42", + "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f" + ], + "version": "==0.6.1" + }, + "monotonic": { + "hashes": [ + "sha256:23953d55076df038541e648a53676fb24980f7a1be290cdda21300b3bc21dfb0", + "sha256:552a91f381532e33cbd07c6a2655a21908088962bb8fa7239ecbcc6ad1140cc7" + ], + "version": "==1.5" + }, + "more-itertools": { + "hashes": [ + "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", + "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1" + ], + "markers": "python_version > '2.7'", + "version": "==6.0.0" + }, + "mypy": { + "hashes": [ + "sha256:308c274eb8482fbf16006f549137ddc0d69e5a589465e37b99c4564414363ca7", + "sha256:e80fd6af34614a0e898a57f14296d0dacb584648f0339c2e000ddbf0f4cc2f8d" + ], + "version": "==0.670" + }, + "mypy-extensions": { + "hashes": [ + "sha256:37e0e956f41369209a3d5f34580150bcacfabaa57b33a15c0b25f4b5725e0812", + "sha256:b16cabe759f55e3409a7d231ebd2841378fb0c27a5d1994719e340e4f429ac3e" + ], + "version": "==0.4.1" + }, + "pkginfo": { + "hashes": [ + "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb", + "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32" + ], + "version": "==1.5.0.1" + }, + "pluggy": { + "hashes": [ + "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", + "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" + ], + "version": "==0.9.0" + }, + "py": { + "hashes": [ + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" + ], + "version": "==1.8.0" + }, + "pycodestyle": { + "hashes": [ + "sha256:95a2219d12372f05704562a14ec30bc76b05a5b297b21a5dfe3f6fac3491ae56", + "sha256:e40a936c9a450ad81df37f549d676d127b1b66000a6c500caa2b085bc0ca976c" + ], + "version": "==2.5.0" + }, + "pydocstyle": { + "hashes": [ + "sha256:2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8", + "sha256:5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4", + "sha256:ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039" + ], + "version": "==3.0.0" + }, + "pyflakes": { + "hashes": [ + "sha256:17dbeb2e3f4d772725c777fabc446d5634d1038f234e77343108ce445ea69ce0", + "sha256:d976835886f8c5b31d47970ed689944a0262b5f3afa00a5a7b4dc81e5449f8a2" + ], + "version": "==2.1.1" + }, + "pygments": { + "hashes": [ + "sha256:5ffada19f6203563680669ee7f53b64dabbeb100eb51b61996085e99c03b284a", + "sha256:e8218dd399a61674745138520d0d4cf2621d7e032439341bc3f647bff125818d" + ], + "version": "==2.3.1" + }, + "pylint": { + "hashes": [ + "sha256:5d77031694a5fb97ea95e828c8d10fc770a1df6eb3906067aaed42201a8a6a09", + "sha256:723e3db49555abaf9bf79dc474c6b9e2935ad82230b10c1138a71ea41ac0fff1" + ], + "version": "==2.3.1" + }, + "pytest": { + "hashes": [ + "sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c", + "sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4" + ], + "version": "==4.3.0" + }, + "pytest-cov": { + "hashes": [ + "sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33", + "sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f" + ], + "version": "==2.6.1" + }, + "readme-renderer": { + "hashes": [ + "sha256:bb16f55b259f27f75f640acf5e00cf897845a8b3e4731b5c1a436e4b8529202f", + "sha256:c8532b79afc0375a85f10433eca157d6b50f7d6990f337fa498c96cd4bfc203d" + ], + "version": "==24.0" + }, + "requests": { + "hashes": [ + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" + ], + "version": "==2.21.0" + }, + "requests-toolbelt": { + "hashes": [ + "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f", + "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0" + ], + "version": "==0.9.1" + }, + "six": { + "hashes": [ + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" + ], + "version": "==1.12.0" + }, + "snowballstemmer": { + "hashes": [ + "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128", + "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89" + ], + "version": "==1.2.1" + }, + "toml": { + "hashes": [ + "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c", + "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e" + ], + "version": "==0.10.0" + }, + "tox": { + "hashes": [ + "sha256:2a8d8a63660563e41e64e3b5b677e81ce1ffa5e2a93c2c565d3768c287445800", + "sha256:edfca7809925f49bdc110d0a2d9966bbf35a0c25637216d9586e7a5c5de17bfb" + ], + "version": "==3.6.1" + }, + "tqdm": { + "hashes": [ + "sha256:d385c95361699e5cf7622485d9b9eae2d4864b21cd5a2374a9c381ffed701021", + "sha256:e22977e3ebe961f72362f6ddfb9197cc531c9737aaf5f607ef09740c849ecd05" + ], + "version": "==4.31.1" + }, + "twine": { + "hashes": [ + "sha256:0fb0bfa3df4f62076cab5def36b1a71a2e4acb4d1fa5c97475b048117b1a6446", + "sha256:d6c29c933ecfc74e9b1d9fa13aa1f87c5d5770e119f5a4ce032092f0ff5b14dc" + ], + "version": "==1.13.0" + }, + "typed-ast": { + "hashes": [ + "sha256:035a54ede6ce1380599b2ce57844c6554666522e376bd111eb940fbc7c3dad23", + "sha256:037c35f2741ce3a9ac0d55abfcd119133cbd821fffa4461397718287092d9d15", + "sha256:049feae7e9f180b64efacbdc36b3af64a00393a47be22fa9cb6794e68d4e73d3", + "sha256:19228f7940beafc1ba21a6e8e070e0b0bfd1457902a3a81709762b8b9039b88d", + "sha256:2ea681e91e3550a30c2265d2916f40a5f5d89b59469a20f3bad7d07adee0f7a6", + "sha256:3a6b0a78af298d82323660df5497bcea0f0a4a25a0b003afd0ce5af049bd1f60", + "sha256:5385da8f3b801014504df0852bf83524599df890387a3c2b17b7caa3d78b1773", + "sha256:606d8afa07eef77280c2bf84335e24390055b478392e1975f96286d99d0cb424", + "sha256:69245b5b23bbf7fb242c9f8f08493e9ecd7711f063259aefffaeb90595d62287", + "sha256:6f6d839ab09830d59b7fa8fb6917023d8cb5498ee1f1dbd82d37db78eb76bc99", + "sha256:730888475f5ac0e37c1de4bd05eeb799fdb742697867f524dc8a4cd74bcecc23", + "sha256:9819b5162ffc121b9e334923c685b0d0826154e41dfe70b2ede2ce29034c71d8", + "sha256:9e60ef9426efab601dd9aa120e4ff560f4461cf8442e9c0a2b92548d52800699", + "sha256:af5fbdde0690c7da68e841d7fc2632345d570768ea7406a9434446d7b33b0ee1", + "sha256:b64efdbdf3bbb1377562c179f167f3bf301251411eb5ac77dec6b7d32bcda463", + "sha256:bac5f444c118aeb456fac1b0b5d14c6a71ea2a42069b09c176f75e9bd4c186f6", + "sha256:bda9068aafb73859491e13b99b682bd299c1b5fd50644d697533775828a28ee0", + "sha256:d659517ca116e6750101a1326107d3479028c5191f0ecee3c7203c50f5b915b0", + "sha256:eddd3fb1f3e0f82e5915a899285a39ee34ce18fd25d89582bc89fc9fb16cd2c6" + ], + "markers": "python_version >= '3.7' and implementation_name == 'cpython'", + "version": "==1.3.1" + }, + "urllib3": { + "hashes": [ + "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", + "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" + ], + "version": "==1.24.1" + }, + "virtualenv": { + "hashes": [ + "sha256:6aebaf4dd2568a0094225ebbca987859e369e3e5c22dc7d52e5406d504890417", + "sha256:984d7e607b0a5d1329425dd8845bd971b957424b5ba664729fab51ab8c11bc39" + ], + "version": "==16.4.3" + }, + "webencodings": { + "hashes": [ + "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", + "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923" + ], + "version": "==0.5.1" + }, + "wrapt": { + "hashes": [ + "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533" + ], + "version": "==1.11.1" + } + } +} diff --git a/README.md b/README.md new file mode 100644 index 00000000..43732e1a --- /dev/null +++ b/README.md @@ -0,0 +1,30 @@ +# teslajsonpy + +Python module for Tesla API primarily for enabling Home-Assistant. + +**NOTE:** Tesla has no official API; therefore, this library may stop +working at any time without warning. + +# Credits +Originally inspired by [this code.](https://github.com/gglockner/teslajson) +Also thanks to [Tim Dorr](https://tesla-api.timdorr.com/) for documenting the API. Additional repo scaffolding from [simplisafe-python.](https://github.com/bachya/simplisafe-python) + +# Contributing + +1. [Check for open features/bugs](https://github.com/zabuldon/teslajsonpy/issues) + or [initiate a discussion on one](https://github.com/zabuldon/teslajsonpy/issues/new). +2. [Fork the repository](https://github.com/zabuldon/teslajsonpy/fork/new). +3. Install the dev environment: `make init`. +4. Enter the virtual environment: `pipenv shell` +5. Code your new feature or bug fix. +6. Write a test that covers your new functionality. +7. Update `README.md` with any new documentation. +8. Run tests and ensure 100% code coverage for your contribution: `make coverage` +9. Ensure you have no linting errors: `make lint` +10. Ensure you have no typed your code correctly: `make typing` +11. Add yourself to `AUTHORS.md`. +12. Submit a [pull request](https://github.com/zabuldon/teslajsonpy/pulls)! + +# License +[Apache-2.0](LICENSE). By providing a contribution, you agree the contribution is licensed under Apache-2.0. +This code is provided as-is with no warranty. Use at your own risk. diff --git a/README.rst b/README.rst deleted file mode 100644 index 1580557a..00000000 --- a/README.rst +++ /dev/null @@ -1,8 +0,0 @@ -teslajsonpy -=============== - -Python module for Tesla API - -This is a work in progress. - -***** this library based on https://github.com/gglockner/teslajson code diff --git a/pylintrc b/pylintrc new file mode 100644 index 00000000..148c2e5d --- /dev/null +++ b/pylintrc @@ -0,0 +1,13 @@ +[MESSAGES CONTROL] +# Reasons disabled: +# unnecessary-pass - This can hurt readability +# too-many-instance-attributes - This should be refactored later +# duplicate-code - This should be refactored later as architecture has redundant Home-assistant devices. +disable= + unnecessary-pass,too-many-instance-attributes,duplicate-code + +[REPORTS] +reports=no + +[FORMAT] +expected-line-ending-format=LF diff --git a/setup.py b/setup.py index 471d4ca7..a7439e22 100644 --- a/setup.py +++ b/setup.py @@ -1,17 +1,134 @@ -from setuptools import setup +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 + +# Note: To use the "upload" functionality of this file, you must: +# $ pip install twine +# sourced from https://github.com/kennethreitz/setup.py +""" +Python Package for controlling Tesla API. + +For more details about this api, please refer to the documentation at +https://github.com/zabuldon/teslajsonpy +""" +import io +import os +import sys +from shutil import rmtree + +from setuptools import find_packages, setup, Command + +# Package meta-data. +NAME = "teslajsonpy" +DESCRIPTION = "A library to work with Tesla API." +URL = "https://github.com/zabuldon/teslajsonpy" +EMAIL = "sergey.isachenkol@bool.by" +AUTHOR = "Sergey Isachenko" +REQUIRES_PYTHON = ">=3.0" +LICENSE = "Apache-2.0" +VERSION = None + +# What packages are required for this module to be executed? +REQUIRED = [ + 'teslajsonpy', + 'requests' +] + +# What packages are optional? +EXTRAS = { + # "fancy feature": ["django"], +} + +# The rest you shouldn"t have to touch too much :) +# ------------------------------------------------ +# Except, perhaps the License and Trove Classifiers! +# If you do change the License, remember to change the Trove Classifier for +# that! +HERE = os.path.abspath(os.path.dirname(__file__)) + +# Import the README and use it as the long-description. +# Note: this will only work if "README.md" is present in your MANIFEST.in file! +try: + with io.open(os.path.join(HERE, "README.md"), encoding="utf-8") as f: + LONG_DESCRIPTION = "\n" + f.read() +except FileNotFoundError: + LONG_DESCRIPTION = DESCRIPTION + +# Load the package"s __version__.py module as a dictionary. +ABOUT = {} +if not VERSION: + PROJECT_SLUG = NAME.lower().replace("-", "_").replace(" ", "_") + with open(os.path.join(HERE, PROJECT_SLUG, "__version__.py")) as f: + exec(f.read(), ABOUT) # pylint: disable=exec-used +else: + ABOUT["__version__"] = VERSION + + +class UploadCommand(Command): + """Support setup.py upload.""" + + description = "Build and publish the package." + user_options = [] + + @staticmethod + def status(string): + """Print things in bold.""" + print("\033[1m{0}\033[0m".format(string)) + + def initialize_options(self): + """Initialize options.""" + + def finalize_options(self): + """Finalize options.""" + + def run(self): + """Run UploadCommand.""" + try: + self.status("Removing previous builds…") + rmtree(os.path.join(HERE, "dist")) + except OSError: + pass + + self.status("Building Source and Wheel (universal) distribution…") + os.system("{0} setup.py sdist bdist_wheel --universal". + format(sys.executable)) + + self.status("Uploading the package to PyPI via Twine…") + os.system("twine upload dist/*") + + self.status("Pushing git tags…") + os.system("git tag v{0}".format(ABOUT["__version__"])) + os.system("git push --tags") + + sys.exit() + + +# Where the magic happens: setup( - name='teslajsonpy', - version='0.0.25', - packages=['teslajsonpy'], + name=NAME, + version=ABOUT["__version__"], + description=DESCRIPTION, + long_description=LONG_DESCRIPTION, + long_description_content_type="text/markdown", + author=AUTHOR, + author_email=EMAIL, + python_requires=REQUIRES_PYTHON, + url=URL, + packages=find_packages(exclude=("tests",)), + # If your package is a single module, use this instead of "packages": + # py_modules=["mypackage"], + + # entry_points={ + # "console_scripts": ["mycli=mymodule:cli"], + # }, + install_requires=REQUIRED, + extras_require=EXTRAS, include_package_data=True, - python_requires='>=3', - license='WTFPL', - description='A library to work with Tesla API.', - long_description='A library to work with Tesla car API.', - url='https://github.com/zabuldon/teslajsonpy', - author='Sergey Isachenko', - author_email='sergey.isachenkol@bool.by', + license=LICENSE, classifiers=[ + # Trove classifiers + # Full list: https://pypi.python.org/pypi?%3Aaction=list_classifiers + "Development Status :: 3 - Alpha", 'Intended Audience :: Developers', 'Operating System :: OS Independent', 'Programming Language :: Python :: 3', @@ -21,4 +138,8 @@ 'Programming Language :: Python :: 3.7', 'Topic :: Internet', ], + # $ setup.py publish support. + cmdclass={ + "upload": UploadCommand, + }, ) diff --git a/teslajsonpy/BinarySensor.py b/teslajsonpy/BinarySensor.py deleted file mode 100644 index ab1bddf0..00000000 --- a/teslajsonpy/BinarySensor.py +++ /dev/null @@ -1,61 +0,0 @@ -from teslajsonpy.vehicle import VehicleDevice - - -class ParkingSensor(VehicleDevice): - def __init__(self, data, controller): - super().__init__(data, controller) - self.__state = False - - self.type = 'parking brake sensor' - self.hass_type = 'binary_sensor' - - self.name = self._name() - - self.uniq_name = self._uniq_name() - self.bin_type = 0x1 - self.update() - - def update(self): - self._controller.update(self._id, wake_if_asleep=False) - data = self._controller.get_drive_params(self._id) - if data: - if not data['shift_state'] or data['shift_state'] == 'P': - self.__state = True - else: - self.__state = False - - def get_value(self): - return self.__state - - @staticmethod - def has_battery(): - return False - - -class ChargerConnectionSensor(VehicleDevice): - def __init__(self, data, controller): - super().__init__(data, controller) - self.__state = False - - self.type = 'charger sensor' - self.hass_type = 'binary_sensor' - self.name = self._name() - - self.uniq_name = self._uniq_name() - self.bin_type = 0x2 - - def update(self): - self._controller.update(self._id, wake_if_asleep=False) - data = self._controller.get_charging_params(self._id) - if data: - if data['charging_state'] in ["Disconnected", "Stopped", "NoPower"]: - self.__state = False - else: - self.__state = True - - def get_value(self): - return self.__state - - @staticmethod - def has_battery(): - return False diff --git a/teslajsonpy/__init__.py b/teslajsonpy/__init__.py index 24a1e719..93b69614 100644 --- a/teslajsonpy/__init__.py +++ b/teslajsonpy/__init__.py @@ -1,9 +1,28 @@ -from teslajsonpy.BatterySensor import (Battery, Range) -from teslajsonpy.BinarySensor import (ChargerConnectionSensor, ParkingSensor) -from teslajsonpy.Charger import (ChargerSwitch, RangeSwitch) -from teslajsonpy.Climate import (Climate, TempSensor) +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +""" +Python Package for controlling Tesla API. + +For more details about this api, please refer to the documentation at +https://github.com/zabuldon/teslajsonpy +""" +from teslajsonpy.battery_sensor import (Battery, Range) +from teslajsonpy.binary_sensor import (ChargerConnectionSensor, ParkingSensor) +from teslajsonpy.charger import (ChargerSwitch, RangeSwitch) +from teslajsonpy.climate import (Climate, TempSensor) from teslajsonpy.controller import Controller -from teslajsonpy.Exceptions import TeslaException -from teslajsonpy.GPS import GPS, Odometer -from teslajsonpy.Lock import Lock +from teslajsonpy.exceptions import TeslaException +from teslajsonpy.gps import GPS, Odometer +from teslajsonpy.lock import Lock +from .__version__ import __version__ +__all__ = ['Battery', 'Range', + 'ChargerConnectionSensor', 'ParkingSensor', + 'ChargerSwitch', 'RangeSwitch', + 'Climate', 'TempSensor', + 'Controller', + 'TeslaException', + 'GPS', 'Odometer', + 'Lock', + '__version__'] diff --git a/teslajsonpy/__version__.py b/teslajsonpy/__version__.py new file mode 100644 index 00000000..98cf8748 --- /dev/null +++ b/teslajsonpy/__version__.py @@ -0,0 +1,11 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +""" +Python Package for controlling Tesla API. + +For more details about this api, please refer to the documentation at +https://github.com/zabuldon/teslajsonpy +""" + +__version__ = '0.0.26' diff --git a/teslajsonpy/BatterySensor.py b/teslajsonpy/battery_sensor.py similarity index 57% rename from teslajsonpy/BatterySensor.py rename to teslajsonpy/battery_sensor.py index 6106e452..41102827 100644 --- a/teslajsonpy/BatterySensor.py +++ b/teslajsonpy/battery_sensor.py @@ -1,8 +1,34 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +""" +Python Package for controlling Tesla API. + +For more details about this api, please refer to the documentation at +https://github.com/zabuldon/teslajsonpy +""" from teslajsonpy.vehicle import VehicleDevice class Battery(VehicleDevice): + """Home-Assistant battery class for a Tesla VehicleDevice.""" + def __init__(self, data, controller): + """Initialize the Battery sensor. + + Parameters + ---------- + data : dict + The charging parameters for a Tesla vehicle. + https://tesla-api.timdorr.com/vehicle/state/chargestate + controller : teslajsonpy.Controller + The controller that controls updates to the Tesla API. + + Returns + ------- + None + + """ super().__init__(data, controller) self.__battery_level = 0 self.__charging_state = None @@ -16,6 +42,7 @@ def __init__(self, data, controller): self.update() def update(self): + """Update the battery state.""" self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_charging_params(self._id) if data: @@ -24,14 +51,33 @@ def update(self): @staticmethod def has_battery(): + """Return whether the device has a battery.""" return False def get_value(self): + """Return the battery level.""" return self.__battery_level class Range(VehicleDevice): + """Home-Assistant class of the battery range for a Tesla VehicleDevice.""" + def __init__(self, data, controller): + """Initialize the Battery range sensor. + + Parameters + ---------- + data : dict + The charging parameters for a Tesla vehicle. + https://tesla-api.timdorr.com/vehicle/state/chargestate + controller : teslajsonpy.Controller + The controller that controls updates to the Tesla API. + + Returns + ------- + None + + """ super().__init__(data, controller) self.__battery_range = 0 self.__est_battery_range = 0 @@ -46,6 +92,7 @@ def __init__(self, data, controller): self.update() def update(self): + """Update the battery range state.""" self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_charging_params(self._id) if data: @@ -62,10 +109,15 @@ def update(self): @staticmethod def has_battery(): + """Return whether the device has a battery.""" return False def get_value(self): + """Return the battery range. + + This function will return either the rated range or the ideal range + based on the gui_settings. + """ if self.__rated: return self.__battery_range - else: - return self.__ideal_battery_range + return self.__ideal_battery_range diff --git a/teslajsonpy/binary_sensor.py b/teslajsonpy/binary_sensor.py new file mode 100644 index 00000000..56f88b6b --- /dev/null +++ b/teslajsonpy/binary_sensor.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +""" +Python Package for controlling Tesla API. + +For more details about this api, please refer to the documentation at +https://github.com/zabuldon/teslajsonpy +""" +from teslajsonpy.vehicle import VehicleDevice + + +class ParkingSensor(VehicleDevice): + """Home-assistant parking brake class for Tesla vehicles. + + This is intended to be partially inherited by a Home-Assitant entity. + """ + + def __init__(self, data, controller): + """Initialize the parking brake sensor. + + Parameters + ---------- + data : dict + The base state for a Tesla vehicle. + https://tesla-api.timdorr.com/vehicle/state/data + controller : teslajsonpy.Controller + The controller that controls updates to the Tesla API. + + Returns + ------- + None + + """ + super().__init__(data, controller) + self.__state = False + + self.type = 'parking brake sensor' + self.hass_type = 'binary_sensor' + self.sensor_type = 'power' + + self.name = self._name() + + self.uniq_name = self._uniq_name() + self.bin_type = 0x1 + self.update() + + def update(self): + """Update the parking brake sensor.""" + self._controller.update(self._id, wake_if_asleep=False) + data = self._controller.get_drive_params(self._id) + if data: + if not data['shift_state'] or data['shift_state'] == 'P': + self.__state = True + else: + self.__state = False + + def get_value(self): + """Return whether parking brake engaged.""" + return self.__state + + @staticmethod + def has_battery(): + """Return whether the device has a battery.""" + return False + + +class ChargerConnectionSensor(VehicleDevice): + """Home-assistant charger connection class for Tesla vehicles. + + This is intended to be partially inherited by a Home-Assitant entity. + """ + + def __init__(self, data, controller): + """Initialize the charger cable connection sensor. + + Parameters + ---------- + data : dict + The base state for a Tesla vehicle. + https://tesla-api.timdorr.com/vehicle/state/data + controller : teslajsonpy.Controller + The controller that controls updates to the Tesla API. + + Returns + ------- + None + + """ + super().__init__(data, controller) + self.__state = False + + self.type = 'charger sensor' + self.hass_type = 'binary_sensor' + self.name = self._name() + self.sensor_type = 'connectivity' + + self.uniq_name = self._uniq_name() + self.bin_type = 0x2 + + def update(self): + """Update the charger connection sensor.""" + self._controller.update(self._id, wake_if_asleep=False) + data = self._controller.get_charging_params(self._id) + if data: + if data['charging_state'] in ["Disconnected"]: + self.__state = False + else: + self.__state = True + + def get_value(self): + """Return whether the charger cable is connected.""" + return self.__state + + @staticmethod + def has_battery(): + """Return whether the device has a battery.""" + return False diff --git a/teslajsonpy/Charger.py b/teslajsonpy/charger.py similarity index 69% rename from teslajsonpy/Charger.py rename to teslajsonpy/charger.py index e8eba65e..f6bc87bb 100644 --- a/teslajsonpy/Charger.py +++ b/teslajsonpy/charger.py @@ -1,9 +1,36 @@ -from teslajsonpy.vehicle import VehicleDevice +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +""" +Python Package for controlling Tesla API. + +For more details about this api, please refer to the documentation at +https://github.com/zabuldon/teslajsonpy +""" import time +from teslajsonpy.vehicle import VehicleDevice + class ChargerSwitch(VehicleDevice): + """Home-Assistant class for the charger of a Tesla VehicleDevice.""" + def __init__(self, data, controller): + """Initialize the Charger Switch. + + Parameters + ---------- + data : dict + The base state for a Tesla vehicle. + https://tesla-api.timdorr.com/vehicle/state/chargestate + controller : teslajsonpy.Controller + The controller that controls updates to the Tesla API. + + Returns + ------- + None + + """ super().__init__(data, controller) self.__manual_update_time = 0 self.__charger_state = False @@ -15,6 +42,7 @@ def __init__(self, data, controller): self.update() def update(self): + """Update the charging state of the Tesla Vehicle.""" self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_charging_params(self._id) if data and (time.time() - self.__manual_update_time > 60): @@ -24,6 +52,7 @@ def update(self): self.__charger_state = True def start_charge(self): + """Start charging the Tesla Vehicle.""" if not self.__charger_state: data = self._controller.command(self._id, 'charge_start', wake_if_asleep=True) @@ -32,6 +61,7 @@ def start_charge(self): self.__manual_update_time = time.time() def stop_charge(self): + """Stop charging the Tesla Vehicle.""" if self.__charger_state: data = self._controller.command(self._id, 'charge_stop', wake_if_asleep=True) @@ -40,15 +70,20 @@ def stop_charge(self): self.__manual_update_time = time.time() def is_charging(self): + """Return whether the Tesla Vehicle is charging.""" return self.__charger_state @staticmethod def has_battery(): + """Return whether the Tesla charger has a battery.""" return False class RangeSwitch(VehicleDevice): + """Home-Assistant class for setting range limit for charger.""" + def __init__(self, data, controller): + """Initialize the charger range switch.""" super().__init__(data, controller) self.__manual_update_time = 0 self.__maxrange_state = False @@ -60,12 +95,14 @@ def __init__(self, data, controller): self.update() def update(self): + """Update the status of the range setting.""" self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_charging_params(self._id) if data and (time.time() - self.__manual_update_time > 60): self.__maxrange_state = data['charge_to_max_range'] def set_max(self): + """Set the charger to max range for trips.""" if not self.__maxrange_state: data = self._controller.command(self._id, 'charge_max_range', wake_if_asleep=True) @@ -74,6 +111,7 @@ def set_max(self): self.__manual_update_time = time.time() def set_standard(self): + """Set the charger to standard range for daily commute.""" if self.__maxrange_state: data = self._controller.command(self._id, 'charge_standard', wake_if_asleep=True) @@ -82,8 +120,10 @@ def set_standard(self): self.__manual_update_time = time.time() def is_maxrange(self): + """Return whether max range setting is set.""" return self.__maxrange_state @staticmethod def has_battery(): + """Return whether the device has a battery.""" return False diff --git a/teslajsonpy/Climate.py b/teslajsonpy/climate.py similarity index 51% rename from teslajsonpy/Climate.py rename to teslajsonpy/climate.py index 4757e1fa..88b4d0f2 100644 --- a/teslajsonpy/Climate.py +++ b/teslajsonpy/climate.py @@ -1,9 +1,41 @@ -from teslajsonpy.vehicle import VehicleDevice +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +""" +Python Package for controlling Tesla API. + +For more details about this api, please refer to the documentation at +https://github.com/zabuldon/teslajsonpy +""" import time +from teslajsonpy.vehicle import VehicleDevice + class Climate(VehicleDevice): + """Home-assistant class of HVAC for Tesla vehicles. + + This is intended to be partially inherited by a Home-Assitant entity. + """ + def __init__(self, data, controller): + """Initialize the environmental controls. + + Vehicles have both a driver and passenger. + + Parameters + ---------- + data : dict + The base state for a Tesla vehicle. + https://tesla-api.timdorr.com/vehicle/state/data + controller : teslajsonpy.Controller + The controller that controls updates to the Tesla API. + + Returns + ------- + None + + """ super().__init__(data, controller) self.__is_auto_conditioning_on = False self.__inside_temp = 0 @@ -26,34 +58,47 @@ def __init__(self, data, controller): self.update() def is_hvac_enabled(self): + """Return whether HVAC is running.""" return self.__is_climate_on def get_current_temp(self): + """Return vehicle inside temperature.""" return self.__inside_temp def get_goal_temp(self): + """Return driver set temperature.""" return self.__driver_temp_setting def get_fan_status(self): + """Return fan status.""" return self.__fan_status def update(self): + """Update the HVAC state.""" self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_climate_params(self._id) if data: if time.time() - self.__manual_update_time > 60: - self.__is_auto_conditioning_on = data['is_auto_conditioning_on'] + self.__is_auto_conditioning_on = (data + ['is_auto_conditioning_on']) self.__is_climate_on = data['is_climate_on'] - self.__driver_temp_setting = data['driver_temp_setting'] \ - if data['driver_temp_setting'] else self.__driver_temp_setting - self.__passenger_temp_setting = data['passenger_temp_setting'] \ - if data['passenger_temp_setting'] else self.__passenger_temp_setting - self.__inside_temp = data['inside_temp'] if data['inside_temp'] else self.__inside_temp - self.__outside_temp = data['outside_temp'] if data['outside_temp'] else self.__outside_temp + self.__driver_temp_setting = (data['driver_temp_setting'] + if data['driver_temp_setting'] + else self.__driver_temp_setting) + self.__passenger_temp_setting = (data['passenger_temp_setting'] + if + data['passenger_temp_setting'] + else + self.__passenger_temp_setting) + self.__inside_temp = (data['inside_temp'] if data['inside_temp'] + else self.__inside_temp) + self.__outside_temp = (data['outside_temp'] if data['outside_temp'] + else self.__outside_temp) self.__fan_status = data['fan_status'] def set_temperature(self, temp): + """Set both the driver and passenger temperature to temp.""" temp = round(temp, 1) self.__manual_update_time = time.time() data = self._controller.command(self._id, 'set_temps', @@ -65,6 +110,7 @@ def set_temperature(self, temp): self.__passenger_temp_setting = temp def set_status(self, enabled): + """Enable or disable the HVAC.""" self.__manual_update_time = time.time() if enabled: data = self._controller.command(self._id, @@ -84,11 +130,34 @@ def set_status(self, enabled): @staticmethod def has_battery(): + """Return whether the device has a battery.""" return False class TempSensor(VehicleDevice): + """Home-assistant class of temperature sensors for Tesla vehicles. + + This is intended to be partially inherited by a Home-Assitant entity. + """ + def __init__(self, data, controller): + """Initialize the temperature sensors and track in celsius. + + Vehicles have both a driver and passenger. + + Parameters + ---------- + data : dict + The base state for a Tesla vehicle. + https://tesla-api.timdorr.com/vehicle/state/data + controller : teslajsonpy.Controller + The controller that controls updates to the Tesla API. + + Returns + ------- + None + + """ super().__init__(data, controller) self.__inside_temp = 0 self.__outside_temp = 0 @@ -102,18 +171,24 @@ def __init__(self, data, controller): self.update() def get_inside_temp(self): + """Get inside temperature.""" return self.__inside_temp def get_outside_temp(self): + """Get outside temperature.""" return self.__outside_temp def update(self): + """Update the temperature.""" self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_climate_params(self._id) if data: - self.__inside_temp = data['inside_temp'] if data['inside_temp'] else self.__inside_temp - self.__outside_temp = data['outside_temp'] if data['outside_temp'] else self.__outside_temp + self.__inside_temp = (data['inside_temp'] if data['inside_temp'] + else self.__inside_temp) + self.__outside_temp = (data['outside_temp'] if data['outside_temp'] + else self.__outside_temp) @staticmethod def has_battery(): + """Return whether the device has a battery.""" return False diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index e1b83508..a54e9eaa 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -1,22 +1,35 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +""" +Python Package for controlling Tesla API. + +For more details about this api, please refer to the documentation at +https://github.com/zabuldon/teslajsonpy +""" import calendar import datetime -from urllib.parse import urlencode -from urllib.request import Request, build_opener -from urllib.error import HTTPError import json import logging -from teslajsonpy.Exceptions import TeslaException +from urllib.error import HTTPError +from urllib.parse import urlencode +from urllib.request import Request, build_opener + +from teslajsonpy.exceptions import TeslaException + _LOGGER = logging.getLogger(__name__) -class Connection(object): - """Connection to Tesla Motors API""" +class Connection(): + """Connection to Tesla Motors API.""" def __init__(self, email, password): - """Initialize connection object""" + """Initialize connection object.""" self.user_agent = 'Model S 2.1.79 (SM-G900V; Android REL 4.4.4; en_US' - self.client_id = "81527cff06843c8634fdc09e8ac0abefb46ac849f38fe1e431c2ef2106796384" - self.client_secret = "c7257eb71a564034f9419ee651c7d0e5f7aa6bfbd18bafb5c5c033b093bb2fa3" + self.client_id = ("81527cff06843c8634fdc09e8ac0abef" + "b46ac849f38fe1e431c2ef2106796384") + self.client_secret = ("c7257eb71a564034f9419ee651c7d0e5f7" + "aa6bfbd18bafb5c5c033b093bb2fa3") self.baseurl = 'https://owner-api.teslamotors.com' self.api = '/api/1/' self.oauth = { @@ -26,21 +39,24 @@ def __init__(self, email, password): "email": email, "password": password} self.expiration = 0 + self.access_token = None + self.head = None def get(self, command): - """Utility command to get data from API""" + """Get data from API.""" return self.post(command, None) - def post(self, command, data={}): - """Utility command to post data to API""" + def post(self, command, data=None): + """Post data to API.""" now = calendar.timegm(datetime.datetime.now().timetuple()) if now > self.expiration: auth = self.__open("/oauth/token", data=self.oauth) self.__sethead(auth['access_token']) - return self.__open("%s%s" % (self.api, command), headers=self.head, data=data) + return self.__open("%s%s" % (self.api, command), + headers=self.head, data=data) def __sethead(self, access_token): - """Set HTTP header""" + """Set HTTP header.""" self.access_token = access_token now = calendar.timegm(datetime.datetime.now().timetuple()) self.expiration = now + 1800 @@ -48,8 +64,9 @@ def __sethead(self, access_token): "User-Agent": self.user_agent } - def __open(self, url, headers={}, data=None, baseurl=""): - """Raw urlopen command""" + def __open(self, url, headers=None, data=None, baseurl=""): + """Use raw urlopen command.""" + headers = headers or {} if not baseurl: baseurl = self.baseurl req = Request("%s%s" % (baseurl, url), headers=headers) @@ -68,9 +85,8 @@ def __open(self, url, headers={}, data=None, baseurl=""): opener.close() _LOGGER.debug(json.dumps(data)) return data - except HTTPError as e: - if e.code == 408: - _LOGGER.debug("%s", e) + except HTTPError as exception_: + if exception_.code == 408: + _LOGGER.debug("%s", exception_) return False - else: - raise TeslaException(e.code) + raise TeslaException(exception_.code) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 03ea860a..5e776c41 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -1,21 +1,51 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +""" +Python Package for controlling Tesla API. + +For more details about this api, please refer to the documentation at +https://github.com/zabuldon/teslajsonpy +""" +import logging import time +from functools import wraps from multiprocessing import RLock + +from teslajsonpy.battery_sensor import Battery, Range +from teslajsonpy.binary_sensor import ChargerConnectionSensor, ParkingSensor +from teslajsonpy.charger import ChargerSwitch, RangeSwitch +from teslajsonpy.climate import Climate, TempSensor from teslajsonpy.connection import Connection -from teslajsonpy.BatterySensor import Battery, Range -from teslajsonpy.Lock import Lock, ChargerLock -from teslajsonpy.Climate import Climate, TempSensor -from teslajsonpy.BinarySensor import ParkingSensor, ChargerConnectionSensor -from teslajsonpy.Charger import ChargerSwitch, RangeSwitch -from teslajsonpy.GPS import GPS, Odometer -from teslajsonpy.Exceptions import TeslaException -from functools import wraps -import logging -from .Exceptions import RetryLimitError +from teslajsonpy.exceptions import TeslaException, RetryLimitError +from teslajsonpy.gps import GPS, Odometer +from teslajsonpy.lock import ChargerLock, Lock + + _LOGGER = logging.getLogger(__name__) class Controller: + """Controller for connections to Tesla Motors API.""" + def __init__(self, email, password, update_interval): + """Initialize controller. + + Parameters + ---------- + email : string + Email of Tesla account + password : type + Password of Tesla account + update_interval : type + Seconds between allowed updates to the API. This is to prevent + being blocked by Tesla + + Returns + ------- + None + + """ self.__connection = Connection(email, password) self.__vehicles = [] self.update_interval = update_interval @@ -29,7 +59,7 @@ def __init__(self, email, password, update_interval): self._last_wake_up_time = {} # succesful wake_ups by car self._last_attempted_update_time = 0 # all attempts by controller self.__lock = RLock() - self._car_online = {} + self.car_online = {} cars = self.get_vehicles() self._last_attempted_update_time = time.time() @@ -38,7 +68,7 @@ def __init__(self, email, password, update_interval): self._last_update_time[car['id']] = 0 self._last_wake_up_time[car['id']] = 0 self.__update[car['id']] = True - self._car_online[car['id']] = (car['state'] == 'online') + self.car_online[car['id']] = (car['state'] == 'online') self.__climate[car['id']] = False self.__charging[car['id']] = False self.__state[car['id']] = False @@ -62,13 +92,17 @@ def __init__(self, email, password, update_interval): self.__vehicles.append(GPS(car, self)) self.__vehicles.append(Odometer(car, self)) - def wake_up(f): - """Wraps a API f so it will attempt to wake the vehicle if asleep. + def wake_up(func): + # pylint: disable=no-self-argument + # issue is use of wraps on classmethods which should be replaced: + # https://hynek.me/articles/decorators/ + """Wrap a API f so it will attempt to wake the vehicle if asleep. The command f is run once if the vehicle_id was last reported online. Assuming f returns None and wake_if_asleep is True, 5 attempts will be made to wake the vehicle to reissue the command. In addition, if there is a `could_not_wake_buses` error, it will retry the command + Args: inst (Controller): The instance of a controller vehicle_id (string): The vehicle to attempt to wake. @@ -79,10 +113,11 @@ def wake_up(f): Throws: RetryLimitError """ - @wraps(f) + @wraps(func) def wrapped(*args, **kwargs): + # pylint: disable=too-many-branches,protected-access, not-callable def valid_result(result): - """Is TeslaAPI result succesful. + """Check if TeslaAPI result succesful. Parameters ---------- @@ -96,6 +131,7 @@ def valid_result(result): ['response']['result'], a bool, or None or ['response']['reason'] == 'could_not_wake_buses' Returns true when a failure state not detected. + """ try: return (result is not None and result is not False and @@ -115,36 +151,40 @@ def valid_result(result): inst = args[0] vehicle_id = args[1] result = None - if (vehicle_id is not None and vehicle_id in inst._car_online and - inst._car_online[vehicle_id]): + if (vehicle_id is not None and vehicle_id in inst.car_online and + inst.car_online[vehicle_id]): try: - result = f(*args, **kwargs) + result = func(*args, **kwargs) except TeslaException: pass if valid_result(result): return result - _LOGGER.debug("Wrapped %s -> %s \n" - "Additional info: args:%s, kwargs:%s, " - "vehicle_id:%s, _car_online:%s", - f.__name__, result, args, kwargs, vehicle_id, - inst._car_online) - inst._car_online[vehicle_id] = False + _LOGGER.debug("wake_up needed for %s -> %s \n" + "Info: args:%s, kwargs:%s, " + "vehicle_id:%s, car_online:%s", + func.__name__, # pylint: disable=no-member + result, args, kwargs, vehicle_id, + inst.car_online) + inst.car_online[vehicle_id] = False while ('wake_if_asleep' in kwargs and kwargs['wake_if_asleep'] and # Check online state (vehicle_id is None or (vehicle_id is not None and - vehicle_id in inst._car_online and - not inst._car_online[vehicle_id]))): - result = inst._wake_up(vehicle_id, *args, **kwargs) - _LOGGER.debug("Wake Attempt(%s): %s", retries, result) + vehicle_id in inst.car_online and + not inst.car_online[vehicle_id]))): + result = inst._wake_up(vehicle_id) + _LOGGER.debug("%s(%s): Wake Attempt(%s): %s", + func.__name__, # pylint: disable=no-member, + vehicle_id, + retries, result) if not result: if retries < 5: time.sleep(sleep_delay**(retries+2)) retries += 1 continue else: - inst._car_online[vehicle_id] = False + inst.car_online[vehicle_id] = False raise RetryLimitError else: break @@ -152,8 +192,11 @@ def valid_result(result): retries = 0 while True: try: - result = f(*args, **kwargs) - _LOGGER.debug("Retry Attempt(%s): %s", retries, result) + result = func(*args, **kwargs) + _LOGGER.debug("%s(%s): Retry Attempt(%s): %s", + func.__name__, # pylint: disable=no-member, + vehicle_id, + retries, result) except TeslaException: pass finally: @@ -161,49 +204,152 @@ def valid_result(result): time.sleep(sleep_delay**(retries+1)) if valid_result(result): return result - elif retries >= 5: + if retries >= 5: raise RetryLimitError return wrapped def get_vehicles(self): + """Get vehicles json from TeslaAPI.""" return self.__connection.get('vehicles')['response'] @wake_up - def post(self, vehicle_id, command, data={}, wake_if_asleep=True): + def post(self, vehicle_id, command, data=None, wake_if_asleep=True): + # pylint: disable=unused-argument + """Send post command to the vehicle_id. + + This is a wrapped function by wake_up. + + Parameters + ---------- + vehicle_id : string + Identifier for the car on the owner-api endpoint. Confusingly it + is not the vehicle_id field for identifying the car across + different endpoints. + https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id + command : string + Tesla API command. https://tesla-api.timdorr.com/vehicle/commands + data : dict + Optional parameters. + wake_if_asleep : bool + Function for wake_up decorator indicating whether a failed response + should wake up the vehicle or retry. + + Returns + ------- + dict + Tesla json object. + + """ + data = data or {} return self.__connection.post('vehicles/%i/%s' % (vehicle_id, command), data) @wake_up def get(self, vehicle_id, command, wake_if_asleep=False): + # pylint: disable=unused-argument + """Send get command to the vehicle_id. + + This is a wrapped function by wake_up. + + Parameters + ---------- + vehicle_id : string + Identifier for the car on the owner-api endpoint. Confusingly it + is not the vehicle_id field for identifying the car across + different endpoints. + https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id + command : string + Tesla API command. https://tesla-api.timdorr.com/vehicle/commands + wake_if_asleep : bool + Function for wake_up decorator indicating whether a failed response + should wake up the vehicle or retry. + + Returns + ------- + dict + Tesla json object. + + """ return self.__connection.get('vehicles/%i/%s' % (vehicle_id, command)) def data_request(self, vehicle_id, name, wake_if_asleep=False): - return self.get(vehicle_id, 'data_request/%s' % name, - wake_if_asleep=False)['response'] + """Get requested data from vehicle_id. - def command(self, vehicle_id, name, data={}, wake_if_asleep=True): + Parameters + ---------- + vehicle_id : string + Identifier for the car on the owner-api endpoint. Confusingly it + is not the vehicle_id field for identifying the car across + different endpoints. + https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id + name: string + Name of data to be requested from the data_request endpoint which + rolls ups all data plus vehicle configuration. + https://tesla-api.timdorr.com/vehicle/state/data + wake_if_asleep : bool + Function for underlying api call for whether a failed response + should wake up the vehicle or retry. + + Returns + ------- + dict + Tesla json object. + + """ + return self.get(vehicle_id, 'vehicle_data/%s' % name, + wake_if_asleep=wake_if_asleep)['response'] + + def command(self, vehicle_id, name, data=None, wake_if_asleep=True): + """Post name command to the vehicle_id. + + Parameters + ---------- + vehicle_id : string + Identifier for the car on the owner-api endpoint. Confusingly it + is not the vehicle_id field for identifying the car across + different endpoints. + https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id + name : string + Tesla API command. https://tesla-api.timdorr.com/vehicle/commands + data : dict + Optional parameters. + wake_if_asleep : bool + Function for underlying api call for whether a failed response + should wake up the vehicle or retry. + + Returns + ------- + dict + Tesla json object. + + """ + data = data or {} return self.post(vehicle_id, 'command/%s' % name, data, - wake_if_asleep=True) + wake_if_asleep=wake_if_asleep) def list_vehicles(self): + """Return list of Tesla components for Home Assistant setup. + + Use get_vehicles() for general API use. + """ return self.__vehicles - def _wake_up(self, vehicle_id, *args, **kwargs): + def _wake_up(self, vehicle_id): cur_time = int(time.time()) - if (not self._car_online[vehicle_id] or + if (not self.car_online[vehicle_id] or (cur_time - self._last_wake_up_time[vehicle_id] > 300)): result = self.post(vehicle_id, 'wake_up', wake_if_asleep=False) # avoid wrapper loop - self._car_online[vehicle_id] = (result['response']['state'] == - 'online') + self.car_online[vehicle_id] = (result['response']['state'] == + 'online') self._last_wake_up_time[vehicle_id] = cur_time _LOGGER.debug("Wakeup %s: %s", vehicle_id, result['response']['state']) - return self._car_online[vehicle_id] + return self.car_online[vehicle_id] def update(self, car_id=None, wake_if_asleep=False, force=False): - """Updates all vehicle attributes in the cache. + """Update all vehicle attributes in the cache. This command will connect to the Tesla API and first update the list of online vehicles assuming no attempt for at least the [update_interval]. @@ -220,8 +366,10 @@ def update(self, car_id=None, wake_if_asleep=False, force=False): Returns: True if any update succeeded for any vehicle else false + Throws: RetryLimitError + """ cur_time = time.time() with self.__lock: @@ -230,25 +378,25 @@ def update(self, car_id=None, wake_if_asleep=False, force=False): if (force or cur_time - last_update > self.update_interval): cars = self.get_vehicles() for car in cars: - self._car_online[car['id']] = (car['state'] == 'online') + self.car_online[car['id']] = (car['state'] == 'online') self._last_attempted_update_time = cur_time # Only update online vehicles that haven't been updated recently # The throttling is per car's last succesful update # Note: This separate check is because there may be individual cars # to update. update_succeeded = False - for id, v in self._car_online.items(): + for id_, value in self.car_online.items(): # If specific car_id provided, only update match - if (car_id is not None and car_id != id): + if (car_id is not None and car_id != id_): continue - if (v and - (id in self.__update and self.__update[id]) and - (force or id not in self._last_update_time or - ((cur_time - self._last_update_time[id]) > - self.update_interval))): + if (value and # pylint: disable=too-many-boolean-expressions + (id_ in self.__update and self.__update[id_]) and + (force or id_ not in self._last_update_time or + ((cur_time - self._last_update_time[id_]) > + self.update_interval))): # Only update cars with update flag on try: - data = self.get(id, 'data', wake_if_asleep) + data = self.get(id_, 'data', wake_if_asleep) except TeslaException: data = None if data and data['response']: @@ -258,32 +406,70 @@ def update(self, car_id=None, wake_if_asleep=False, force=False): self.__state[car_id] = response['vehicle_state'] self.__driving[car_id] = response['drive_state'] self.__gui[car_id] = response['gui_settings'] - self._car_online[car_id] = (response['state'] - == 'online') + self.car_online[car_id] = (response['state'] + == 'online') self._last_update_time[car_id] = time.time() update_succeeded = True return update_succeeded def get_climate_params(self, car_id): + """Return cached copy of climate_params for car_id.""" return self.__climate[car_id] def get_charging_params(self, car_id): + """Return cached copy of charging_params for car_id.""" return self.__charging[car_id] def get_state_params(self, car_id): + """Return cached copy of state_params for car_id.""" return self.__state[car_id] def get_drive_params(self, car_id): + """Return cached copy of drive_params for car_id.""" return self.__driving[car_id] def get_gui_params(self, car_id): + """Return cached copy of gui_params for car_id.""" return self.__gui[car_id] def get_updates(self, car_id=None): + """Get updates dictionary. + + Parameters + ---------- + car_id : string + Identifier for the car on the owner-api endpoint. Confusingly it + is not the vehicle_id field for identifying the car across + different endpoints. + https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id + If no car_id, returns the complete dictionary. + + Returns + ------- + bool or dict of booleans + If car_id exists, a bool indicating whether updates should be + procssed. Othewise, the entire updates dictionary. + + """ if car_id is not None: return self.__update[car_id] - else: - return self.__update + return self.__update def set_updates(self, car_id, value): + """Set updates dictionary. + + Parameters + ---------- + car_id : string + Identifier for the car on the owner-api endpoint. Confusingly it + is not the vehicle_id field for identifying the car across + different endpoints. + https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id + value : bool + Whether the specific car_id should be updated. + Returns + ------- + None + + """ self.__update[car_id] = value diff --git a/teslajsonpy/Exceptions.py b/teslajsonpy/exceptions.py similarity index 59% rename from teslajsonpy/Exceptions.py rename to teslajsonpy/exceptions.py index 082fbe44..ffbaaa66 100644 --- a/teslajsonpy/Exceptions.py +++ b/teslajsonpy/exceptions.py @@ -1,5 +1,19 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +""" +Python Package for controlling Tesla API. + +For more details about this api, please refer to the documentation at +https://github.com/zabuldon/teslajsonpy +""" + + class TeslaException(Exception): - def __init__(self, code, *args, **kwargs): + """Class of Tesla API exceptions.""" + + def __init__(self, code, *args, **kwargs): + """Initialize exceptions for the Tesla API.""" self.message = "" super().__init__(*args, **kwargs) self.code = code @@ -20,6 +34,11 @@ def __init__(self, code, *args, **kwargs): elif self.code > 299: self.message = "UNKNOWN_ERROR" + class RetryLimitError(TeslaException): + """Class of exceptions for hitting retry limits.""" + def __init__(self, *args, **kwargs): + # pylint: disable=super-init-not-called + """Initialize exceptions for the Tesla retry limit API.""" pass diff --git a/teslajsonpy/GPS.py b/teslajsonpy/gps.py similarity index 60% rename from teslajsonpy/GPS.py rename to teslajsonpy/gps.py index 24c6b940..bc741849 100644 --- a/teslajsonpy/GPS.py +++ b/teslajsonpy/gps.py @@ -1,8 +1,34 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +""" +Python Package for controlling Tesla API. + +For more details about this api, please refer to the documentation at +https://github.com/zabuldon/teslajsonpy +""" from teslajsonpy.vehicle import VehicleDevice class GPS(VehicleDevice): + """Home-assistant class for GPS of Tesla vehicles.""" + def __init__(self, data, controller): + """Initialize the Vehicle's GPS information. + + Parameters + ---------- + data : dict + The base state for a Tesla vehicle. + https://tesla-api.timdorr.com/vehicle/state/data + controller : teslajsonpy.Controller + The controller that controls updates to the Tesla API. + + Returns + ------- + None + + """ super().__init__(data, controller) self.__longitude = 0 self.__latitude = 0 @@ -21,9 +47,11 @@ def __init__(self, data, controller): self.update() def get_location(self): + """Return the current location.""" return self.__location def update(self): + """Update the current GPS location.""" self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_drive_params(self._id) if data: @@ -37,11 +65,29 @@ def update(self): @staticmethod def has_battery(): + """Return whether the device has a battery.""" return False class Odometer(VehicleDevice): + """Home-assistant class for odometer of Tesla vehicles.""" + def __init__(self, data, controller): + """Initialize the Vehicle's odometer information. + + Parameters + ---------- + data : dict + The base state for a Tesla vehicle. + https://tesla-api.timdorr.com/vehicle/state/data + controller : teslajsonpy.Controller + The controller that controls updates to the Tesla API. + + Returns + ------- + None + + """ super().__init__(data, controller) self.__odometer = 0 self.type = 'mileage sensor' @@ -54,6 +100,7 @@ def __init__(self, data, controller): self.__rated = True def update(self): + """Update the odometer and the unit of measurement based on GUI.""" self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_state_params(self._id) if data: @@ -68,7 +115,9 @@ def update(self): @staticmethod def has_battery(): + """Return whether the device has a battery.""" return False def get_value(self): + """Return the odometer reading.""" return round(self.__odometer, 1) diff --git a/teslajsonpy/Lock.py b/teslajsonpy/lock.py similarity index 61% rename from teslajsonpy/Lock.py rename to teslajsonpy/lock.py index cc27e26b..40162323 100644 --- a/teslajsonpy/Lock.py +++ b/teslajsonpy/lock.py @@ -1,9 +1,39 @@ -from teslajsonpy.vehicle import VehicleDevice +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +""" +Python Package for controlling Tesla API. + +For more details about this api, please refer to the documentation at +https://github.com/zabuldon/teslajsonpy +""" import time +from teslajsonpy.vehicle import VehicleDevice + class Lock(VehicleDevice): + """Home-assistant lock class for Tesla vehicles. + + This is intended to be partially inherited by a Home-Assitant entity. + """ + def __init__(self, data, controller): + """Initialize the locks for the vehicle. + + Parameters + ---------- + data : dict + The base state for a Tesla vehicle. + https://tesla-api.timdorr.com/vehicle/state/data + controller : teslajsonpy.Controller + The controller that controls updates to the Tesla API. + + Returns + ------- + None + + """ super().__init__(data, controller) self.__manual_update_time = 0 self.__lock_state = False @@ -18,12 +48,14 @@ def __init__(self, data, controller): self.update() def update(self): + """Update the lock state.""" self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_state_params(self._id) if data and (time.time() - self.__manual_update_time > 60): self.__lock_state = data['locked'] def lock(self): + """Lock the doors.""" if not self.__lock_state: data = self._controller.command(self._id, 'door_lock', wake_if_asleep=True) @@ -32,6 +64,7 @@ def lock(self): self.__manual_update_time = time.time() def unlock(self): + """Unlock the doors and extend handles where applicable.""" if self.__lock_state: data = self._controller.command(self._id, 'door_unlock', wake_if_asleep=True) @@ -40,15 +73,37 @@ def unlock(self): self.__manual_update_time = time.time() def is_locked(self): + """Return whether doors are locked.""" return self.__lock_state @staticmethod def has_battery(): + """Return whether the device has a battery.""" return False class ChargerLock(VehicleDevice): + """Home-assistant lock class for the charger of Tesla vehicles. + + This is intended to be partially inherited by a Home-Assitant entity. + """ + def __init__(self, data, controller): + """Initialize the charger lock for the vehicle. + + Parameters + ---------- + data : dict + The base state for a Tesla vehicle. + https://tesla-api.timdorr.com/vehicle/state/data + controller : teslajsonpy.Controller + The controller that controls updates to the Tesla API. + + Returns + ------- + None + + """ super().__init__(data, controller) self.__manual_update_time = 0 self.__lock_state = False @@ -63,12 +118,15 @@ def __init__(self, data, controller): self.update() def update(self): + """Update state of the charger lock.""" self._controller.update(self._id, wake_if_asleep=False) data = self._controller.get_charging_params(self._id) if data and (time.time() - self.__manual_update_time > 60): - self.__lock_state = not ((data['charge_port_door_open']) and (data['charge_port_door_open']) and (data['charge_port_latch'] != 'Engaged')) + self.__lock_state = not ((data['charge_port_door_open']) and + (data['charge_port_latch'] != 'Engaged')) def lock(self): + """Close the charger door.""" if not self.__lock_state: data = self._controller.command(self._id, 'charge_port_door_close', wake_if_asleep=True) @@ -77,6 +135,7 @@ def lock(self): self.__manual_update_time = time.time() def unlock(self): + """Open the charger door.""" if self.__lock_state: data = self._controller.command(self._id, 'charge_port_door_open', wake_if_asleep=True) @@ -85,8 +144,10 @@ def unlock(self): self.__manual_update_time = time.time() def is_locked(self): + """Return whether the charger is closed.""" return self.__lock_state @staticmethod def has_battery(): + """Return whether the device has a battery.""" return False diff --git a/teslajsonpy/vehicle.py b/teslajsonpy/vehicle.py index c8a10e34..3b455b0c 100644 --- a/teslajsonpy/vehicle.py +++ b/teslajsonpy/vehicle.py @@ -1,33 +1,75 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# SPDX-License-Identifier: Apache-2.0 +""" +Python Package for controlling Tesla API. + +For more details about this api, please refer to the documentation at +https://github.com/zabuldon/teslajsonpy +""" + + class VehicleDevice: + """Home-assistant class of Tesla vehicles. + + This is intended to be partially inherited by a Home-Assitant entity. + """ + def __init__(self, data, controller): + """Initialize the Vehicle. + + Parameters + ---------- + data : dict + The base state for a Tesla vehicle. + https://tesla-api.timdorr.com/vehicle/state/data + controller : teslajsonpy.Controller + The controller that controls updates to the Tesla API. + + Returns + ------- + None + + """ self._id = data['id'] self._vehicle_id = data['vehicle_id'] + self._display_name = data['display_name'] self._vin = data['vin'] self._state = data['state'] self._controller = controller self.should_poll = True + self.type = "device" def _name(self): - return 'Tesla Model {} {}'.format( - str(self._vin[3]).upper(), self.type) + return ('{} {}'.format(self._display_name, self.type) if + self._display_name is not None and + self._display_name != self._vin[-6:] + else 'Tesla Model {} {}'.format(str(self._vin[3]).upper(), + self.type)) def _uniq_name(self): return 'Tesla Model {} {} {}'.format( - str(self._vin[3]).upper(), self._vin, self.type) + str(self._vin[3]).upper(), self._vin[-6:], self.type) def id(self): + # pylint: disable=invalid-name + """Return the id of this Vehicle.""" return self._id def assumed_state(self): - return (not self._controller._car_online[self.id()] and + # pylint: disable=protected-access + """Return whether the data is from an online vehicle.""" + return (not self._controller.car_online[self.id()] and (self._controller._last_update_time[self.id()] - self._controller._last_wake_up_time[self.id()] > self._controller.update_interval)) @staticmethod def is_armable(): + """Return whether the data is from an online vehicle.""" return False @staticmethod def is_armed(): + """Return whether the vehicle is armed.""" return False diff --git a/tox.ini b/tox.ini new file mode 100644 index 00000000..829e62d2 --- /dev/null +++ b/tox.ini @@ -0,0 +1,31 @@ +[tox] +envlist = py36, py37, py38, cov, lint, typing +skip_missing_interpreters = True + +[testenv] +whitelist_externals = make +deps = pipenv +commands= + make init + make test + +[testenv:cov] +whitelist_externals = make +deps = pipenv +commands= + make init + make coverage + +[testenv:lint] +whitelist_externals = make +deps = pipenv +commands= + make init + make lint + +[testenv:typing] +whitelist_externals = make +deps = pipenv +commands= + make init + make typing