From 673ecdb5c9483160fb1b97e30e62f2c863761c39 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Fri, 29 Mar 2019 02:18:02 -0700 Subject: [PATCH] Update repo documents and complete linting (#41) * Truncate VIN and add display_name **breaks HA entities** * Add sensor_type * Fix ChargerConnectionSensor to reflect cable connection * Begin adding test suites and linting * Rename filenames to fix pylint snake_case errors * Fix pylint errors and add initial linting * Flesh out documentation and setup.py * Fix import errors caused by case rename * Fix call for built-in id instead of id_ * Add more verbose debugging for wrapper * Cleanup redundant import * Fix bug where list of online vehicles not updated (#39) * Bump version to 0.0.25 (#40) * Change to Apache-2.0 from WTFPL * Bump version to 0.0.26 --- .coveragerc | 7 + .gitignore | 118 ++++- AUTHORS.md | 14 + LICENSE | 206 +++++++- Makefile | 27 + Pipfile | 16 + Pipfile.lock | 476 ++++++++++++++++++ README.md | 30 ++ README.rst | 8 - pylintrc | 13 + setup.py | 143 +++++- teslajsonpy/BinarySensor.py | 61 --- teslajsonpy/__init__.py | 33 +- teslajsonpy/__version__.py | 11 + .../{BatterySensor.py => battery_sensor.py} | 56 ++- teslajsonpy/binary_sensor.py | 118 +++++ teslajsonpy/{Charger.py => charger.py} | 42 +- teslajsonpy/{Climate.py => climate.py} | 95 +++- teslajsonpy/connection.py | 58 ++- teslajsonpy/controller.py | 300 ++++++++--- teslajsonpy/{Exceptions.py => exceptions.py} | 21 +- teslajsonpy/{GPS.py => gps.py} | 49 ++ teslajsonpy/{Lock.py => lock.py} | 65 ++- teslajsonpy/vehicle.py | 50 +- tox.ini | 31 ++ 25 files changed, 1841 insertions(+), 207 deletions(-) create mode 100644 .coveragerc create mode 100644 AUTHORS.md create mode 100644 Makefile create mode 100644 Pipfile create mode 100644 Pipfile.lock create mode 100644 README.md delete mode 100644 README.rst create mode 100644 pylintrc delete mode 100644 teslajsonpy/BinarySensor.py create mode 100644 teslajsonpy/__version__.py rename teslajsonpy/{BatterySensor.py => battery_sensor.py} (57%) create mode 100644 teslajsonpy/binary_sensor.py rename teslajsonpy/{Charger.py => charger.py} (69%) rename teslajsonpy/{Climate.py => climate.py} (51%) rename teslajsonpy/{Exceptions.py => exceptions.py} (59%) rename teslajsonpy/{GPS.py => gps.py} (60%) rename teslajsonpy/{Lock.py => lock.py} (61%) create mode 100644 tox.ini 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