From d8a4d9e9fbcfe0cef9b83adf658000acd1d15c71 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 14 Aug 2022 18:44:44 -0700 Subject: [PATCH 01/84] Fix power sensor naming --- poetry.lock | 537 ++++++++---------- teslajsonpy/controller.py | 9 + teslajsonpy/homeassistant/power.py | 2 +- tests/tesla_mock.py | 142 +++-- .../homeassistant/test_power_sensor.py | 19 +- 5 files changed, 355 insertions(+), 354 deletions(-) diff --git a/poetry.lock b/poetry.lock index dc1b00d6..3f0b74fb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -100,17 +100,17 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "attrs" -version = "21.4.0" +version = "22.1.0" description = "Classes Without Boilerplate" category = "main" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] +dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"] +tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] +tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] [[package]] name = "authcaptureproxy" @@ -238,7 +238,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.4.1" +version = "6.4.3" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -263,7 +263,7 @@ graph = ["objgraph (>=1.7.2)"] [[package]] name = "distlib" -version = "0.3.4" +version = "0.3.5" description = "Distribution utilities" category = "dev" optional = false @@ -271,23 +271,23 @@ python-versions = "*" [[package]] name = "docutils" -version = "0.17.1" +version = "0.19" description = "Docutils -- Python Documentation Utilities" category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +python-versions = ">=3.7" [[package]] name = "filelock" -version = "3.7.1" +version = "3.8.0" description = "A platform independent file lock." category = "dev" optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] -testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] +docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] +testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] [[package]] name = "flake8" @@ -305,7 +305,7 @@ pyflakes = ">=2.3.0,<2.4.0" [[package]] name = "frozenlist" -version = "1.3.0" +version = "1.3.1" description = "A list-like structure which implements collections.abc.MutableSequence" category = "main" optional = false @@ -436,14 +436,14 @@ python-versions = ">=3.6" [[package]] name = "m2r2" -version = "0.3.2" +version = "0.3.3" description = "Markdown and reStructuredText in a single file." category = "dev" optional = false python-versions = "*" [package.dependencies] -docutils = "*" +docutils = ">=0.19" mistune = "0.8.4" [[package]] @@ -480,7 +480,7 @@ python-versions = ">=3.7" [[package]] name = "mypy" -version = "0.961" +version = "0.971" description = "Optional static typing for Python" category = "dev" optional = false @@ -548,8 +548,8 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] -dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] +testing = ["pytest-benchmark", "pytest"] +dev = ["tox", "pre-commit"] [[package]] name = "py" @@ -653,7 +653,7 @@ testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2. [[package]] name = "pytest-asyncio" -version = "0.18.3" +version = "0.19.0" description = "Pytest support for asyncio" category = "dev" optional = false @@ -664,7 +664,7 @@ pytest = ">=6.1.0" typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] -testing = ["coverage (==6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (==0.931)", "pytest-trio (>=0.7.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "pytest-cov" @@ -679,11 +679,11 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"] +testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] [[package]] name = "pytz" -version = "2022.1" +version = "2022.2.1" description = "World timezone definitions, modern and historical" category = "dev" optional = false @@ -763,7 +763,7 @@ python-versions = ">=3.6" [[package]] name = "sphinx" -version = "5.0.2" +version = "5.1.1" description = "Python documentation generator" category = "dev" optional = false @@ -773,7 +773,7 @@ python-versions = ">=3.6" alabaster = ">=0.7,<0.8" babel = ">=1.3" colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} -docutils = ">=0.14,<0.19" +docutils = ">=0.14,<0.20" imagesize = "*" importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} Jinja2 = ">=2.3" @@ -790,16 +790,16 @@ sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "isort", "mypy (>=0.950)", "docutils-stubs", "types-typed-ast", "types-requests"] +lint = ["flake8 (>=3.5.0)", "flake8-comprehensions", "flake8-bugbear", "isort", "mypy (>=0.971)", "sphinx-lint", "docutils-stubs", "types-typed-ast", "types-requests"] test = ["pytest (>=4.6)", "html5lib", "cython", "typed-ast"] [[package]] name = "sphinx-autoapi" -version = "1.8.4" +version = "1.9.0" description = "Sphinx API documentation generator" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] astroid = ">=2.7" @@ -825,23 +825,22 @@ python-versions = ">=3.6" sphinx = ">=1.8" [package.extras] +rtd = ["sphinx-book-theme", "myst-nb", "ipython", "sphinx"] code_style = ["pre-commit (==2.12.1)"] -rtd = ["sphinx", "ipython", "myst-nb", "sphinx-book-theme"] [[package]] name = "sphinx-rtd-theme" -version = "1.0.0" +version = "0.5.1" description = "Read the Docs theme for Sphinx" category = "dev" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*" +python-versions = "*" [package.dependencies] -docutils = "<0.18" -sphinx = ">=1.6" +sphinx = "*" [package.extras] -dev = ["transifex-client", "sphinxcontrib-httpdomain", "bump2version"] +dev = ["bump2version", "sphinxcontrib-httpdomain", "transifex-client"] [[package]] name = "sphinxcontrib-applehelp" @@ -852,8 +851,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-devhelp" @@ -864,8 +863,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-htmlhelp" @@ -876,8 +875,8 @@ optional = false python-versions = ">=3.6" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] -test = ["pytest", "html5lib"] +test = ["html5lib", "pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-jsmath" @@ -888,7 +887,7 @@ optional = false python-versions = ">=3.5" [package.extras] -test = ["pytest", "flake8", "mypy"] +test = ["mypy", "flake8", "pytest"] [[package]] name = "sphinxcontrib-qthelp" @@ -899,8 +898,8 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] test = ["pytest"] +lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-serializinghtml" @@ -963,7 +962,7 @@ python-versions = ">=3.6" [[package]] name = "typer" -version = "0.5.0" +version = "0.6.1" description = "Typer, build great CLIs. Easy to code. Based on Python type hints." category = "main" optional = false @@ -973,10 +972,10 @@ python-versions = ">=3.6" click = ">=7.1.1,<9.0.0" [package.extras] -all = ["colorama (>=0.4.3,<0.5.0)", "shellingham (>=1.3.0,<2.0.0)", "rich (>=10.11.0,<13.0.0)"] -dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] -doc = ["mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mdx-include (>=1.4.1,<2.0.0)"] -test = ["shellingham (>=1.3.0,<2.0.0)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "coverage (>=5.2,<6.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "mypy (==0.910)", "black (>=22.3.0,<23.0.0)", "isort (>=5.0.6,<6.0.0)", "rich (>=10.11.0,<13.0.0)"] +test = ["rich (>=10.11.0,<13.0.0)", "isort (>=5.0.6,<6.0.0)", "black (>=22.3.0,<23.0.0)", "mypy (==0.910)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "coverage (>=5.2,<6.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest (>=4.4.0,<5.4.0)", "shellingham (>=1.3.0,<2.0.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)"] +dev = ["pre-commit (>=2.17.0,<3.0.0)", "flake8 (>=3.8.3,<4.0.0)", "autoflake (>=1.3.1,<2.0.0)"] +all = ["rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)", "colorama (>=0.4.3,<0.5.0)"] [[package]] name = "typing-extensions" @@ -996,7 +995,7 @@ python-versions = ">=3.5" [[package]] name = "urllib3" -version = "1.26.10" +version = "1.26.11" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false @@ -1009,22 +1008,21 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.15.1" +version = "20.16.3" description = "Virtual Python Environment builder" category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.6" [package.dependencies] -distlib = ">=0.3.1,<1" -filelock = ">=3.2,<4" -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} -platformdirs = ">=2,<3" -six = ">=1.9.0,<2" +distlib = ">=0.3.5,<1" +filelock = ">=3.4.1,<4" +importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} +platformdirs = ">=2.4,<3" [package.extras] -docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] -testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)", "packaging (>=20.0)"] +docs = ["proselint (>=0.13)", "sphinx (>=5.1.1)", "sphinx-argparse (>=0.3.1)", "sphinx-rtd-theme (>=1)", "towncrier (>=21.9)"] +testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] [[package]] name = "wrapt" @@ -1036,11 +1034,11 @@ python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" [[package]] name = "yarl" -version = "1.7.2" +version = "1.8.1" description = "Yet another URL library" category = "main" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] idna = ">=2.0" @@ -1049,15 +1047,15 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [[package]] name = "zipp" -version = "3.8.0" +version = "3.8.1" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] +testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" @@ -1151,7 +1149,10 @@ anyio = [ {file = "anyio-3.6.1-py3-none-any.whl", hash = "sha256:cb29b9c70620506a9a8f87a309591713446953302d7d995344d0d7c6c0c9a7be"}, {file = "anyio-3.6.1.tar.gz", hash = "sha256:413adf95f93886e442aea925f3ee43baa5a765a64a0f52c6081894f9992fdd0b"}, ] -astroid = [] +astroid = [ + {file = "astroid-2.11.7-py3-none-any.whl", hash = "sha256:86b0a340a512c65abf4368b80252754cda17c02cdbbd3f587dddf98112233e7b"}, + {file = "astroid-2.11.7.tar.gz", hash = "sha256:bb24615c77f4837c707669d16907331374ae8a964650a66999da3f5ca68dc946"}, +] async-timeout = [ {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, @@ -1160,10 +1161,12 @@ asynctest = [ {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, ] -atomicwrites = [] +atomicwrites = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] attrs = [ - {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, - {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, + {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, + {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, ] authcaptureproxy = [ {file = "authcaptureproxy-1.1.4-py3-none-any.whl", hash = "sha256:efd1a3077957e3d0269dacbc7c1252b47c4f711e22ce1474ec86f5e1cd584d60"}, @@ -1184,12 +1187,39 @@ beautifulsoup4 = [ {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, ] -black = [] +black = [ + {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, + {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, + {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, + {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"}, + {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"}, + {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"}, + {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"}, + {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"}, + {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"}, + {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"}, + {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"}, + {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"}, + {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"}, + {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"}, + {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"}, + {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"}, + {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"}, + {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"}, + {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"}, + {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"}, + {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"}, + {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"}, + {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"}, +] certifi = [ {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, ] -charset-normalizer = [] +charset-normalizer = [ + {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, + {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, +] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, @@ -1198,127 +1228,19 @@ colorama = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] -coverage = [ - {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"}, - {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"}, - {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"}, - {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"}, - {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"}, - {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"}, - {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"}, - {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"}, - {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"}, - {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"}, - {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"}, - {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"}, - {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"}, - {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"}, - {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"}, - {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"}, - {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"}, - {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"}, - {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"}, - {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"}, - {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"}, - {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"}, - {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"}, - {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, - {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, -] +coverage = [] dill = [ {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, ] distlib = [ - {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, - {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, + {file = "distlib-0.3.5-py2.py3-none-any.whl", hash = "sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c"}, + {file = "distlib-0.3.5.tar.gz", hash = "sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe"}, ] docutils = [] -filelock = [ - {file = "filelock-3.7.1-py3-none-any.whl", hash = "sha256:37def7b658813cda163b56fc564cdc75e86d338246458c4c28ae84cabefa2404"}, - {file = "filelock-3.7.1.tar.gz", hash = "sha256:3a0fd85166ad9dbab54c9aec96737b744106dc5f15c0b09a6744a445299fcf04"}, -] -flake8 = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, -] -frozenlist = [ - {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2257aaba9660f78c7b1d8fea963b68f3feffb1a9d5d05a18401ca9eb3e8d0a3"}, - {file = "frozenlist-1.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:4a44ebbf601d7bac77976d429e9bdb5a4614f9f4027777f9e54fd765196e9d3b"}, - {file = "frozenlist-1.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:45334234ec30fc4ea677f43171b18a27505bfb2dba9aca4398a62692c0ea8868"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47be22dc27ed933d55ee55845d34a3e4e9f6fee93039e7f8ebadb0c2f60d403f"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:03a7dd1bfce30216a3f51a84e6dd0e4a573d23ca50f0346634916ff105ba6e6b"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:691ddf6dc50480ce49f68441f1d16a4c3325887453837036e0fb94736eae1e58"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bde99812f237f79eaf3f04ebffd74f6718bbd216101b35ac7955c2d47c17da02"}, - {file = "frozenlist-1.3.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6a202458d1298ced3768f5a7d44301e7c86defac162ace0ab7434c2e961166e8"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b9e3e9e365991f8cc5f5edc1fd65b58b41d0514a6a7ad95ef5c7f34eb49b3d3e"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:04cb491c4b1c051734d41ea2552fde292f5f3a9c911363f74f39c23659c4af78"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:436496321dad302b8b27ca955364a439ed1f0999311c393dccb243e451ff66aa"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:754728d65f1acc61e0f4df784456106e35afb7bf39cfe37227ab00436fb38676"}, - {file = "frozenlist-1.3.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6eb275c6385dd72594758cbe96c07cdb9bd6becf84235f4a594bdf21e3596c9d"}, - {file = "frozenlist-1.3.0-cp310-cp310-win32.whl", hash = "sha256:e30b2f9683812eb30cf3f0a8e9f79f8d590a7999f731cf39f9105a7c4a39489d"}, - {file = "frozenlist-1.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:f7353ba3367473d1d616ee727945f439e027f0bb16ac1a750219a8344d1d5d3c"}, - {file = "frozenlist-1.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:88aafd445a233dbbf8a65a62bc3249a0acd0d81ab18f6feb461cc5a938610d24"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4406cfabef8f07b3b3af0f50f70938ec06d9f0fc26cbdeaab431cbc3ca3caeaa"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8cf829bd2e2956066dd4de43fd8ec881d87842a06708c035b37ef632930505a2"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:603b9091bd70fae7be28bdb8aa5c9990f4241aa33abb673390a7f7329296695f"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25af28b560e0c76fa41f550eacb389905633e7ac02d6eb3c09017fa1c8cdfde1"}, - {file = "frozenlist-1.3.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94c7a8a9fc9383b52c410a2ec952521906d355d18fccc927fca52ab575ee8b93"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:65bc6e2fece04e2145ab6e3c47428d1bbc05aede61ae365b2c1bddd94906e478"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:3f7c935c7b58b0d78c0beea0c7358e165f95f1fd8a7e98baa40d22a05b4a8141"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd89acd1b8bb4f31b47072615d72e7f53a948d302b7c1d1455e42622de180eae"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:6983a31698490825171be44ffbafeaa930ddf590d3f051e397143a5045513b01"}, - {file = "frozenlist-1.3.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:adac9700675cf99e3615eb6a0eb5e9f5a4143c7d42c05cea2e7f71c27a3d0846"}, - {file = "frozenlist-1.3.0-cp37-cp37m-win32.whl", hash = "sha256:0c36e78b9509e97042ef869c0e1e6ef6429e55817c12d78245eb915e1cca7468"}, - {file = "frozenlist-1.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:57f4d3f03a18facacb2a6bcd21bccd011e3b75d463dc49f838fd699d074fabd1"}, - {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8c905a5186d77111f02144fab5b849ab524f1e876a1e75205cd1386a9be4b00a"}, - {file = "frozenlist-1.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:b5009062d78a8c6890d50b4e53b0ddda31841b3935c1937e2ed8c1bda1c7fb9d"}, - {file = "frozenlist-1.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2fdc3cd845e5a1f71a0c3518528bfdbfe2efaf9886d6f49eacc5ee4fd9a10953"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e650bd09b5dda929523b9f8e7f99b24deac61240ecc1a32aeba487afcd970f"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40dff8962b8eba91fd3848d857203f0bd704b5f1fa2b3fc9af64901a190bba08"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:768efd082074bb203c934e83a61654ed4931ef02412c2fbdecea0cff7ecd0274"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:006d3595e7d4108a12025ddf415ae0f6c9e736e726a5db0183326fd191b14c5e"}, - {file = "frozenlist-1.3.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:871d42623ae15eb0b0e9df65baeee6976b2e161d0ba93155411d58ff27483ad8"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:aff388be97ef2677ae185e72dc500d19ecaf31b698986800d3fc4f399a5e30a5"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9f892d6a94ec5c7b785e548e42722e6f3a52f5f32a8461e82ac3e67a3bd073f1"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:e982878792c971cbd60ee510c4ee5bf089a8246226dea1f2138aa0bb67aff148"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c6c321dd013e8fc20735b92cb4892c115f5cdb82c817b1e5b07f6b95d952b2f0"}, - {file = "frozenlist-1.3.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:30530930410855c451bea83f7b272fb1c495ed9d5cc72895ac29e91279401db3"}, - {file = "frozenlist-1.3.0-cp38-cp38-win32.whl", hash = "sha256:40ec383bc194accba825fbb7d0ef3dda5736ceab2375462f1d8672d9f6b68d07"}, - {file = "frozenlist-1.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:f20baa05eaa2bcd5404c445ec51aed1c268d62600362dc6cfe04fae34a424bd9"}, - {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:0437fe763fb5d4adad1756050cbf855bbb2bf0d9385c7bb13d7a10b0dd550486"}, - {file = "frozenlist-1.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b684c68077b84522b5c7eafc1dc735bfa5b341fb011d5552ebe0968e22ed641c"}, - {file = "frozenlist-1.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:93641a51f89473837333b2f8100f3f89795295b858cd4c7d4a1f18e299dc0a4f"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6d32ff213aef0fd0bcf803bffe15cfa2d4fde237d1d4838e62aec242a8362fa"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31977f84828b5bb856ca1eb07bf7e3a34f33a5cddce981d880240ba06639b94d"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3c62964192a1c0c30b49f403495911298810bada64e4f03249ca35a33ca0417a"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4eda49bea3602812518765810af732229b4291d2695ed24a0a20e098c45a707b"}, - {file = "frozenlist-1.3.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:acb267b09a509c1df5a4ca04140da96016f40d2ed183cdc356d237286c971b51"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e1e26ac0a253a2907d654a37e390904426d5ae5483150ce3adedb35c8c06614a"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:f96293d6f982c58ebebb428c50163d010c2f05de0cde99fd681bfdc18d4b2dc2"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:e84cb61b0ac40a0c3e0e8b79c575161c5300d1d89e13c0e02f76193982f066ed"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:ff9310f05b9d9c5c4dd472983dc956901ee6cb2c3ec1ab116ecdde25f3ce4951"}, - {file = "frozenlist-1.3.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d26b650b71fdc88065b7a21f8ace70175bcf3b5bdba5ea22df4bfd893e795a3b"}, - {file = "frozenlist-1.3.0-cp39-cp39-win32.whl", hash = "sha256:01a73627448b1f2145bddb6e6c2259988bb8aee0fb361776ff8604b99616cd08"}, - {file = "frozenlist-1.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:772965f773757a6026dea111a15e6e2678fbd6216180f82a48a40b27de1ee2ab"}, - {file = "frozenlist-1.3.0.tar.gz", hash = "sha256:ce6f2ba0edb7b0c1d8976565298ad2deba6f8064d2bebb6ffce2ca896eb35b0b"}, -] +filelock = [] +flake8 = [] +frozenlist = [] h11 = [ {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, @@ -1335,11 +1257,11 @@ idna = [ {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, ] -imagesize = [] -importlib-metadata = [ - {file = "importlib_metadata-4.12.0-py3-none-any.whl", hash = "sha256:7401a975809ea1fdc658c3aa4f78cc2195a0e019c5cbc4c06122884e9ae80c23"}, - {file = "importlib_metadata-4.12.0.tar.gz", hash = "sha256:637245b8bab2b6502fcbc752cc4b7a6f6243bb02b31c5c26156ad103d3d45670"}, +imagesize = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] +importlib-metadata = [] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -1391,10 +1313,7 @@ lazy-object-proxy = [ {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, ] -m2r2 = [ - {file = "m2r2-0.3.2-py3-none-any.whl", hash = "sha256:d3684086b61b4bebe2307f15189495360f05a123c9bda2a66462649b7ca236aa"}, - {file = "m2r2-0.3.2.tar.gz", hash = "sha256:ccd95b052dcd1ac7442ecb3111262b2001c10e4119b459c34c93ac7a5c2c7868"}, -] +m2r2 = [] markupsafe = [ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, @@ -1437,10 +1356,7 @@ markupsafe = [ {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] -mccabe = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, -] +mccabe = [] mistune = [ {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, @@ -1507,29 +1423,29 @@ multidict = [ {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, ] mypy = [ - {file = "mypy-0.961-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:697540876638ce349b01b6786bc6094ccdaba88af446a9abb967293ce6eaa2b0"}, - {file = "mypy-0.961-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b117650592e1782819829605a193360a08aa99f1fc23d1d71e1a75a142dc7e15"}, - {file = "mypy-0.961-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:bdd5ca340beffb8c44cb9dc26697628d1b88c6bddf5c2f6eb308c46f269bb6f3"}, - {file = "mypy-0.961-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3e09f1f983a71d0672bbc97ae33ee3709d10c779beb613febc36805a6e28bb4e"}, - {file = "mypy-0.961-cp310-cp310-win_amd64.whl", hash = "sha256:e999229b9f3198c0c880d5e269f9f8129c8862451ce53a011326cad38b9ccd24"}, - {file = "mypy-0.961-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:b24be97351084b11582fef18d79004b3e4db572219deee0212078f7cf6352723"}, - {file = "mypy-0.961-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f4a21d01fc0ba4e31d82f0fff195682e29f9401a8bdb7173891070eb260aeb3b"}, - {file = "mypy-0.961-cp36-cp36m-win_amd64.whl", hash = "sha256:439c726a3b3da7ca84a0199a8ab444cd8896d95012c4a6c4a0d808e3147abf5d"}, - {file = "mypy-0.961-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:5a0b53747f713f490affdceef835d8f0cb7285187a6a44c33821b6d1f46ed813"}, - {file = "mypy-0.961-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0e9f70df36405c25cc530a86eeda1e0867863d9471fe76d1273c783df3d35c2e"}, - {file = "mypy-0.961-cp37-cp37m-win_amd64.whl", hash = "sha256:b88f784e9e35dcaa075519096dc947a388319cb86811b6af621e3523980f1c8a"}, - {file = "mypy-0.961-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:d5aaf1edaa7692490f72bdb9fbd941fbf2e201713523bdb3f4038be0af8846c6"}, - {file = "mypy-0.961-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9f5f5a74085d9a81a1f9c78081d60a0040c3efb3f28e5c9912b900adf59a16e6"}, - {file = "mypy-0.961-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f4b794db44168a4fc886e3450201365c9526a522c46ba089b55e1f11c163750d"}, - {file = "mypy-0.961-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:64759a273d590040a592e0f4186539858c948302c653c2eac840c7a3cd29e51b"}, - {file = "mypy-0.961-cp38-cp38-win_amd64.whl", hash = "sha256:63e85a03770ebf403291ec50097954cc5caf2a9205c888ce3a61bd3f82e17569"}, - {file = "mypy-0.961-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f1332964963d4832a94bebc10f13d3279be3ce8f6c64da563d6ee6e2eeda932"}, - {file = "mypy-0.961-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:006be38474216b833eca29ff6b73e143386f352e10e9c2fbe76aa8549e5554f5"}, - {file = "mypy-0.961-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9940e6916ed9371809b35b2154baf1f684acba935cd09928952310fbddaba648"}, - {file = "mypy-0.961-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:a5ea0875a049de1b63b972456542f04643daf320d27dc592d7c3d9cd5d9bf950"}, - {file = "mypy-0.961-cp39-cp39-win_amd64.whl", hash = "sha256:1ece702f29270ec6af25db8cf6185c04c02311c6bb21a69f423d40e527b75c56"}, - {file = "mypy-0.961-py3-none-any.whl", hash = "sha256:03c6cc893e7563e7b2949b969e63f02c000b32502a1b4d1314cabe391aa87d66"}, - {file = "mypy-0.961.tar.gz", hash = "sha256:f730d56cb924d371c26b8eaddeea3cc07d78ff51c521c6d04899ac6904b75492"}, + {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, + {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, + {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, + {file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"}, + {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, + {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, + {file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"}, + {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, + {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, + {file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"}, + {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, + {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, + {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, + {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, + {file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"}, + {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, + {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, + {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, + {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, + {file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"}, + {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, + {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, + {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, @@ -1555,23 +1471,20 @@ py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] -pycodestyle = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, -] +pycodestyle = [] pydocstyle = [ {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, ] -pyflakes = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, -] +pyflakes = [] pygments = [ {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, ] -pylint = [] +pylint = [ + {file = "pylint-2.13.9-py3-none-any.whl", hash = "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526"}, + {file = "pylint-2.13.9.tar.gz", hash = "sha256:095567c96e19e6f57b5b907e67d265ff535e588fe26b12b5ebe1fc5645b2c731"}, +] pyparsing = [ {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, @@ -1581,18 +1494,14 @@ pytest = [ {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, ] pytest-asyncio = [ - {file = "pytest-asyncio-0.18.3.tar.gz", hash = "sha256:7659bdb0a9eb9c6e3ef992eef11a2b3e69697800ad02fb06374a210d85b29f91"}, - {file = "pytest_asyncio-0.18.3-1-py3-none-any.whl", hash = "sha256:16cf40bdf2b4fb7fc8e4b82bd05ce3fbcd454cbf7b92afc445fe299dabb88213"}, - {file = "pytest_asyncio-0.18.3-py3-none-any.whl", hash = "sha256:8fafa6c52161addfd41ee7ab35f11836c5a16ec208f93ee388f752bea3493a84"}, + {file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"}, + {file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"}, ] pytest-cov = [ {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, ] -pytz = [ - {file = "pytz-2022.1-py2.py3-none-any.whl", hash = "sha256:e68985985296d9a66a881eb3193b0906246245294a881e7c8afe623866ac6a5c"}, - {file = "pytz-2022.1.tar.gz", hash = "sha256:1e760e2fe6a8163bc0b3d9a19c4f84342afa0a2affebfaa84b01b978a02ecaa7"}, -] +pytz = [] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, @@ -1628,7 +1537,10 @@ pyyaml = [ {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, ] -requests = [] +requests = [ + {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, + {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, +] rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, @@ -1650,12 +1562,12 @@ soupsieve = [ {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, ] sphinx = [ - {file = "Sphinx-5.0.2-py3-none-any.whl", hash = "sha256:d3e57663eed1d7c5c50895d191fdeda0b54ded6f44d5621b50709466c338d1e8"}, - {file = "Sphinx-5.0.2.tar.gz", hash = "sha256:b18e978ea7565720f26019c702cd85c84376e948370f1cd43d60265010e1c7b0"}, + {file = "Sphinx-5.1.1-py3-none-any.whl", hash = "sha256:309a8da80cb6da9f4713438e5b55861877d5d7976b69d87e336733637ea12693"}, + {file = "Sphinx-5.1.1.tar.gz", hash = "sha256:ba3224a4e206e1fbdecf98a4fae4992ef9b24b85ebf7b584bb340156eaf08d89"}, ] sphinx-autoapi = [ - {file = "sphinx-autoapi-1.8.4.tar.gz", hash = "sha256:8c4ec5fbedc1e6e8f4692bcc4fcd1abcfb9e8dfca8a4ded60ad811a743c22ccc"}, - {file = "sphinx_autoapi-1.8.4-py2.py3-none-any.whl", hash = "sha256:007bf9e24cd2aa0ac0561f67e8bcd6a6e2e8911ef4b4fd54aaba799d8832c8d0"}, + {file = "sphinx-autoapi-1.9.0.tar.gz", hash = "sha256:c897ea337df16ad0cde307cbdfe2bece207788dde1587fa4fc8b857d1fc5dcba"}, + {file = "sphinx_autoapi-1.9.0-py2.py3-none-any.whl", hash = "sha256:d217953273b359b699d8cb81a5a72985a3e6e15cfe3f703d9a3c201ffc30849b"}, ] sphinx-copybutton = [ {file = "sphinx-copybutton-0.5.0.tar.gz", hash = "sha256:a0c059daadd03c27ba750da534a92a63e7a36a7736dcf684f26ee346199787f6"}, @@ -1694,7 +1606,10 @@ tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] -tox = [] +tox = [ + {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"}, + {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"}, +] typed-ast = [ {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, {file = "typed_ast-1.5.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:211260621ab1cd7324e0798d6be953d00b74e0428382991adfddb352252f1d62"}, @@ -1721,13 +1636,22 @@ typed-ast = [ {file = "typed_ast-1.5.4-cp39-cp39-win_amd64.whl", hash = "sha256:0fdbcf2fef0ca421a3f5912555804296f0b0960f0418c440f5d6d3abb549f3e1"}, {file = "typed_ast-1.5.4.tar.gz", hash = "sha256:39e21ceb7388e4bb37f4c679d72707ed46c2fbf2a5609b8b8ebc4b067d977df2"}, ] -typer = [] -typing-extensions = [] +typer = [ + {file = "typer-0.6.1-py3-none-any.whl", hash = "sha256:54b19e5df18654070a82f8c2aa1da456a4ac16a2a83e6dcd9f170e291c56338e"}, + {file = "typer-0.6.1.tar.gz", hash = "sha256:2d5720a5e63f73eaf31edaa15f6ab87f35f0690f8ca233017d7d23d743a91d73"}, +] +typing-extensions = [ + {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, + {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, +] unidecode = [ {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, ] -urllib3 = [] +urllib3 = [ + {file = "urllib3-1.26.11-py2.py3-none-any.whl", hash = "sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc"}, + {file = "urllib3-1.26.11.tar.gz", hash = "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"}, +] virtualenv = [] wrapt = [ {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, @@ -1796,80 +1720,67 @@ wrapt = [ {file = "wrapt-1.14.1.tar.gz", hash = "sha256:380a85cf89e0e69b7cfbe2ea9f765f004ff419f34194018a6827ac0e3edfed4d"}, ] yarl = [ - {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2a8508f7350512434e41065684076f640ecce176d262a7d54f0da41d99c5a95"}, - {file = "yarl-1.7.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:da6df107b9ccfe52d3a48165e48d72db0eca3e3029b5b8cb4fe6ee3cb870ba8b"}, - {file = "yarl-1.7.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a1d0894f238763717bdcfea74558c94e3bc34aeacd3351d769460c1a586a8b05"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfe4b95b7e00c6635a72e2d00b478e8a28bfb122dc76349a06e20792eb53a523"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c145ab54702334c42237a6c6c4cc08703b6aa9b94e2f227ceb3d477d20c36c63"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca56f002eaf7998b5fcf73b2421790da9d2586331805f38acd9997743114e98"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d3d5ad8ea96bd6d643d80c7b8d5977b4e2fb1bab6c9da7322616fd26203d125"}, - {file = "yarl-1.7.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:167ab7f64e409e9bdd99333fe8c67b5574a1f0495dcfd905bc7454e766729b9e"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:95a1873b6c0dd1c437fb3bb4a4aaa699a48c218ac7ca1e74b0bee0ab16c7d60d"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6152224d0a1eb254f97df3997d79dadd8bb2c1a02ef283dbb34b97d4f8492d23"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5bb7d54b8f61ba6eee541fba4b83d22b8a046b4ef4d8eb7f15a7e35db2e1e245"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:9c1f083e7e71b2dd01f7cd7434a5f88c15213194df38bc29b388ccdf1492b739"}, - {file = "yarl-1.7.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:f44477ae29025d8ea87ec308539f95963ffdc31a82f42ca9deecf2d505242e72"}, - {file = "yarl-1.7.2-cp310-cp310-win32.whl", hash = "sha256:cff3ba513db55cc6a35076f32c4cdc27032bd075c9faef31fec749e64b45d26c"}, - {file = "yarl-1.7.2-cp310-cp310-win_amd64.whl", hash = "sha256:c9c6d927e098c2d360695f2e9d38870b2e92e0919be07dbe339aefa32a090265"}, - {file = "yarl-1.7.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:9b4c77d92d56a4c5027572752aa35082e40c561eec776048330d2907aead891d"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c01a89a44bb672c38f42b49cdb0ad667b116d731b3f4c896f72302ff77d71656"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c19324a1c5399b602f3b6e7db9478e5b1adf5cf58901996fc973fe4fccd73eed"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3abddf0b8e41445426d29f955b24aeecc83fa1072be1be4e0d194134a7d9baee"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6a1a9fe17621af43e9b9fcea8bd088ba682c8192d744b386ee3c47b56eaabb2c"}, - {file = "yarl-1.7.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8b0915ee85150963a9504c10de4e4729ae700af11df0dc5550e6587ed7891e92"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:29e0656d5497733dcddc21797da5a2ab990c0cb9719f1f969e58a4abac66234d"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:bf19725fec28452474d9887a128e98dd67eee7b7d52e932e6949c532d820dc3b"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:d6f3d62e16c10e88d2168ba2d065aa374e3c538998ed04996cd373ff2036d64c"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ac10bbac36cd89eac19f4e51c032ba6b412b3892b685076f4acd2de18ca990aa"}, - {file = "yarl-1.7.2-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:aa32aaa97d8b2ed4e54dc65d241a0da1c627454950f7d7b1f95b13985afd6c5d"}, - {file = "yarl-1.7.2-cp36-cp36m-win32.whl", hash = "sha256:87f6e082bce21464857ba58b569370e7b547d239ca22248be68ea5d6b51464a1"}, - {file = "yarl-1.7.2-cp36-cp36m-win_amd64.whl", hash = "sha256:ac35ccde589ab6a1870a484ed136d49a26bcd06b6a1c6397b1967ca13ceb3913"}, - {file = "yarl-1.7.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a467a431a0817a292121c13cbe637348b546e6ef47ca14a790aa2fa8cc93df63"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ab0c3274d0a846840bf6c27d2c60ba771a12e4d7586bf550eefc2df0b56b3b4"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d260d4dc495c05d6600264a197d9d6f7fc9347f21d2594926202fd08cf89a8ba"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fc4dd8b01a8112809e6b636b00f487846956402834a7fd59d46d4f4267181c41"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c1164a2eac148d85bbdd23e07dfcc930f2e633220f3eb3c3e2a25f6148c2819e"}, - {file = "yarl-1.7.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:67e94028817defe5e705079b10a8438b8cb56e7115fa01640e9c0bb3edf67332"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:89ccbf58e6a0ab89d487c92a490cb5660d06c3a47ca08872859672f9c511fc52"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:8cce6f9fa3df25f55521fbb5c7e4a736683148bcc0c75b21863789e5185f9185"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:211fcd65c58bf250fb994b53bc45a442ddc9f441f6fec53e65de8cba48ded986"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c10ea1e80a697cf7d80d1ed414b5cb8f1eec07d618f54637067ae3c0334133c4"}, - {file = "yarl-1.7.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:52690eb521d690ab041c3919666bea13ab9fbff80d615ec16fa81a297131276b"}, - {file = "yarl-1.7.2-cp37-cp37m-win32.whl", hash = "sha256:695ba021a9e04418507fa930d5f0704edbce47076bdcfeeaba1c83683e5649d1"}, - {file = "yarl-1.7.2-cp37-cp37m-win_amd64.whl", hash = "sha256:c17965ff3706beedafd458c452bf15bac693ecd146a60a06a214614dc097a271"}, - {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:fce78593346c014d0d986b7ebc80d782b7f5e19843ca798ed62f8e3ba8728576"}, - {file = "yarl-1.7.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c2a1ac41a6aa980db03d098a5531f13985edcb451bcd9d00670b03129922cd0d"}, - {file = "yarl-1.7.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:39d5493c5ecd75c8093fa7700a2fb5c94fe28c839c8e40144b7ab7ccba6938c8"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1eb6480ef366d75b54c68164094a6a560c247370a68c02dddb11f20c4c6d3c9d"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5ba63585a89c9885f18331a55d25fe81dc2d82b71311ff8bd378fc8004202ff6"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e39378894ee6ae9f555ae2de332d513a5763276a9265f8e7cbaeb1b1ee74623a"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c0910c6b6c31359d2f6184828888c983d54d09d581a4a23547a35f1d0b9484b1"}, - {file = "yarl-1.7.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6feca8b6bfb9eef6ee057628e71e1734caf520a907b6ec0d62839e8293e945c0"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8300401dc88cad23f5b4e4c1226f44a5aa696436a4026e456fe0e5d2f7f486e6"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:788713c2896f426a4e166b11f4ec538b5736294ebf7d5f654ae445fd44270832"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:fd547ec596d90c8676e369dd8a581a21227fe9b4ad37d0dc7feb4ccf544c2d59"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:737e401cd0c493f7e3dd4db72aca11cfe069531c9761b8ea474926936b3c57c8"}, - {file = "yarl-1.7.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:baf81561f2972fb895e7844882898bda1eef4b07b5b385bcd308d2098f1a767b"}, - {file = "yarl-1.7.2-cp38-cp38-win32.whl", hash = "sha256:ede3b46cdb719c794427dcce9d8beb4abe8b9aa1e97526cc20de9bd6583ad1ef"}, - {file = "yarl-1.7.2-cp38-cp38-win_amd64.whl", hash = "sha256:cc8b7a7254c0fc3187d43d6cb54b5032d2365efd1df0cd1749c0c4df5f0ad45f"}, - {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:580c1f15500e137a8c37053e4cbf6058944d4c114701fa59944607505c2fe3a0"}, - {file = "yarl-1.7.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ec1d9a0d7780416e657f1e405ba35ec1ba453a4f1511eb8b9fbab81cb8b3ce1"}, - {file = "yarl-1.7.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3bf8cfe8856708ede6a73907bf0501f2dc4e104085e070a41f5d88e7faf237f3"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1be4bbb3d27a4e9aa5f3df2ab61e3701ce8fcbd3e9846dbce7c033a7e8136746"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:534b047277a9a19d858cde163aba93f3e1677d5acd92f7d10ace419d478540de"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6ddcd80d79c96eb19c354d9dca95291589c5954099836b7c8d29278a7ec0bda"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9bfcd43c65fbb339dc7086b5315750efa42a34eefad0256ba114cd8ad3896f4b"}, - {file = "yarl-1.7.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f64394bd7ceef1237cc604b5a89bf748c95982a84bcd3c4bbeb40f685c810794"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044daf3012e43d4b3538562da94a88fb12a6490652dbc29fb19adfa02cf72eac"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:368bcf400247318382cc150aaa632582d0780b28ee6053cd80268c7e72796dec"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:bab827163113177aee910adb1f48ff7af31ee0289f434f7e22d10baf624a6dfe"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:0cba38120db72123db7c58322fa69e3c0efa933040ffb586c3a87c063ec7cae8"}, - {file = "yarl-1.7.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:59218fef177296451b23214c91ea3aba7858b4ae3306dde120224cfe0f7a6ee8"}, - {file = "yarl-1.7.2-cp39-cp39-win32.whl", hash = "sha256:1edc172dcca3f11b38a9d5c7505c83c1913c0addc99cd28e993efeaafdfaa18d"}, - {file = "yarl-1.7.2-cp39-cp39-win_amd64.whl", hash = "sha256:797c2c412b04403d2da075fb93c123df35239cd7b4cc4e0cd9e5839b73f52c58"}, - {file = "yarl-1.7.2.tar.gz", hash = "sha256:45399b46d60c253327a460e99856752009fcee5f5d3c80b2f7c0cae1c38d56dd"}, + {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:abc06b97407868ef38f3d172762f4069323de52f2b70d133d096a48d72215d28"}, + {file = "yarl-1.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:07b21e274de4c637f3e3b7104694e53260b5fc10d51fb3ec5fed1da8e0f754e3"}, + {file = "yarl-1.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9de955d98e02fab288c7718662afb33aab64212ecb368c5dc866d9a57bf48880"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ec362167e2c9fd178f82f252b6d97669d7245695dc057ee182118042026da40"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20df6ff4089bc86e4a66e3b1380460f864df3dd9dccaf88d6b3385d24405893b"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5999c4662631cb798496535afbd837a102859568adc67d75d2045e31ec3ac497"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed19b74e81b10b592084a5ad1e70f845f0aacb57577018d31de064e71ffa267a"}, + {file = "yarl-1.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e4808f996ca39a6463f45182e2af2fae55e2560be586d447ce8016f389f626f"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:2d800b9c2eaf0684c08be5f50e52bfa2aa920e7163c2ea43f4f431e829b4f0fd"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6628d750041550c5d9da50bb40b5cf28a2e63b9388bac10fedd4f19236ef4957"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:f5af52738e225fcc526ae64071b7e5342abe03f42e0e8918227b38c9aa711e28"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:76577f13333b4fe345c3704811ac7509b31499132ff0181f25ee26619de2c843"}, + {file = "yarl-1.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0c03f456522d1ec815893d85fccb5def01ffaa74c1b16ff30f8aaa03eb21e453"}, + {file = "yarl-1.8.1-cp310-cp310-win32.whl", hash = "sha256:ea30a42dc94d42f2ba4d0f7c0ffb4f4f9baa1b23045910c0c32df9c9902cb272"}, + {file = "yarl-1.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:9130ddf1ae9978abe63808b6b60a897e41fccb834408cde79522feb37fb72fb0"}, + {file = "yarl-1.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0ab5a138211c1c366404d912824bdcf5545ccba5b3ff52c42c4af4cbdc2c5035"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0fb2cb4204ddb456a8e32381f9a90000429489a25f64e817e6ff94879d432fc"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:85cba594433915d5c9a0d14b24cfba0339f57a2fff203a5d4fd070e593307d0b"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1ca7e596c55bd675432b11320b4eacc62310c2145d6801a1f8e9ad160685a231"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0f77539733e0ec2475ddcd4e26777d08996f8cd55d2aef82ec4d3896687abda"}, + {file = "yarl-1.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29e256649f42771829974e742061c3501cc50cf16e63f91ed8d1bf98242e5507"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7fce6cbc6c170ede0221cc8c91b285f7f3c8b9fe28283b51885ff621bbe0f8ee"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:59ddd85a1214862ce7c7c66457f05543b6a275b70a65de366030d56159a979f0"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:12768232751689c1a89b0376a96a32bc7633c08da45ad985d0c49ede691f5c0d"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:b19255dde4b4f4c32e012038f2c169bb72e7f081552bea4641cab4d88bc409dd"}, + {file = "yarl-1.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6c8148e0b52bf9535c40c48faebb00cb294ee577ca069d21bd5c48d302a83780"}, + {file = "yarl-1.8.1-cp37-cp37m-win32.whl", hash = "sha256:de839c3a1826a909fdbfe05f6fe2167c4ab033f1133757b5936efe2f84904c07"}, + {file = "yarl-1.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:dd032e8422a52e5a4860e062eb84ac94ea08861d334a4bcaf142a63ce8ad4802"}, + {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:19cd801d6f983918a3f3a39f3a45b553c015c5aac92ccd1fac619bd74beece4a"}, + {file = "yarl-1.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6347f1a58e658b97b0a0d1ff7658a03cb79bdbda0331603bed24dd7054a6dea1"}, + {file = "yarl-1.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7c0da7e44d0c9108d8b98469338705e07f4bb7dab96dbd8fa4e91b337db42548"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5587bba41399854703212b87071c6d8638fa6e61656385875f8c6dff92b2e461"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:31a9a04ecccd6b03e2b0e12e82131f1488dea5555a13a4d32f064e22a6003cfe"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:205904cffd69ae972a1707a1bd3ea7cded594b1d773a0ce66714edf17833cdae"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea513a25976d21733bff523e0ca836ef1679630ef4ad22d46987d04b372d57fc"}, + {file = "yarl-1.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d0b51530877d3ad7a8d47b2fff0c8df3b8f3b8deddf057379ba50b13df2a5eae"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d2b8f245dad9e331540c350285910b20dd913dc86d4ee410c11d48523c4fd546"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:ab2a60d57ca88e1d4ca34a10e9fb4ab2ac5ad315543351de3a612bbb0560bead"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:449c957ffc6bc2309e1fbe67ab7d2c1efca89d3f4912baeb8ead207bb3cc1cd4"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:a165442348c211b5dea67c0206fc61366212d7082ba8118c8c5c1c853ea4d82e"}, + {file = "yarl-1.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b3ded839a5c5608eec8b6f9ae9a62cb22cd037ea97c627f38ae0841a48f09eae"}, + {file = "yarl-1.8.1-cp38-cp38-win32.whl", hash = "sha256:c1445a0c562ed561d06d8cbc5c8916c6008a31c60bc3655cdd2de1d3bf5174a0"}, + {file = "yarl-1.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:56c11efb0a89700987d05597b08a1efcd78d74c52febe530126785e1b1a285f4"}, + {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e80ed5a9939ceb6fda42811542f31c8602be336b1fb977bccb012e83da7e4936"}, + {file = "yarl-1.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6afb336e23a793cd3b6476c30f030a0d4c7539cd81649683b5e0c1b0ab0bf350"}, + {file = "yarl-1.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4c322cbaa4ed78a8aac89b2174a6df398faf50e5fc12c4c191c40c59d5e28357"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fae37373155f5ef9b403ab48af5136ae9851151f7aacd9926251ab26b953118b"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5395da939ffa959974577eff2cbfc24b004a2fb6c346918f39966a5786874e54"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:076eede537ab978b605f41db79a56cad2e7efeea2aa6e0fa8f05a26c24a034fb"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d1a50e461615747dd93c099f297c1994d472b0f4d2db8a64e55b1edf704ec1c"}, + {file = "yarl-1.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7de89c8456525650ffa2bb56a3eee6af891e98f498babd43ae307bd42dca98f6"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:4a88510731cd8d4befaba5fbd734a7dd914de5ab8132a5b3dde0bbd6c9476c64"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:2d93a049d29df172f48bcb09acf9226318e712ce67374f893b460b42cc1380ae"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:21ac44b763e0eec15746a3d440f5e09ad2ecc8b5f6dcd3ea8cb4773d6d4703e3"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:d0272228fabe78ce00a3365ffffd6f643f57a91043e119c289aaba202f4095b0"}, + {file = "yarl-1.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:99449cd5366fe4608e7226c6cae80873296dfa0cde45d9b498fefa1de315a09e"}, + {file = "yarl-1.8.1-cp39-cp39-win32.whl", hash = "sha256:8b0af1cf36b93cee99a31a545fe91d08223e64390c5ecc5e94c39511832a4bb6"}, + {file = "yarl-1.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:de49d77e968de6626ba7ef4472323f9d2e5a56c1d85b7c0e2a190b2173d3b9be"}, + {file = "yarl-1.8.1.tar.gz", hash = "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf"}, ] zipp = [ - {file = "zipp-3.8.0-py3-none-any.whl", hash = "sha256:c4f6e5bbf48e74f7a38e7cc5b0480ff42b0ae5178957d564d18932525d5cf099"}, - {file = "zipp-3.8.0.tar.gz", hash = "sha256:56bf8aadb83c24db6c4b577e13de374ccfb67da2078beba1d037c17980bf43ad"}, + {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, + {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, ] diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index de44376e..edf62730 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -448,6 +448,9 @@ async def connect( ) self.__energysite_type[energysite_id] = energysite["solar_type"] self.__power[energysite_id] = {"solar_power": energysite["solar_power"]} + # Get site_config data for site name and update energysite dict + site_config = await self.get_site_config(energysite_id) + energysite.update(site_config) # Set initial values to setup GridPowerSensor & LoadPowerSensor # Actual values update immediately after setup when refresh is called energysite["grid_power"] = 0 @@ -552,6 +555,12 @@ async def get_energysites(self): if p.get("resource_type") == "solar" ] + @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) + async def get_site_config(self, energysite_id): + """Get site config json from TeslaAPI.""" + return (await self.api("SITE_CONFIG", + path_vars={"site_id": energysite_id}))["response"] + @wake_up async def post( self, diff --git a/teslajsonpy/homeassistant/power.py b/teslajsonpy/homeassistant/power.py index f4e97279..32fa53d0 100644 --- a/teslajsonpy/homeassistant/power.py +++ b/teslajsonpy/homeassistant/power.py @@ -45,7 +45,7 @@ def _name(self) -> Text: return f"{self._site_name} {self.type}" def _uniq_name(self) -> Text: - return self._name() + return f"{self._energy_site_id} {self.type}" def id(self) -> int: # pylint: disable=invalid-name diff --git a/tests/tesla_mock.py b/tests/tesla_mock.py index 4b7ebb53..4356d650 100644 --- a/tests/tesla_mock.py +++ b/tests/tesla_mock.py @@ -471,50 +471,118 @@ def command_ok(): } # Example config for solar only (no powerall) and Neurio -# Combination of PRODUCT_LIST and SITE_DATA from Controller +# Combination of PRODUCT_LIST, SITE_DATA & SITE_CONFIG from Controller ENERGYSITE_CONFIG = { - 'energy_site_id': 1234567890, - 'resource_type': 'solar', - 'id': 'c31d46d3-d3f3-4319-a2cb-34719c30243d', - 'asset_site_id': '3f345132-3c13-2cda-351a-341fq3a2dab2', - 'solar_power': 4230, - 'solar_type': 'pv_panel', - 'storm_mode_enabled': None, - 'powerwall_onboarding_settings_set': None, - 'sync_grid_alert_enabled': False, - 'breaker_alert_enabled': False, - 'components': { - 'battery': False, - 'solar': True, - 'solar_type': 'pv_panel', - 'grid': True, - 'load_meter': True, - 'market_type': 'residential' - }, - 'energy_left': 0, - 'total_pack_energy': 1, - 'percentage_charged': 0, - 'battery_power': 0, - 'load_power': 3245.4599609375, - 'grid_status': 'Unknown', - 'grid_services_active': False, - 'grid_power': -984.5400390625, - 'grid_services_power': 0, - 'generator_power': 0, - 'island_status': 'island_status_unknown', - 'storm_mode_active': False, - 'timestamp': '2022-07-29T23:02:14Z', - 'wall_connectors': None} + "energy_site_id": 1234567890, + "resource_type": "solar", + "id": "c31d46d3-d3f3-4319-a2cb-34719c30243d", + "asset_site_id": "3f345132-3c13-2cda-351a-341fq3a2dab2", + "solar_power": 4230, + "solar_type": "pv_panel", + "storm_mode_enabled": None, + "powerwall_onboarding_settings_set": None, + "sync_grid_alert_enabled": False, + "breaker_alert_enabled": False, + "components": { + "solar": True, + "solar_type": "pv_panel", + "battery": False, + "grid": True, + "backup": False, + "gateway": "gateway_type_none", + "load_meter": True, + "tou_capable": False, + "storm_mode_capable": False, + "flex_energy_request_capable": False, + "car_charging_data_supported": False, + "off_grid_vehicle_charging_reserve_supported": False, + "vehicle_charging_performance_view_enabled": False, + "vehicle_charging_solar_offset_view_enabled": False, + "battery_solar_offset_view_enabled": False, + "energy_service_self_scheduling_enabled": True, + "configurable": False, + "grid_services_enabled": False + }, + "grid_power": -984.5400390625, + "load_power": 3245.4599609375, + "site_name": "My Home", + "site_number": "STE32474374-31631", + "installation_date": "2021-03-01T12:58:33-07:00", + "user_settings": { + "storm_mode_enabled": None, + "powerwall_onboarding_settings_set": None, + "sync_grid_alert_enabled": False, + "breaker_alert_enabled": False + }, + "installation_time_zone": "America/Los_Angeles", + "time_zone_offset": -420, + "geolocation": { + "latitude": 31.32463100000001, + "longitude": -103.1425259 + }, + "address": { + "address_line1": "1234 Tesla Solar Ave", + "city": "Austin", + "state": "TX", + "zip": "123456", + "country": "US" + } +} ENERGYSITE_CONFIG_NO_NAME = { - "id": 12345678901234567, "energy_site_id": 1234567890, - "asset_site_id": 1234567890, "resource_type": "solar", - "solar_type": "pv_panels", - "solar_power": None, + "id": "c31d46d3-d3f3-4319-a2cb-34719c30243d", + "asset_site_id": "3f345132-3c13-2cda-351a-341fq3a2dab2", + "solar_power": 4230, + "solar_type": "pv_panel", + "storm_mode_enabled": None, + "powerwall_onboarding_settings_set": None, "sync_grid_alert_enabled": False, "breaker_alert_enabled": False, + "components": { + "solar": True, + "solar_type": "pv_panel", + "battery": False, + "grid": True, + "backup": False, + "gateway": "gateway_type_none", + "load_meter": True, + "tou_capable": False, + "storm_mode_capable": False, + "flex_energy_request_capable": False, + "car_charging_data_supported": False, + "off_grid_vehicle_charging_reserve_supported": False, + "vehicle_charging_performance_view_enabled": False, + "vehicle_charging_solar_offset_view_enabled": False, + "battery_solar_offset_view_enabled": False, + "energy_service_self_scheduling_enabled": True, + "configurable": False, + "grid_services_enabled": False + }, + "grid_power": -984.5400390625, + "load_power": 3245.4599609375, + "site_number": "STE32474374-31631", + "installation_date": "2021-03-01T12:58:33-07:00", + "user_settings": { + "storm_mode_enabled": None, + "powerwall_onboarding_settings_set": None, + "sync_grid_alert_enabled": False, + "breaker_alert_enabled": False + }, + "installation_time_zone": "America/Los_Angeles", + "time_zone_offset": -420, + "geolocation": { + "latitude": 31.32463100000001, + "longitude": -103.1425259 + }, + "address": { + "address_line1": "1234 Tesla Solar Ave", + "city": "Austin", + "state": "TX", + "zip": "123456", + "country": "US" + } } ENERGYSITE_STATE = { diff --git a/tests/unit_tests/homeassistant/test_power_sensor.py b/tests/unit_tests/homeassistant/test_power_sensor.py index 3df131de..4642e7b2 100644 --- a/tests/unit_tests/homeassistant/test_power_sensor.py +++ b/tests/unit_tests/homeassistant/test_power_sensor.py @@ -22,17 +22,31 @@ def test_device_class(monkeypatch): _sensor = SolarPowerSensor(_data, _controller) assert _sensor.type == "solar panel" + assert _sensor.name == "My Home solar panel" _sensor = LoadPowerSensor(_data, _controller) assert _sensor.type == "load power" + assert _sensor.name == "My Home load power" _sensor = GridPowerSensor(_data, _controller) assert _sensor.type == "grid power" + assert _sensor.name == "My Home grid power" -def test_device_no_name(monkeypatch): - """Test device_class().""" +def test_site_with_name(monkeypatch): + """Test site with no site_name in json data.""" + + _mock = TeslaMock(monkeypatch) + _controller = Controller(None) + + _data = _mock.data_request_energy_site() + _sensor = PowerSensor(_data, _controller) + + assert _sensor.site_name() == "My Home" + +def test_site_without_name(monkeypatch): + """Test site with no site_name in json data.""" _mock = TeslaMock(monkeypatch) _controller = Controller(None) @@ -42,7 +56,6 @@ def test_device_no_name(monkeypatch): assert _sensor.site_name() == "1234567890" - def test_get_solar_power_on_init(monkeypatch): """Test get_power() after initialization.""" From bb6609cb817852582528adace4c31da3f652ab71 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 14 Aug 2022 19:24:31 -0700 Subject: [PATCH 02/84] Updates to site name --- teslajsonpy/const.py | 1 + teslajsonpy/controller.py | 16 +++++++++------- teslajsonpy/homeassistant/power.py | 4 +++- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/teslajsonpy/const.py b/teslajsonpy/const.py index aeea2568..f7395838 100644 --- a/teslajsonpy/const.py +++ b/teslajsonpy/const.py @@ -19,3 +19,4 @@ TESLA_PRODUCT_TYPE_VEHICLES = "vehicles" TESLA_PRODUCT_TYPE_ENERGY_SITES = "energy_sites" TESLA_PRODUCT_TYPE_POWERWALLS = "powerwalls" +TESLA_DEFAULT_ENERGY_SITE_NAME = "My Home" \ No newline at end of file diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index edf62730..6a35b46b 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -30,6 +30,7 @@ SLEEP_INTERVAL, TESLA_PRODUCT_TYPE_ENERGY_SITES, TESLA_PRODUCT_TYPE_VEHICLES, + TESLA_DEFAULT_ENERGY_SITE_NAME ) from teslajsonpy.exceptions import should_giveup, RetryLimitError, TeslaException from teslajsonpy.homeassistant.battery_sensor import Battery, Range @@ -441,13 +442,6 @@ async def connect( for energysite in self.energysites: energysite_id = energysite["energy_site_id"] - self.__id_energysiteid_map[energysite["id"]] = energysite_id - self.__energysiteid_id_map[energysite_id] = energysite["id"] - self.__energysite_name[energysite_id] = energysite.get( - "site_name", f"{energysite_id}" - ) - self.__energysite_type[energysite_id] = energysite["solar_type"] - self.__power[energysite_id] = {"solar_power": energysite["solar_power"]} # Get site_config data for site name and update energysite dict site_config = await self.get_site_config(energysite_id) energysite.update(site_config) @@ -456,6 +450,14 @@ async def connect( energysite["grid_power"] = 0 energysite["load_power"] = 0 + self.__id_energysiteid_map[energysite["id"]] = energysite_id + self.__energysiteid_id_map[energysite_id] = energysite["id"] + self.__energysite_name[energysite_id] = energysite.get( + "site_name", TESLA_DEFAULT_ENERGY_SITE_NAME + ) + self.__energysite_type[energysite_id] = energysite["solar_type"] + self.__power[energysite_id] = {"solar_power": energysite["solar_power"]} + self.__lock[energysite_id] = asyncio.Lock() self._add_energysite_components(energysite) diff --git a/teslajsonpy/homeassistant/power.py b/teslajsonpy/homeassistant/power.py index 32fa53d0..cdee31e1 100644 --- a/teslajsonpy/homeassistant/power.py +++ b/teslajsonpy/homeassistant/power.py @@ -7,6 +7,8 @@ import logging from typing import Dict, Text +from teslajsonpy.const import TESLA_DEFAULT_ENERGY_SITE_NAME + _LOGGER = logging.getLogger(__name__) @@ -34,7 +36,7 @@ def __init__(self, data, controller): """ self._id: int = data["id"] self._energy_site_id: int = data["energy_site_id"] - self._site_name: Text = data.get("site_name", f"{self._energy_site_id}") + self._site_name: Text = data.get("site_name", TESLA_DEFAULT_ENERGY_SITE_NAME) self._controller = controller self.should_poll: bool = True self.type: Text = "device" From 09b6bda283048a0a5c726f0e4fe970d763b7d8f2 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 14 Aug 2022 19:48:40 -0700 Subject: [PATCH 03/84] Update tests --- teslajsonpy/const.py | 2 +- tests/unit_tests/homeassistant/test_power_sensor.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/const.py b/teslajsonpy/const.py index f7395838..165c67ea 100644 --- a/teslajsonpy/const.py +++ b/teslajsonpy/const.py @@ -19,4 +19,4 @@ TESLA_PRODUCT_TYPE_VEHICLES = "vehicles" TESLA_PRODUCT_TYPE_ENERGY_SITES = "energy_sites" TESLA_PRODUCT_TYPE_POWERWALLS = "powerwalls" -TESLA_DEFAULT_ENERGY_SITE_NAME = "My Home" \ No newline at end of file +TESLA_DEFAULT_ENERGY_SITE_NAME = "My Home" diff --git a/tests/unit_tests/homeassistant/test_power_sensor.py b/tests/unit_tests/homeassistant/test_power_sensor.py index 4642e7b2..55ad84b4 100644 --- a/tests/unit_tests/homeassistant/test_power_sensor.py +++ b/tests/unit_tests/homeassistant/test_power_sensor.py @@ -54,7 +54,7 @@ def test_site_without_name(monkeypatch): _data = _mock.data_request_energy_site_no_name() _sensor = PowerSensor(_data, _controller) - assert _sensor.site_name() == "1234567890" + assert _sensor.site_name() == "My Home" def test_get_solar_power_on_init(monkeypatch): """Test get_power() after initialization.""" From 74251915ae48583ff97f57fab10d44d5558636b2 Mon Sep 17 00:00:00 2001 From: shred86 Date: Tue, 16 Aug 2022 11:38:29 -0700 Subject: [PATCH 04/84] Initial commit to add battery site support --- teslajsonpy/const.py | 2 + teslajsonpy/controller.py | 73 +++++++++++++++++++++++------- teslajsonpy/homeassistant/power.py | 57 +++++++++++++++++++++-- 3 files changed, 111 insertions(+), 21 deletions(-) diff --git a/teslajsonpy/const.py b/teslajsonpy/const.py index 165c67ea..c29fe018 100644 --- a/teslajsonpy/const.py +++ b/teslajsonpy/const.py @@ -20,3 +20,5 @@ TESLA_PRODUCT_TYPE_ENERGY_SITES = "energy_sites" TESLA_PRODUCT_TYPE_POWERWALLS = "powerwalls" TESLA_DEFAULT_ENERGY_SITE_NAME = "My Home" +TESLA_RESOURCE_TYPE_SOLAR = "solar" +TESLA_RESOURCE_TYPE_BATTERY = "battery" diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 6a35b46b..1ff15357 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -30,7 +30,9 @@ SLEEP_INTERVAL, TESLA_PRODUCT_TYPE_ENERGY_SITES, TESLA_PRODUCT_TYPE_VEHICLES, - TESLA_DEFAULT_ENERGY_SITE_NAME + TESLA_DEFAULT_ENERGY_SITE_NAME, + TESLA_RESOURCE_TYPE_SOLAR, + TESLA_RESOURCE_TYPE_BATTERY, ) from teslajsonpy.exceptions import should_giveup, RetryLimitError, TeslaException from teslajsonpy.homeassistant.battery_sensor import Battery, Range @@ -57,6 +59,7 @@ SolarPowerSensor, GridPowerSensor, LoadPowerSensor, + BatteryPowerSensor, ) from teslajsonpy.homeassistant.alerts import Horn, FlashLights from teslajsonpy.homeassistant.homelink import TriggerHomelink @@ -442,21 +445,37 @@ async def connect( for energysite in self.energysites: energysite_id = energysite["energy_site_id"] - # Get site_config data for site name and update energysite dict - site_config = await self.get_site_config(energysite_id) - energysite.update(site_config) - # Set initial values to setup GridPowerSensor & LoadPowerSensor - # Actual values update immediately after setup when refresh is called - energysite["grid_power"] = 0 - energysite["load_power"] = 0 + if energysite["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: + # Set initial values to setup GridPowerSensor & LoadPowerSensor + # Actual values update immediately after setup when refresh is called + energysite["grid_power"] = 0 + energysite["load_power"] = 0 + # Non-powerwall sites do not include "site_name" in "PRODUCT_LIST" endpoint + # Get "site_config" data for "site_name" and update energysite dict + site_config = await self.get_site_config(energysite_id) + energysite.update(site_config) + + self.__power[energysite_id] = {"solar_power": energysite["solar_power"]} + + if energysite["resource_type"] == TESLA_RESOURCE_TYPE_BATTERY: + # Set initial values to setup Solar, Grid, Load and Battery PowerSensors + # Actual values update immediately after setup when refresh is called + energysite["power_reading"] = [ + { + "solar_power": 0, + "load_power": 0, + "grid_power": 0, + "battery_power": 0, + } + ] self.__id_energysiteid_map[energysite["id"]] = energysite_id self.__energysiteid_id_map[energysite_id] = energysite["id"] self.__energysite_name[energysite_id] = energysite.get( "site_name", TESLA_DEFAULT_ENERGY_SITE_NAME ) - self.__energysite_type[energysite_id] = energysite["solar_type"] - self.__power[energysite_id] = {"solar_power": energysite["solar_power"]} + # Sites with powerwall only contain "solar_type" in "components" + self.__energysite_type[energysite_id] = energysite["components"]["solar_type"] self.__lock[energysite_id] = asyncio.Lock() self._add_energysite_components(energysite) @@ -550,11 +569,12 @@ async def get_vehicles(self): @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) async def get_energysites(self): - """Get energy sites json from TeslaAPI and filter to solar.""" + """Get energy sites json from TeslaAPI and filter to solar and battery.""" return [ p for p in (await self.api("PRODUCT_LIST"))["response"] - if p.get("resource_type") == "solar" + if p.get("resource_type") == TESLA_RESOURCE_TYPE_SOLAR + or p.get("resource_type") == TESLA_RESOURCE_TYPE_BATTERY ] @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) @@ -734,6 +754,8 @@ def _add_energysite_components(self, energysite): self.__components.append(SolarPowerSensor(energysite, self)) self.__components.append(LoadPowerSensor(energysite, self)) self.__components.append(GridPowerSensor(energysite, self)) + if energysite["resource_type"] == TESLA_RESOURCE_TYPE_BATTERY: + self.__components.append(BatteryPowerSensor(energysite, self)) def _add_car_components(self, car): self.__components.append(Climate(car, self)) @@ -970,9 +992,9 @@ async def _get_and_process_car_data(vin: Text) -> None: ) ) - async def _get_and_process_energysite_data(energysite_id: Text) -> None: + async def _get_and_process_site_data(energysite_id: Text) -> None: async with self.__lock[energysite_id]: - _LOGGER.debug("Updating energysite %s", energysite_id) + _LOGGER.debug("Updating energysite site data %s", energysite_id) try: data = await self.api( "SITE_DATA", @@ -985,6 +1007,21 @@ async def _get_and_process_energysite_data(energysite_id: Text) -> None: response = data["response"] self.__power[energysite_id] = response + async def _get_and_process_battery_data(battery_id: Text) -> None: + async with self.__lock[battery_id]: + _LOGGER.debug("Updating energysite battery_data %s", battery_id) + try: + data = await self.api( + "BATTERY_DATA", + path_vars={"battery_id": battery_id}, + wake_if_asleep=wake_if_asleep, + ) + except TeslaException: + data = None + if data and data["response"]: + response = data["response"] + self.__power[id] = response + async with self.__update_lock: cur_time = round(time.time()) # Update the online cars using get_vehicles() @@ -1060,8 +1097,12 @@ async def _get_and_process_energysite_data(energysite_id: Text) -> None: if not car_id: # do not update energy sites if car_id was a parameter. for energysite in self.energysites: - energysite_id = energysite["energy_site_id"] - tasks.append(_get_and_process_energysite_data(energysite_id)) + if energysite["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: + energysite_id = energysite["energy_site_id"] + tasks.append(_get_and_process_site_data(energysite_id)) + if energysite["resource_type"] == TESLA_RESOURCE_TYPE_BATTERY: + battery_id = energysite["id"] + tasks.append(_get_and_process_battery_data(battery_id)) return any(await asyncio.gather(*tasks)) diff --git a/teslajsonpy/homeassistant/power.py b/teslajsonpy/homeassistant/power.py index cdee31e1..8f9a2547 100644 --- a/teslajsonpy/homeassistant/power.py +++ b/teslajsonpy/homeassistant/power.py @@ -7,7 +7,11 @@ import logging from typing import Dict, Text -from teslajsonpy.const import TESLA_DEFAULT_ENERGY_SITE_NAME +from teslajsonpy.const import ( + TESLA_DEFAULT_ENERGY_SITE_NAME, + TESLA_RESOURCE_TYPE_SOLAR, + TESLA_RESOURCE_TYPE_BATTERY, +) _LOGGER = logging.getLogger(__name__) @@ -138,8 +142,11 @@ class SolarPowerSensor(PowerSensor): def __init__(self, data, controller): """Initialize the solar panel sensor.""" super().__init__(data, controller) - self._solar_type: Text = data["solar_type"] - self.__solar_power: float = data["solar_power"] + self._solar_type: Text = data["components"]["solar_type"] + if data["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: + self.__solar_power: float = data["solar_power"] + if data["resource_type"] == TESLA_RESOURCE_TYPE_BATTERY: + self.__solar_power: float = data["power_reading"][0]["solar_power"] self.__generating_status: bool = None self.type = "solar panel" self.name = self._name() @@ -198,7 +205,10 @@ class LoadPowerSensor(PowerSensor): def __init__(self, data, controller): """Initialize the load power sensor.""" super().__init__(data, controller) - self.__load_power: float = data["load_power"] + if data["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: + self.__load_power: float = data["load_power"] + if data["resource_type"] == TESLA_RESOURCE_TYPE_BATTERY: + self.__load_power: float = data["power_reading"][0]["load_power"] self.type = "load power" self.name = self._name() self.uniq_name = self._uniq_name() @@ -232,7 +242,10 @@ class GridPowerSensor(PowerSensor): def __init__(self, data, controller): """Initialize the grid power sensor.""" super().__init__(data, controller) - self.__grid_power: float = data["grid_power"] + if data["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: + self.__grid_power: float = data["grid_power"] + if data["resource_type"] == TESLA_RESOURCE_TYPE_BATTERY: + self.__grid_power: float = data["power_reading"][0]["grid_power"] self.type = "grid power" self.name = self._name() self.uniq_name = self._uniq_name() @@ -255,3 +268,37 @@ def refresh(self) -> None: if data: self.__grid_power = data["grid_power"] + + +class BatteryPowerSensor(PowerSensor): + """Home-assistant class for grid power sensors for Tesla Energy Sites (Solar Panels). + + This is intended to be partially inherited by a Home-Assitant entity. + """ + + def __init__(self, data, controller): + """Initialize the battery power sensor.""" + super().__init__(data, controller) + self.__battery_power: float = data["power_reading"][0]["battery_power"] + self.type = "battery power" + self.name = self._name() + self.uniq_name = self._uniq_name() + + def get_value(self) -> float: + """Return grid power.""" + return self.__battery_power + + def get_grid_power(self): + """Get grid power (grid import/export).""" + return self.__battery_power + + def refresh(self) -> None: + """Refresh data. + + This assumes the controller has already been updated + """ + super().refresh() + data = self._controller.get_power_params(self._id) + + if data: + self.__battery_power = data["grid_power"] \ No newline at end of file From de05463709f1b5b584da08091d64b13932f292f9 Mon Sep 17 00:00:00 2001 From: shred86 Date: Tue, 16 Aug 2022 11:51:51 -0700 Subject: [PATCH 05/84] For troubleshooting HA dependencies --- teslajsonpy/controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 1ff15357..c866b838 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -443,6 +443,8 @@ async def connect( self._add_car_components(car) + _LOGGER.info("Running custom version of teslajsonpy with Powerwall support.") # TEMPORARY + for energysite in self.energysites: energysite_id = energysite["energy_site_id"] if energysite["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: From c2d5cdebc933540b1cefed8c2f3e7deb3068add2 Mon Sep 17 00:00:00 2001 From: shred86 Date: Tue, 16 Aug 2022 14:34:05 -0700 Subject: [PATCH 06/84] Refactor for getting power values --- teslajsonpy/controller.py | 27 ++++++++---------------- teslajsonpy/homeassistant/power.py | 33 +++++++++--------------------- 2 files changed, 18 insertions(+), 42 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index c866b838..e88c38d3 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -447,30 +447,19 @@ async def connect( for energysite in self.energysites: energysite_id = energysite["energy_site_id"] + # Set initial values to initialize power sensors + # Actual values update immediately after setup when refresh is called + energysite["solar_power"] = 0 + energysite["load_power"] = 0 + energysite["grid_power"] = 0 + energysite["battery_power"] = 0 + if energysite["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: - # Set initial values to setup GridPowerSensor & LoadPowerSensor - # Actual values update immediately after setup when refresh is called - energysite["grid_power"] = 0 - energysite["load_power"] = 0 # Non-powerwall sites do not include "site_name" in "PRODUCT_LIST" endpoint # Get "site_config" data for "site_name" and update energysite dict site_config = await self.get_site_config(energysite_id) energysite.update(site_config) - self.__power[energysite_id] = {"solar_power": energysite["solar_power"]} - - if energysite["resource_type"] == TESLA_RESOURCE_TYPE_BATTERY: - # Set initial values to setup Solar, Grid, Load and Battery PowerSensors - # Actual values update immediately after setup when refresh is called - energysite["power_reading"] = [ - { - "solar_power": 0, - "load_power": 0, - "grid_power": 0, - "battery_power": 0, - } - ] - self.__id_energysiteid_map[energysite["id"]] = energysite_id self.__energysiteid_id_map[energysite_id] = energysite["id"] self.__energysite_name[energysite_id] = energysite.get( @@ -1021,7 +1010,7 @@ async def _get_and_process_battery_data(battery_id: Text) -> None: except TeslaException: data = None if data and data["response"]: - response = data["response"] + response = data["response"]["power_reading"][0] self.__power[id] = response async with self.__update_lock: diff --git a/teslajsonpy/homeassistant/power.py b/teslajsonpy/homeassistant/power.py index 8f9a2547..72b7adb2 100644 --- a/teslajsonpy/homeassistant/power.py +++ b/teslajsonpy/homeassistant/power.py @@ -7,11 +7,7 @@ import logging from typing import Dict, Text -from teslajsonpy.const import ( - TESLA_DEFAULT_ENERGY_SITE_NAME, - TESLA_RESOURCE_TYPE_SOLAR, - TESLA_RESOURCE_TYPE_BATTERY, -) +from teslajsonpy.const import TESLA_DEFAULT_ENERGY_SITE_NAME _LOGGER = logging.getLogger(__name__) @@ -143,10 +139,7 @@ def __init__(self, data, controller): """Initialize the solar panel sensor.""" super().__init__(data, controller) self._solar_type: Text = data["components"]["solar_type"] - if data["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: - self.__solar_power: float = data["solar_power"] - if data["resource_type"] == TESLA_RESOURCE_TYPE_BATTERY: - self.__solar_power: float = data["power_reading"][0]["solar_power"] + self.__solar_power: float = data["solar_power"] self.__generating_status: bool = None self.type = "solar panel" self.name = self._name() @@ -205,10 +198,7 @@ class LoadPowerSensor(PowerSensor): def __init__(self, data, controller): """Initialize the load power sensor.""" super().__init__(data, controller) - if data["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: - self.__load_power: float = data["load_power"] - if data["resource_type"] == TESLA_RESOURCE_TYPE_BATTERY: - self.__load_power: float = data["power_reading"][0]["load_power"] + self.__load_power: float = data["load_power"] self.type = "load power" self.name = self._name() self.uniq_name = self._uniq_name() @@ -242,10 +232,7 @@ class GridPowerSensor(PowerSensor): def __init__(self, data, controller): """Initialize the grid power sensor.""" super().__init__(data, controller) - if data["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: - self.__grid_power: float = data["grid_power"] - if data["resource_type"] == TESLA_RESOURCE_TYPE_BATTERY: - self.__grid_power: float = data["power_reading"][0]["grid_power"] + self.__grid_power: float = data["grid_power"] self.type = "grid power" self.name = self._name() self.uniq_name = self._uniq_name() @@ -271,7 +258,7 @@ def refresh(self) -> None: class BatteryPowerSensor(PowerSensor): - """Home-assistant class for grid power sensors for Tesla Energy Sites (Solar Panels). + """Home-assistant class for battery power sensors for Tesla Energy Sites (Solar Panels). This is intended to be partially inherited by a Home-Assitant entity. """ @@ -279,17 +266,17 @@ class BatteryPowerSensor(PowerSensor): def __init__(self, data, controller): """Initialize the battery power sensor.""" super().__init__(data, controller) - self.__battery_power: float = data["power_reading"][0]["battery_power"] + self.__battery_power: float = data["battery_power"] self.type = "battery power" self.name = self._name() self.uniq_name = self._uniq_name() def get_value(self) -> float: - """Return grid power.""" + """Return battery power.""" return self.__battery_power - def get_grid_power(self): - """Get grid power (grid import/export).""" + def get_battery_power(self): + """Get battery power (battery charge/discharge).""" return self.__battery_power def refresh(self) -> None: @@ -301,4 +288,4 @@ def refresh(self) -> None: data = self._controller.get_power_params(self._id) if data: - self.__battery_power = data["grid_power"] \ No newline at end of file + self.__battery_power = data["battery_power"] From 76b4fa586a87e3676b31fbca9fde104209ef6a60 Mon Sep 17 00:00:00 2001 From: shred86 Date: Tue, 16 Aug 2022 17:37:35 -0700 Subject: [PATCH 07/84] Fix incorrect variable name --- teslajsonpy/controller.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index e88c38d3..3924b6ca 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -443,8 +443,6 @@ async def connect( self._add_car_components(car) - _LOGGER.info("Running custom version of teslajsonpy with Powerwall support.") # TEMPORARY - for energysite in self.energysites: energysite_id = energysite["energy_site_id"] # Set initial values to initialize power sensors @@ -465,7 +463,7 @@ async def connect( self.__energysite_name[energysite_id] = energysite.get( "site_name", TESLA_DEFAULT_ENERGY_SITE_NAME ) - # Sites with powerwall only contain "solar_type" in "components" + # Sites with Powerwall only contain "solar_type" in "components" self.__energysite_type[energysite_id] = energysite["components"]["solar_type"] self.__lock[energysite_id] = asyncio.Lock() @@ -560,7 +558,7 @@ async def get_vehicles(self): @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) async def get_energysites(self): - """Get energy sites json from TeslaAPI and filter to solar and battery.""" + """Get energy sites json from TeslaAPI and filter to solar or battery.""" return [ p for p in (await self.api("PRODUCT_LIST"))["response"] @@ -1000,7 +998,7 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: async def _get_and_process_battery_data(battery_id: Text) -> None: async with self.__lock[battery_id]: - _LOGGER.debug("Updating energysite battery_data %s", battery_id) + _LOGGER.debug("Updating energysite battery data %s", battery_id) try: data = await self.api( "BATTERY_DATA", @@ -1011,7 +1009,7 @@ async def _get_and_process_battery_data(battery_id: Text) -> None: data = None if data and data["response"]: response = data["response"]["power_reading"][0] - self.__power[id] = response + self.__power[battery_id] = response async with self.__update_lock: cur_time = round(time.time()) From acfb9f63b1981fb13647a05e9514e0129ef9bbe4 Mon Sep 17 00:00:00 2001 From: shred86 Date: Tue, 16 Aug 2022 18:54:43 -0700 Subject: [PATCH 08/84] Add tests and minor updates --- teslajsonpy/homeassistant/power.py | 6 +- tests/tesla_mock.py | 537 +++++++++++++++--- .../homeassistant/test_power_sensor.py | 174 +++--- 3 files changed, 535 insertions(+), 182 deletions(-) diff --git a/teslajsonpy/homeassistant/power.py b/teslajsonpy/homeassistant/power.py index 72b7adb2..1818df95 100644 --- a/teslajsonpy/homeassistant/power.py +++ b/teslajsonpy/homeassistant/power.py @@ -207,7 +207,7 @@ def get_value(self) -> float: """Return load power.""" return self.__load_power - def get_load_power(self): + def get_power(self): """Get load power (home consumption).""" return self.__load_power @@ -241,7 +241,7 @@ def get_value(self) -> float: """Return grid power.""" return self.__grid_power - def get_grid_power(self): + def get_power(self): """Get grid power (grid import/export).""" return self.__grid_power @@ -275,7 +275,7 @@ def get_value(self) -> float: """Return battery power.""" return self.__battery_power - def get_battery_power(self): + def get_power(self): """Get battery power (battery charge/discharge).""" return self.__battery_power diff --git a/tests/tesla_mock.py b/tests/tesla_mock.py index 4356d650..c9c7d107 100644 --- a/tests/tesla_mock.py +++ b/tests/tesla_mock.py @@ -18,7 +18,7 @@ def __init__(self, monkeypatch) -> None: """ self._monkeypatch = monkeypatch - self._monkeypatch.setattr(Controller, "connect", self.mock_connect) + # self._monkeypatch.setattr(Controller, "connect", self.mock_connect) self._monkeypatch.setattr(Controller, "command", self.mock_command) self._monkeypatch.setattr(Controller, "api", self.mock_api) # self._monkeypatch.setattr( @@ -37,6 +37,8 @@ def __init__(self, monkeypatch) -> None: # Controller, "get_state_params", self.mock_get_state_params # ) self._monkeypatch.setattr(Controller, "get_vehicles", self.mock_get_vehicles) + self._monkeypatch.setattr(Controller, "get_energysites", self.mock_get_energysites) + self._monkeypatch.setattr(Controller, "get_site_config", self.mock_get_site_config) # self._monkeypatch.setattr( # Controller, "get_last_update_time", self.mock_get_last_update_time # ) @@ -44,19 +46,23 @@ def __init__(self, monkeypatch) -> None: self._monkeypatch.setattr( Controller, "get_power_params", self.mock_get_power_params ) + self._vehicle_product_list = copy.deepcopy(VEHICLE_PRODUCT_LIST) + self._site_product_list = copy.deepcopy(ENERGYSITE_PRODUCT_LIST) + self._site_config = copy.deepcopy(SITE_CONFIG) self._drive_state = copy.deepcopy(DRIVE_STATE) self._climate_state = copy.deepcopy(CLIMATE_STATE) self._charge_state = copy.deepcopy(CHARGE_STATE) self._gui_settings = copy.deepcopy(GUI_SETTINGS) self._vehicle_state = copy.deepcopy(VEHICLE_STATE) self._vehicle_config = copy.deepcopy(VEHICLE_CONFIG) - self._energysite_config = copy.deepcopy(ENERGYSITE_CONFIG) - self._energysite_state = copy.deepcopy(ENERGYSITE_STATE) - self._energysite_state_unknown_grid = copy.deepcopy( - ENERGYSITE_STATE_UNKNOWN_GRID + self._solar_combined_data = copy.deepcopy(SOLAR_COMBINED_DATA) + self._solar_combined_data_no_name = copy.deepcopy(SOLAR_COMBINED_DATA_NO_NAME) + self._battery_combined_data = copy.deepcopy(BATTERY_COMBINED_DATA) + self._site_config = copy.deepcopy(SITE_CONFIG) + self._site_state = copy.deepcopy(SITE_STATE) + self._site_state_unknown_grid = copy.deepcopy( + SITE_STATE_UNKNOWN_GRID ) - self._energysite_config_no_name = copy.deepcopy(ENERGYSITE_CONFIG_NO_NAME) - self._vehicle = copy.deepcopy(VEHICLE) self._vehicle["drive_state"] = self._drive_state self._vehicle["climate_state"] = self._climate_state @@ -120,6 +126,16 @@ def mock_get_vehicles(self, *args, **kwargs): """Mock controller's get_vehicles method.""" return self.controller_get_vehicles() + def mock_get_energysites(self, *args, **kwargs): + # pylint: disable=unused-argument + """Mock controller's get_energysites method.""" + return self.controller_get_energysites() + + def mock_get_site_config(self, *args, **kwargs): + # pylint: disable=unused-argument + """Mock controller's get_site_config method.""" + return self.controller_get_site_config() + def mock_get_last_update_time(self, *args, **kwargs): # pylint: disable=unused-argument """Mock controller's get_last_update_time method.""" @@ -155,11 +171,11 @@ def controller_get_climate_params(self): def controller_get_power_params(self): """Monkeypatch for controller.get_climate_params().""" - return self._energysite_state + return self._site_state def controller_get_power_unknown_grid_params(self): """Monkeypatch for controller.get_climate_params().""" - return self._energysite_state_unknown_grid + return self._site_state_unknown_grid def controller_get_drive_params(self): """Monkeypatch for controller.get_drive_params().""" @@ -173,10 +189,17 @@ def controller_get_state_params(self): """Monkeypatch for controller.get_state_params().""" return self._vehicle_state - @staticmethod - def controller_get_vehicles(): + async def controller_get_vehicles(self): """Monkeypatch for controller.get_vehicles().""" - return {SAMPLE_VEHICLE} + return self._vehicle_product_list + + async def controller_get_energysites(self): + """Monkeypatch for controller.get_energysites().""" + return self._site_product_list + + async def controller_get_site_config(self): + """Monkeypatch for controller.get_site_config().""" + return self._site_config @staticmethod async def controller_update(): @@ -204,21 +227,25 @@ def data_request_vehicle_state(self): """Simulate the result of vehicle state data request.""" return self._vehicle_state - def data_request_energy_site(self): - """Similate the result of energy site data request.""" - return self._energysite_config + def data_request_solar_combined_data(self): + """Similate the result of combined product list & site config request.""" + return self._solar_combined_data + + def data_request_solar_combined_data_no_name(self): + """Similate the result of combined product list & site config without name.""" + return self._solar_combined_data_no_name - def data_request_energy_site_no_name(self): - """Similate the result of energy site data request without a name.""" - return self._energysite_config_no_name + def data_request_battery_combined_data(self): + """Similate the result of a battery site from product_list.""" + return self._battery_combined_data - def data_request_energy_state(self): - """Similate the result of energy status data request.""" - return self._energysite_state + def data_request_site_state(self): + """Similate the result of site state request.""" + return self._site_state - def data_request_energy_state_unknown_grid(self): - """Similate the result of energy status unknown grid data request.""" - return self._energysite_state_unknown_grid + def data_request_site_state_unknown_grid(self): + """Similate the result of site state with unknown grid data request.""" + return self._site_state_unknown_grid @staticmethod def command_ok(): @@ -236,27 +263,28 @@ def command_ok(): "error_description": "", } - VIN = "5YJSA11111111111" CAR_ID = 12345678901234567 - -SAMPLE_VEHICLE = { - "id": 12345678901234567, - "vehicle_id": 1234567890, - "vin": "5YJSA11111111111", - "display_name": "Nikola 2.0", - "option_codes": "MDLS,RENA,AF02,APF1,APH2,APPB,AU01,BC0R,BP00,BR00,BS00,CDM0,CH05,PBCW,CW00,DCF0,DRLH,DSH7,DV4W,FG02,FR04,HP00,IDBA,IX01,LP01,ME02,MI01,PF01,PI01,PK00,PS01,PX00,PX4D,QTVB,RFP2,SC01,SP00,SR01,SU01,TM00,TP03,TR00,UTAB,WTAS,X001,X003,X007,X011,X013,X021,X024,X027,X028,X031,X037,X040,X044,YFFC,COUS", - "color": None, - "tokens": ["abcdef1234567890", "1234567890abcdef"], - "state": "online", - "in_service": False, - "id_s": "12345678901234567", - "calendar_enabled": True, - "api_version": 7, - "backseat_token": None, - "backseat_token_updated_at": None, -} +VEHICLE_PRODUCT_LIST = [ + { + "id": 12345678901234567, + "vehicle_id": 1234567890, + "vin": "5YJSA11111111111", + "display_name": "Nikola 2.0", + "option_codes": "AD15,MDL3,PBSB,RENA,BT37,ID3W,RF3G,S3PB,DRLH,DV2W,W39B,APF0,COUS,BC3B,CH07,PC30,FC3P,FG31,GLFR,HL31,HM31,IL31,LTPB,MR31,FM3B,RS3H,SA3P,STCP,SC04,SU3C,T3CA,TW00,TM00,UT3P,WR00,AU3P,APH3,AF00,ZCST,MI00,CDM0", + "color": None, + "access_type": "OWNER", + "tokens": ["abcdef1234567890", "1234567890abcdef"], + "state": "online", + "in_service": False, + "id_s": "12345678901234567", + "calendar_enabled": True, + "api_version": 36, + "backseat_token": None, + "backseat_token_updated_at": None, + } +] DRIVE_STATE = { "gps_as_of": 1538363883, @@ -470,14 +498,111 @@ def command_ok(): "vehicle_config": None, } -# Example config for solar only (no powerall) and Neurio -# Combination of PRODUCT_LIST, SITE_DATA & SITE_CONFIG from Controller -ENERGYSITE_CONFIG = { - "energy_site_id": 1234567890, +# Likely a rare setup simulating a home with two energy sites,one solar system with and +# another without Powerwall. However, this enables testing multiple scenarios. +ENERGYSITE_PRODUCT_LIST = [ + { + "energy_site_id": 12345, + "resource_type": "solar", + "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", + "asset_site_id": "12345", + "solar_power": 2260, + "solar_type": "pv_panel", + "storm_mode_enabled": None, + "powerwall_onboarding_settings_set": None, + "sync_grid_alert_enabled": False, + "breaker_alert_enabled": False, + "components": { + "battery": False, + "solar": True, + "solar_type": "pv_panel", + "grid": True, + "load_meter": True, + "market_type": "residential", + }, + }, + { + "energy_site_id": 67890, + "resource_type": "battery", + "site_name": "My Battery Home", + "id": "212dbc27-333c-45b1-81bb-31e2zd2fs2cm", + "gateway_id": "67890", + "asset_site_id": "67890", + "energy_left": 2864.7368421052633, + "total_pack_energy": 14070, + "percentage_charged": 20.360603000037408, + "battery_type": "ac_powerwall", + "backup_capable": True, + "battery_power": 3080, + "storm_mode_enabled": True, + "powerwall_onboarding_settings_set": True, + "sync_grid_alert_enabled": True, + "breaker_alert_enabled": True, + "components": { + "battery": True, + "battery_type": "ac_powerwall", + "solar": True, + "solar_type": "pv_panel", + "grid": True, + "load_meter": True, + "market_type": "residential", + }, + } +] + +SITE_CONFIG = { + "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", + "site_name": "My Solar Home", + "site_number": "STE16235182-31459", + "installation_date": "2022-02-07T13:51:26-07:00", + "user_settings": { + "storm_mode_enabled": None, + "powerwall_onboarding_settings_set": None, + "sync_grid_alert_enabled": False, + "breaker_alert_enabled": False + }, + "components": { + "solar": True, + "solar_type": "pv_panel", + "battery": False, + "grid": True, + "backup": False, + "gateway": "gateway_type_none", + "load_meter": True, + "tou_capable": False, + "storm_mode_capable": False, + "flex_energy_request_capable": False, + "car_charging_data_supported": False, + "off_grid_vehicle_charging_reserve_supported": False, + "vehicle_charging_performance_view_enabled": False, + "vehicle_charging_solar_offset_view_enabled": False, + "battery_solar_offset_view_enabled": False, + "energy_service_self_scheduling_enabled": True, + "rate_plan_manager_supported": True, + "configurable": False, + "grid_services_enabled": False + }, + "installation_time_zone": "America/Los_Angeles", + "time_zone_offset": -420, + "geolocation": { + "latitude": 31.12345600000001, + "longitude": -119.1234567 + }, + "address": { + "address_line1": "1234 Tesla Energy Ave", + "city": "Austin", + "state": "TX", + "zip": "12345", + "country": "US" + } +} +# Data combined from PRODUCT_LIST & SITE_CONFIG which occurs in Controller.connect() +SOLAR_COMBINED_DATA = { + "energy_site_id": 12345, "resource_type": "solar", - "id": "c31d46d3-d3f3-4319-a2cb-34719c30243d", - "asset_site_id": "3f345132-3c13-2cda-351a-341fq3a2dab2", - "solar_power": 4230, + "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", + "asset_site_id": "12345", + "solar_power": 0, "solar_type": "pv_panel", "storm_mode_enabled": None, "powerwall_onboarding_settings_set": None, @@ -500,41 +625,40 @@ def command_ok(): "vehicle_charging_solar_offset_view_enabled": False, "battery_solar_offset_view_enabled": False, "energy_service_self_scheduling_enabled": True, + "rate_plan_manager_supported": True, "configurable": False, - "grid_services_enabled": False + "grid_services_enabled": False, }, - "grid_power": -984.5400390625, - "load_power": 3245.4599609375, - "site_name": "My Home", - "site_number": "STE32474374-31631", - "installation_date": "2021-03-01T12:58:33-07:00", + "load_power": 0, + "grid_power": 0, + "battery_power": 0, + "site_name": "My Solar Home", + "site_number": "STE16235182-31459", + "installation_date": "2022-02-07T13:51:26-07:00", "user_settings": { "storm_mode_enabled": None, "powerwall_onboarding_settings_set": None, "sync_grid_alert_enabled": False, - "breaker_alert_enabled": False + "breaker_alert_enabled": False, }, "installation_time_zone": "America/Los_Angeles", "time_zone_offset": -420, - "geolocation": { - "latitude": 31.32463100000001, - "longitude": -103.1425259 - }, + "geolocation": {"latitude": 31.12345600000001, "longitude": -119.1234567}, "address": { - "address_line1": "1234 Tesla Solar Ave", + "address_line1": "1234 Tesla Energy Ave", "city": "Austin", "state": "TX", - "zip": "123456", - "country": "US" - } + "zip": "12345", + "country": "US", + }, } -ENERGYSITE_CONFIG_NO_NAME = { - "energy_site_id": 1234567890, +SOLAR_COMBINED_DATA_NO_NAME = { + "energy_site_id": 12345, "resource_type": "solar", - "id": "c31d46d3-d3f3-4319-a2cb-34719c30243d", - "asset_site_id": "3f345132-3c13-2cda-351a-341fq3a2dab2", - "solar_power": 4230, + "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", + "asset_site_id": "12345", + "solar_power": 0, "solar_type": "pv_panel", "storm_mode_enabled": None, "powerwall_onboarding_settings_set": None, @@ -557,35 +681,65 @@ def command_ok(): "vehicle_charging_solar_offset_view_enabled": False, "battery_solar_offset_view_enabled": False, "energy_service_self_scheduling_enabled": True, + "rate_plan_manager_supported": True, "configurable": False, - "grid_services_enabled": False + "grid_services_enabled": False, }, - "grid_power": -984.5400390625, - "load_power": 3245.4599609375, - "site_number": "STE32474374-31631", - "installation_date": "2021-03-01T12:58:33-07:00", + "load_power": 0, + "grid_power": 0, + "battery_power": 0, + "site_number": "STE16235182-31459", + "installation_date": "2022-02-07T13:51:26-07:00", "user_settings": { "storm_mode_enabled": None, "powerwall_onboarding_settings_set": None, "sync_grid_alert_enabled": False, - "breaker_alert_enabled": False + "breaker_alert_enabled": False, }, "installation_time_zone": "America/Los_Angeles", "time_zone_offset": -420, - "geolocation": { - "latitude": 31.32463100000001, - "longitude": -103.1425259 - }, + "geolocation": {"latitude": 31.12345600000001, "longitude": -119.1234567}, "address": { - "address_line1": "1234 Tesla Solar Ave", + "address_line1": "1234 Tesla Energy Ave", "city": "Austin", "state": "TX", - "zip": "123456", - "country": "US" - } + "zip": "12345", + "country": "US", + }, +} +# Data added from Controller.connect() initialization (solar_power, load_power, etc.) +BATTERY_COMBINED_DATA = { + "energy_site_id": 67890, + "resource_type": "battery", + "site_name": "My Battery Home", + "id": "212dbc27-333c-45b1-81bb-31e2zd2fs2cm", + "gateway_id": "67890", + "asset_site_id": "67890", + "energy_left": 2864.7368421052633, + "total_pack_energy": 14070, + "percentage_charged": 20.360603000037408, + "battery_type": "ac_powerwall", + "backup_capable": True, + "battery_power": 0, + "storm_mode_enabled": True, + "powerwall_onboarding_settings_set": True, + "sync_grid_alert_enabled": True, + "breaker_alert_enabled": True, + "components": { + "battery": True, + "battery_type": "ac_powerwall", + "solar": True, + "solar_type": "pv_panel", + "grid": True, + "load_meter": True, + "market_type": "residential", + }, + "solar_power": 0, + "load_power": 0, + "grid_power": 0, } -ENERGYSITE_STATE = { +SITE_STATE = { "solar_power": 7720, "energy_left": 0, "total_pack_energy": 1, @@ -601,12 +755,223 @@ def command_ok(): "storm_mode_active": False, "timestamp": "2022-07-28T17:11:27Z", "wall_connectors": None - } +} -ENERGYSITE_STATE_UNKNOWN_GRID = { +SITE_STATE_UNKNOWN_GRID = { "id": 12345678901234567, "timestamp": "2011-01-01", "solar_power": 1750, "grid_status": "Unknown", "grid_services_active": False, } +# Example of battery_data response for future tests +BATTERY_DATA = { + "energy_site_id": 123456789, + "resource_type": "battery", + "site_name": "My Battery Home", + "id": "XXX", + "gateway_id": "XXX", + "asset_site_id": "XXX", + "energy_left": 12650.052631578948, + "total_pack_energy": 14069, + "percentage_charged": 20.360603000037408, + "battery_type": "ac_powerwall", + "backup_capable": True, + "battery_power": 3080, + "storm_mode_enabled": True, + "powerwall_onboarding_settings_set": True, + "sync_grid_alert_enabled": True, + "breaker_alert_enabled": True, + "components": { + "solar": True, + "solar_type": "pv_panel", + "battery": True, + "grid": True, + "backup": True, + "gateway": "teg", + "load_meter": True, + "tou_capable": True, + "storm_mode_capable": True, + "flex_energy_request_capable": False, + "car_charging_data_supported": False, + "off_grid_vehicle_charging_reserve_supported": False, + "vehicle_charging_performance_view_enabled": False, + "vehicle_charging_solar_offset_view_enabled": False, + "battery_solar_offset_view_enabled": True, + "solar_value_enabled": True, + "energy_value_header": "Energy Value", + "energy_value_subheader": "Estimated Value", + "show_grid_import_battery_source_cards": True, + "backup_time_remaining_enabled": True, + "rate_plan_manager_supported": True, + "battery_type": "ac_powerwall", + "configurable": False, + "grid_services_enabled": False, + "customer_preferred_export_rule": "battery_ok", + "net_meter_mode": "battery_ok" + }, + "grid_status": "Active", + "backup": { + "backup_reserve_percent": 100, + "events": [ + { + "timestamp": "2022-07-12T06:56:55+10:00", + "duration": 38773 + }, + { + "timestamp": "2022-07-11T20:46:25+10:00", + "duration": 66479 + }, + { + "timestamp": "2022-06-29T11:35:43+10:00", + "duration": 842030 + }, + { + "timestamp": "2022-06-18T15:28:35+10:00", + "duration": 1013486 + }, + { + "timestamp": "2022-06-15T15:43:20+10:00", + "duration": 210737 + }, + { + "timestamp": "2022-06-10T08:26:12+10:00", + "duration": 47649 + }, + { + "timestamp": "2022-06-03T13:58:52+10:00", + "duration": 443079 + }, + { + "timestamp": "2022-05-15T10:46:58+10:00", + "duration": 31389950 + }, + { + "timestamp": "2022-05-14T15:33:38+10:00", + "duration": 1279604 + }, + { + "timestamp": "2022-05-07T19:39:07+10:00", + "duration": 901817 + }, + { + "timestamp": "2022-04-23T08:26:14+10:00", + "duration": 437693 + }, + { + "timestamp": "2022-04-22T19:14:33+10:00", + "duration": 757615 + }, + { + "timestamp": "2022-04-14T11:54:35+10:00", + "duration": 581358 + }, + { + "timestamp": "2022-04-06T22:26:41+10:00", + "duration": 65188 + }, + { + "timestamp": "2022-04-03T22:12:07+10:00", + "duration": 654161 + }, + { + "timestamp": "2022-04-03T21:57:36+10:00", + "duration": 798912 + }, + { + "timestamp": "2022-04-03T18:51:05+10:00", + "duration": 67764 + }, + { + "timestamp": "2022-04-03T17:22:58+10:00", + "duration": 641782 + }, + { + "timestamp": "2022-04-03T17:21:19+10:00", + "duration": 69942 + }, + { + "timestamp": "2022-04-03T06:34:17+10:00", + "duration": 232350 + }, + { + "timestamp": "2022-04-02T19:05:41+10:00", + "duration": 47104 + }, + { + "timestamp": "2022-04-02T09:35:18+10:00", + "duration": 258895 + }, + { + "timestamp": "2022-04-02T05:21:14+10:00", + "duration": 63814 + }, + { + "timestamp": "2022-04-01T11:59:57+10:00", + "duration": 586849 + }, + { + "timestamp": "2022-04-01T11:50:56+10:00", + "duration": 457199 + }, + { + "timestamp": "2022-04-01T11:48:21+10:00", + "duration": 51065 + }, + { + "timestamp": "2022-04-01T11:47:23+10:00", + "duration": 41783 + }, + { + "timestamp": "2022-04-01T11:01:46+10:00", + "duration": 73278 + }, + { + "timestamp": "2022-03-31T17:12:00+10:00", + "duration": 45838 + }, + { + "timestamp": "2022-03-24T16:28:07+10:00", + "duration": 122233 + }, + { + "timestamp": "2022-03-24T06:15:44+10:00", + "duration": 5932791 + }, + { + "timestamp": "2022-03-23T17:01:37+10:00", + "duration": 210322 + }, + { + "timestamp": "2022-03-23T16:11:27+10:00", + "duration": 2608373 + }, + { + "timestamp": "2022-03-21T21:04:54+10:00", + "duration": 296080 + } + ], + "events_count": 0, + "total_events": 0 + }, + "user_settings": { + "storm_mode_enabled": True, + "powerwall_onboarding_settings_set": True, + "sync_grid_alert_enabled": False, + "breaker_alert_enabled": False + }, + "default_real_mode": "backup", + "operation": "backup", + "installation_date": "2022-03-21T17:15:23+10:00", + "power_reading": [ + { + "timestamp": "2022-08-16T15:23:24+10:00", + "load_power": 329, + "solar_power": 709, + "grid_power": 2930, + "battery_power": -3310, + "generator_power": 0 + } + ], + "battery_count": 1 +} \ No newline at end of file diff --git a/tests/unit_tests/homeassistant/test_power_sensor.py b/tests/unit_tests/homeassistant/test_power_sensor.py index 55ad84b4..9cb39114 100644 --- a/tests/unit_tests/homeassistant/test_power_sensor.py +++ b/tests/unit_tests/homeassistant/test_power_sensor.py @@ -3,162 +3,150 @@ import pytest from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.power import PowerSensor, SolarPowerSensor, LoadPowerSensor, GridPowerSensor +from teslajsonpy.homeassistant.power import ( + SolarPowerSensor, + LoadPowerSensor, + GridPowerSensor, + BatteryPowerSensor, +) from tests.tesla_mock import TeslaMock -def test_device_class(monkeypatch): - """Test device_class().""" - - _mock = TeslaMock(monkeypatch) +@pytest.mark.asyncio +async def test_energysite_setup(monkeypatch): + """Test setup of energysites in Controller.connect().""" + TeslaMock(monkeypatch) _controller = Controller(None) + await _controller.connect() - _data = _mock.data_request_energy_site() - _sensor = PowerSensor(_data, _controller) - - assert _sensor.device_class == "power" + assert _controller.energysites is not None + assert _controller.energysites[0]["energy_site_id"] == 12345 + assert _controller.energysites[1]["energy_site_id"] == 67890 - _sensor = SolarPowerSensor(_data, _controller) - - assert _sensor.type == "solar panel" - assert _sensor.name == "My Home solar panel" - - _sensor = LoadPowerSensor(_data, _controller) - - assert _sensor.type == "load power" - assert _sensor.name == "My Home load power" - - _sensor = GridPowerSensor(_data, _controller) - - assert _sensor.type == "grid power" - assert _sensor.name == "My Home grid power" - -def test_site_with_name(monkeypatch): - """Test site with no site_name in json data.""" +@pytest.mark.asyncio +async def test_solar_power_sensor(monkeypatch): + """Test SolarPowerSensor class.""" _mock = TeslaMock(monkeypatch) _controller = Controller(None) + # Test a solar only site (no Powerwall) + _data = _mock.data_request_solar_combined_data() + _sensor = SolarPowerSensor(_data, _controller) - _data = _mock.data_request_energy_site() - _sensor = PowerSensor(_data, _controller) + assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" + assert _sensor.uniq_name == f"{_sensor._energy_site_id} {_sensor.type}" + assert _sensor.get_power() == 0 + # Test a battery site (Powerwall) + _data = _mock.data_request_battery_combined_data() + _sensor = SolarPowerSensor(_data, _controller) - assert _sensor.site_name() == "My Home" + assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" + assert _sensor.uniq_name == f"{_sensor._energy_site_id} {_sensor.type}" + assert _sensor.get_power() == 0 -def test_site_without_name(monkeypatch): - """Test site with no site_name in json data.""" +@pytest.mark.asyncio +async def test_load_power_sensor(monkeypatch): + """Test LoadPowerSensor class.""" _mock = TeslaMock(monkeypatch) _controller = Controller(None) + # Test a solar only site (no Powerwall) + _data = _mock.data_request_solar_combined_data() + _sensor = LoadPowerSensor(_data, _controller) - _data = _mock.data_request_energy_site_no_name() - _sensor = PowerSensor(_data, _controller) + assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" + assert _sensor.uniq_name == f"{_sensor._energy_site_id} {_sensor.type}" + assert _sensor.get_power() == 0 + # Test a battery site (Powerwall) + _data = _mock.data_request_battery_combined_data() + _sensor = LoadPowerSensor(_data, _controller) - assert _sensor.site_name() == "My Home" + assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" + assert _sensor.uniq_name == f"{_sensor._energy_site_id} {_sensor.type}" + assert _sensor.get_power() == 0 -def test_get_solar_power_on_init(monkeypatch): - """Test get_power() after initialization.""" +@pytest.mark.asyncio +async def test_grid_power_sensor(monkeypatch): + """Test GridPowerSensor class.""" _mock = TeslaMock(monkeypatch) _controller = Controller(None) + # Test a solar only site (no Powerwall) + _data = _mock.data_request_solar_combined_data() + _sensor = GridPowerSensor(_data, _controller) - _data = _mock.data_request_energy_site() - _sensor = SolarPowerSensor(_data, _controller) + assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" + assert _sensor.uniq_name == f"{_sensor._energy_site_id} {_sensor.type}" + assert _sensor.get_power() == 0 + # Test a battery site (Powerwall) + _data = _mock.data_request_battery_combined_data() + _sensor = GridPowerSensor(_data, _controller) - assert _sensor is not None - assert _sensor.get_power() == 4230 + assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" + assert _sensor.uniq_name == f"{_sensor._energy_site_id} {_sensor.type}" + assert _sensor.get_power() == 0 -def test_get_load_power_on_init(monkeypatch): - """Test get_load_power() after initialization.""" +@pytest.mark.asyncio +async def test_battery_power_sensor(monkeypatch): + """Test BatteryPowerSensor class.""" _mock = TeslaMock(monkeypatch) _controller = Controller(None) + _data = _mock.data_request_battery_combined_data() + _sensor = BatteryPowerSensor(_data, _controller) - _data = _mock.data_request_energy_site() - _sensor = LoadPowerSensor(_data, _controller) + assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" + assert _sensor.uniq_name == f"{_sensor._energy_site_id} {_sensor.type}" + assert _sensor.get_power() == 0 - assert _sensor is not None - assert _sensor.get_load_power() == 3245.4599609375 - -def test_get_grid_power_on_init(monkeypatch): - """Test get_grid_power() after initialization.""" +def test_site_without_name(monkeypatch): + """Test site with no site_name in json data.""" _mock = TeslaMock(monkeypatch) _controller = Controller(None) + _data = _mock.data_request_solar_combined_data_no_name() + _sensor = LoadPowerSensor(_data, _controller) - _data = _mock.data_request_energy_site() - _sensor = GridPowerSensor(_data, _controller) + assert _sensor.site_name() == "My Home" - assert _sensor is not None - assert _sensor.get_grid_power() == -984.5400390625 @pytest.mark.asyncio async def test_get_power_after_update(monkeypatch): """Test get_power() after an update.""" - _mock = TeslaMock(monkeypatch) _controller = Controller(None) - - _data = _mock.data_request_energy_site() + _data = _mock.data_request_solar_combined_data() _data["solar_power"] = 1800 + _data["load_power"] = 1800 + _data["grid_power"] = 1800 + _sensor = SolarPowerSensor(_data, _controller) await _sensor.async_update() - - assert _sensor is not None - assert not _sensor.get_power() is None assert _sensor.get_power() == 7720 -@pytest.mark.asyncio -async def test_get_load_power_after_update(monkeypatch): - """Test get_load_power() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_energy_site() - _data["load_power"] = 1800 _sensor = LoadPowerSensor(_data, _controller) await _sensor.async_update() + assert _sensor.get_power() == 4517.14990234375 - assert _sensor is not None - assert not _sensor.get_load_power() is None - assert _sensor.get_load_power() == 4517.14990234375 - -@pytest.mark.asyncio -async def test_get_grid_power_after_update(monkeypatch): - """Test get_grid_power() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_energy_site() - _data["grid_power"] = 1800 _sensor = GridPowerSensor(_data, _controller) await _sensor.async_update() + assert _sensor.get_power() == -3202.85009765625 - assert _sensor is not None - assert not _sensor.get_grid_power() is None - assert _sensor.get_grid_power() == -3202.85009765625 @pytest.mark.asyncio async def test_get_power_after_update_with_unknown_status(monkeypatch): - """Test get_power() after an update with an unknown grid status.""" - + """Test get_power() after an update with unknown grid status.""" _mock = TeslaMock(monkeypatch) monkeypatch.setattr( Controller, "get_power_params", _mock.mock_get_power_unknown_grid_params ) _controller = Controller(None) - - _data = _mock.data_request_energy_site() - _data["solar_power"] = 1800 + _data = _mock.data_request_solar_combined_data() _sensor = SolarPowerSensor(_data, _controller) await _sensor.async_update() - - assert _sensor is not None - assert not _sensor.get_power() is None - assert _sensor.get_power() == 1750 \ No newline at end of file + assert _sensor.get_power() == 1750 From d4cc3dbfb8d0d80cbde47eecbbe6243db20c10b2 Mon Sep 17 00:00:00 2001 From: shred86 Date: Tue, 16 Aug 2022 19:08:35 -0700 Subject: [PATCH 09/84] Round power values --- teslajsonpy/homeassistant/power.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/teslajsonpy/homeassistant/power.py b/teslajsonpy/homeassistant/power.py index 1818df95..6d091607 100644 --- a/teslajsonpy/homeassistant/power.py +++ b/teslajsonpy/homeassistant/power.py @@ -147,11 +147,11 @@ def __init__(self, data, controller): def get_value(self) -> float: """Return solar power.""" - return self.__solar_power + return round(self.__solar_power) def get_power(self): """Get solar power.""" - return self.__solar_power + return round(self.__solar_power) def get_generating_status(self): """Get generating status.""" @@ -205,11 +205,11 @@ def __init__(self, data, controller): def get_value(self) -> float: """Return load power.""" - return self.__load_power + return round(self.__load_power) def get_power(self): """Get load power (home consumption).""" - return self.__load_power + return round(self.__load_power) def refresh(self) -> None: """Refresh data. @@ -239,11 +239,11 @@ def __init__(self, data, controller): def get_value(self) -> float: """Return grid power.""" - return self.__grid_power + return round(self.__grid_power) def get_power(self): """Get grid power (grid import/export).""" - return self.__grid_power + return round(self.__grid_power) def refresh(self) -> None: """Refresh data. @@ -273,11 +273,11 @@ def __init__(self, data, controller): def get_value(self) -> float: """Return battery power.""" - return self.__battery_power + return round(self.__battery_power) def get_power(self): """Get battery power (battery charge/discharge).""" - return self.__battery_power + return round(self.__battery_power) def refresh(self) -> None: """Refresh data. From 2bf7d7b698eeb9dbe256b635e1e1d393d51387ec Mon Sep 17 00:00:00 2001 From: shred86 Date: Wed, 17 Aug 2022 10:40:27 -0700 Subject: [PATCH 10/84] Fix for storing battery site power data --- teslajsonpy/controller.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 3924b6ca..75e816dd 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -996,7 +996,7 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: response = data["response"] self.__power[energysite_id] = response - async def _get_and_process_battery_data(battery_id: Text) -> None: + async def _get_and_process_battery_data(energysite_id: Text, battery_id: Text) -> None: async with self.__lock[battery_id]: _LOGGER.debug("Updating energysite battery data %s", battery_id) try: @@ -1009,7 +1009,8 @@ async def _get_and_process_battery_data(battery_id: Text) -> None: data = None if data and data["response"]: response = data["response"]["power_reading"][0] - self.__power[battery_id] = response + # Store the response using energysite_id since that's how it's retrieved + self.__power[energysite_id] = response async with self.__update_lock: cur_time = round(time.time()) @@ -1086,12 +1087,12 @@ async def _get_and_process_battery_data(battery_id: Text) -> None: if not car_id: # do not update energy sites if car_id was a parameter. for energysite in self.energysites: + energysite_id = energysite["energy_site_id"] if energysite["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: - energysite_id = energysite["energy_site_id"] tasks.append(_get_and_process_site_data(energysite_id)) if energysite["resource_type"] == TESLA_RESOURCE_TYPE_BATTERY: battery_id = energysite["id"] - tasks.append(_get_and_process_battery_data(battery_id)) + tasks.append(_get_and_process_battery_data(energysite_id, battery_id)) return any(await asyncio.gather(*tasks)) From a3d923c387ffac63efbf482980a714b621a88236 Mon Sep 17 00:00:00 2001 From: shred86 Date: Wed, 17 Aug 2022 17:52:45 -0700 Subject: [PATCH 11/84] Initial commit for re-write support --- poetry.lock | 64 +++++- teslajsonpy/controller.py | 74 +++--- teslajsonpy/homeassistant/power.py | 16 +- tests/tesla_mock.py | 351 ++++++++++++----------------- 4 files changed, 252 insertions(+), 253 deletions(-) diff --git a/poetry.lock b/poetry.lock index 3f0b74fb..383a012a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -238,7 +238,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.4.3" +version = "6.4.4" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -591,12 +591,15 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "pygments" -version = "2.12.0" +version = "2.13.0" description = "Pygments is a syntax highlighting package written in Python." category = "dev" optional = false python-versions = ">=3.6" +[package.extras] +plugins = ["importlib-metadata"] + [[package]] name = "pylint" version = "2.13.9" @@ -1228,7 +1231,58 @@ colorama = [ {file = "colorama-0.4.5-py2.py3-none-any.whl", hash = "sha256:854bf444933e37f5824ae7bfc1e98d5bce2ebe4160d46b5edf346a89358e99da"}, {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] -coverage = [] +coverage = [ + {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, + {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, + {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, + {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, + {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, + {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, + {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, + {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, + {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, + {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, + {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, + {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, + {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, + {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, + {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, + {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, + {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, + {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, + {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, + {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, + {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, + {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, + {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, + {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, + {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, + {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, + {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, + {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, + {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, + {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, + {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, + {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, + {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, + {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, + {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, + {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, + {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, + {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, + {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, + {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, + {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, + {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, + {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, + {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, + {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, + {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, + {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, + {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, + {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, + {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, +] dill = [ {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, @@ -1478,8 +1532,8 @@ pydocstyle = [ ] pyflakes = [] pygments = [ - {file = "Pygments-2.12.0-py3-none-any.whl", hash = "sha256:dc9c10fb40944260f6ed4c688ece0cd2048414940f1cea51b8b226318411c519"}, - {file = "Pygments-2.12.0.tar.gz", hash = "sha256:5eb116118f9612ff1ee89ac96437bb6b49e8f04d8a13b514ba26f620208e26eb"}, + {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, + {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, ] pylint = [ {file = "pylint-2.13.9-py3-none-any.whl", hash = "sha256:705c620d388035bdd9ff8b44c5bcdd235bfb49d276d488dd2c8ff1736aa42526"}, diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 75e816dd..e99bed84 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -414,8 +414,6 @@ async def connect( self._last_attempted_update_time = round(time.time()) self.__update_lock = asyncio.Lock() - self.energysites = await self.get_energysites() - for car in cars: vin = car["vin"] if filtered_vins and vin not in filtered_vins: @@ -443,29 +441,10 @@ async def connect( self._add_car_components(car) + self.energysites = await self.get_energysites() + for energysite in self.energysites: energysite_id = energysite["energy_site_id"] - # Set initial values to initialize power sensors - # Actual values update immediately after setup when refresh is called - energysite["solar_power"] = 0 - energysite["load_power"] = 0 - energysite["grid_power"] = 0 - energysite["battery_power"] = 0 - - if energysite["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: - # Non-powerwall sites do not include "site_name" in "PRODUCT_LIST" endpoint - # Get "site_config" data for "site_name" and update energysite dict - site_config = await self.get_site_config(energysite_id) - energysite.update(site_config) - - self.__id_energysiteid_map[energysite["id"]] = energysite_id - self.__energysiteid_id_map[energysite_id] = energysite["id"] - self.__energysite_name[energysite_id] = energysite.get( - "site_name", TESLA_DEFAULT_ENERGY_SITE_NAME - ) - # Sites with Powerwall only contain "solar_type" in "components" - self.__energysite_type[energysite_id] = energysite["components"]["solar_type"] - self.__lock[energysite_id] = asyncio.Lock() self._add_energysite_components(energysite) @@ -559,18 +538,46 @@ async def get_vehicles(self): @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) async def get_energysites(self): """Get energy sites json from TeslaAPI and filter to solar or battery.""" - return [ + energysites = [ p for p in (await self.api("PRODUCT_LIST"))["response"] if p.get("resource_type") == TESLA_RESOURCE_TYPE_SOLAR or p.get("resource_type") == TESLA_RESOURCE_TYPE_BATTERY ] + for energysite in energysites: + energysite_id = energysite["energy_site_id"] + # Set initial values to initialize power sensors + # Actual values update immediately after setup when refresh is called + energysite["solar_power"] = 0 + energysite["load_power"] = 0 + energysite["grid_power"] = 0 + energysite["battery_power"] = 0 + + if energysite["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: + # Non-powerwall sites do not include "site_name" in "PRODUCT_LIST" endpoint + # Get "site_config" data for "site_name" and update energysite dict + site_config = await self.get_site_config(energysite_id) + energysite.update(site_config) + + self.__id_energysiteid_map[energysite["id"]] = energysite_id + self.__energysiteid_id_map[energysite_id] = energysite["id"] + self.__energysite_name[energysite_id] = energysite.get( + "site_name", TESLA_DEFAULT_ENERGY_SITE_NAME + ) + # Sites with Powerwall only contain "solar_type" in "components" + self.__energysite_type[energysite_id] = energysite["components"][ + "solar_type" + ] + + return energysites + @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) async def get_site_config(self, energysite_id): """Get site config json from TeslaAPI.""" - return (await self.api("SITE_CONFIG", - path_vars={"site_id": energysite_id}))["response"] + return (await self.api("SITE_CONFIG", path_vars={"site_id": energysite_id}))[ + "response" + ] @wake_up async def post( @@ -996,7 +1003,9 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: response = data["response"] self.__power[energysite_id] = response - async def _get_and_process_battery_data(energysite_id: Text, battery_id: Text) -> None: + async def _get_and_process_battery_data( + energysite_id: Text, battery_id: Text + ) -> None: async with self.__lock[battery_id]: _LOGGER.debug("Updating energysite battery data %s", battery_id) try: @@ -1092,7 +1101,9 @@ async def _get_and_process_battery_data(energysite_id: Text, battery_id: Text) - tasks.append(_get_and_process_site_data(energysite_id)) if energysite["resource_type"] == TESLA_RESOURCE_TYPE_BATTERY: battery_id = energysite["id"] - tasks.append(_get_and_process_battery_data(energysite_id, battery_id)) + tasks.append( + _get_and_process_battery_data(energysite_id, battery_id) + ) return any(await asyncio.gather(*tasks)) @@ -1628,8 +1639,13 @@ def set_vehicle_id_vin(self, vehicle_id: Text, vin: Text) -> None: self.__vehicle_id_vin_map[vehicle_id] = vin self.__vin_vehicle_id_map[vin] = vehicle_id + def get_power(self, energysite_id: Text, power_type: Text) -> int: + """Return solar power.""" + + return self.__power[energysite_id][power_type] + @property - def update_interval(self) -> int: + def update_interval(self) -> float: """Return update_interval. Returns diff --git a/teslajsonpy/homeassistant/power.py b/teslajsonpy/homeassistant/power.py index 6d091607..1818df95 100644 --- a/teslajsonpy/homeassistant/power.py +++ b/teslajsonpy/homeassistant/power.py @@ -147,11 +147,11 @@ def __init__(self, data, controller): def get_value(self) -> float: """Return solar power.""" - return round(self.__solar_power) + return self.__solar_power def get_power(self): """Get solar power.""" - return round(self.__solar_power) + return self.__solar_power def get_generating_status(self): """Get generating status.""" @@ -205,11 +205,11 @@ def __init__(self, data, controller): def get_value(self) -> float: """Return load power.""" - return round(self.__load_power) + return self.__load_power def get_power(self): """Get load power (home consumption).""" - return round(self.__load_power) + return self.__load_power def refresh(self) -> None: """Refresh data. @@ -239,11 +239,11 @@ def __init__(self, data, controller): def get_value(self) -> float: """Return grid power.""" - return round(self.__grid_power) + return self.__grid_power def get_power(self): """Get grid power (grid import/export).""" - return round(self.__grid_power) + return self.__grid_power def refresh(self) -> None: """Refresh data. @@ -273,11 +273,11 @@ def __init__(self, data, controller): def get_value(self) -> float: """Return battery power.""" - return round(self.__battery_power) + return self.__battery_power def get_power(self): """Get battery power (battery charge/discharge).""" - return round(self.__battery_power) + return self.__battery_power def refresh(self) -> None: """Refresh data. diff --git a/tests/tesla_mock.py b/tests/tesla_mock.py index c9c7d107..13a721be 100644 --- a/tests/tesla_mock.py +++ b/tests/tesla_mock.py @@ -37,8 +37,12 @@ def __init__(self, monkeypatch) -> None: # Controller, "get_state_params", self.mock_get_state_params # ) self._monkeypatch.setattr(Controller, "get_vehicles", self.mock_get_vehicles) - self._monkeypatch.setattr(Controller, "get_energysites", self.mock_get_energysites) - self._monkeypatch.setattr(Controller, "get_site_config", self.mock_get_site_config) + self._monkeypatch.setattr( + Controller, "get_energysites", self.mock_get_energysites + ) + self._monkeypatch.setattr( + Controller, "get_site_config", self.mock_get_site_config + ) # self._monkeypatch.setattr( # Controller, "get_last_update_time", self.mock_get_last_update_time # ) @@ -47,7 +51,6 @@ def __init__(self, monkeypatch) -> None: Controller, "get_power_params", self.mock_get_power_params ) self._vehicle_product_list = copy.deepcopy(VEHICLE_PRODUCT_LIST) - self._site_product_list = copy.deepcopy(ENERGYSITE_PRODUCT_LIST) self._site_config = copy.deepcopy(SITE_CONFIG) self._drive_state = copy.deepcopy(DRIVE_STATE) self._climate_state = copy.deepcopy(CLIMATE_STATE) @@ -60,9 +63,7 @@ def __init__(self, monkeypatch) -> None: self._battery_combined_data = copy.deepcopy(BATTERY_COMBINED_DATA) self._site_config = copy.deepcopy(SITE_CONFIG) self._site_state = copy.deepcopy(SITE_STATE) - self._site_state_unknown_grid = copy.deepcopy( - SITE_STATE_UNKNOWN_GRID - ) + self._site_state_unknown_grid = copy.deepcopy(SITE_STATE_UNKNOWN_GRID) self._vehicle = copy.deepcopy(VEHICLE) self._vehicle["drive_state"] = self._drive_state self._vehicle["climate_state"] = self._climate_state @@ -195,7 +196,7 @@ async def controller_get_vehicles(self): async def controller_get_energysites(self): """Monkeypatch for controller.get_energysites().""" - return self._site_product_list + return self._solar_combined_data async def controller_get_site_config(self): """Monkeypatch for controller.get_site_config().""" @@ -229,7 +230,7 @@ def data_request_vehicle_state(self): def data_request_solar_combined_data(self): """Similate the result of combined product list & site config request.""" - return self._solar_combined_data + return self._solar_combined_data[0] def data_request_solar_combined_data_no_name(self): """Similate the result of combined product list & site config without name.""" @@ -498,8 +499,7 @@ def command_ok(): "vehicle_config": None, } -# Likely a rare setup simulating a home with two energy sites,one solar system with and -# another without Powerwall. However, this enables testing multiple scenarios. +# Example of battery_data response with two energy sites for future tests ENERGYSITE_PRODUCT_LIST = [ { "energy_site_id": 12345, @@ -547,7 +547,7 @@ def command_ok(): "load_meter": True, "market_type": "residential", }, - } + }, ] SITE_CONFIG = { @@ -559,7 +559,7 @@ def command_ok(): "storm_mode_enabled": None, "powerwall_onboarding_settings_set": None, "sync_grid_alert_enabled": False, - "breaker_alert_enabled": False + "breaker_alert_enabled": False, }, "components": { "solar": True, @@ -580,78 +580,109 @@ def command_ok(): "energy_service_self_scheduling_enabled": True, "rate_plan_manager_supported": True, "configurable": False, - "grid_services_enabled": False + "grid_services_enabled": False, }, "installation_time_zone": "America/Los_Angeles", "time_zone_offset": -420, - "geolocation": { - "latitude": 31.12345600000001, - "longitude": -119.1234567 - }, + "geolocation": {"latitude": 31.12345600000001, "longitude": -119.1234567}, "address": { "address_line1": "1234 Tesla Energy Ave", "city": "Austin", "state": "TX", "zip": "12345", - "country": "US" - } + "country": "US", + }, } -# Data combined from PRODUCT_LIST & SITE_CONFIG which occurs in Controller.connect() -SOLAR_COMBINED_DATA = { - "energy_site_id": 12345, - "resource_type": "solar", - "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", - "asset_site_id": "12345", - "solar_power": 0, - "solar_type": "pv_panel", - "storm_mode_enabled": None, - "powerwall_onboarding_settings_set": None, - "sync_grid_alert_enabled": False, - "breaker_alert_enabled": False, - "components": { - "solar": True, +# Likely a rare setup simulating a home with two energy sites,one solar system with and +# another without Powerwall. However, this enables testing multiple scenarios. +# Simulates the list returned from Controller.get_energysites() +SOLAR_COMBINED_DATA = [ + { + "energy_site_id": 12345, + "resource_type": "solar", + "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", + "asset_site_id": "12345", + "solar_power": 0, "solar_type": "pv_panel", - "battery": False, - "grid": True, - "backup": False, - "gateway": "gateway_type_none", - "load_meter": True, - "tou_capable": False, - "storm_mode_capable": False, - "flex_energy_request_capable": False, - "car_charging_data_supported": False, - "off_grid_vehicle_charging_reserve_supported": False, - "vehicle_charging_performance_view_enabled": False, - "vehicle_charging_solar_offset_view_enabled": False, - "battery_solar_offset_view_enabled": False, - "energy_service_self_scheduling_enabled": True, - "rate_plan_manager_supported": True, - "configurable": False, - "grid_services_enabled": False, - }, - "load_power": 0, - "grid_power": 0, - "battery_power": 0, - "site_name": "My Solar Home", - "site_number": "STE16235182-31459", - "installation_date": "2022-02-07T13:51:26-07:00", - "user_settings": { "storm_mode_enabled": None, "powerwall_onboarding_settings_set": None, "sync_grid_alert_enabled": False, "breaker_alert_enabled": False, + "components": { + "solar": True, + "solar_type": "pv_panel", + "battery": False, + "grid": True, + "backup": False, + "gateway": "gateway_type_none", + "load_meter": True, + "tou_capable": False, + "storm_mode_capable": False, + "flex_energy_request_capable": False, + "car_charging_data_supported": False, + "off_grid_vehicle_charging_reserve_supported": False, + "vehicle_charging_performance_view_enabled": False, + "vehicle_charging_solar_offset_view_enabled": False, + "battery_solar_offset_view_enabled": False, + "energy_service_self_scheduling_enabled": True, + "rate_plan_manager_supported": True, + "configurable": False, + "grid_services_enabled": False, + }, + "load_power": 0, + "grid_power": 0, + "battery_power": 0, + "site_name": "My Solar Home", + "site_number": "STE16235182-31459", + "installation_date": "2022-02-07T13:51:26-07:00", + "user_settings": { + "storm_mode_enabled": None, + "powerwall_onboarding_settings_set": None, + "sync_grid_alert_enabled": False, + "breaker_alert_enabled": False, + }, + "installation_time_zone": "America/Los_Angeles", + "time_zone_offset": -420, + "geolocation": {"latitude": 31.12345600000001, "longitude": -119.1234567}, + "address": { + "address_line1": "1234 Tesla Energy Ave", + "city": "Austin", + "state": "TX", + "zip": "12345", + "country": "US", + }, }, - "installation_time_zone": "America/Los_Angeles", - "time_zone_offset": -420, - "geolocation": {"latitude": 31.12345600000001, "longitude": -119.1234567}, - "address": { - "address_line1": "1234 Tesla Energy Ave", - "city": "Austin", - "state": "TX", - "zip": "12345", - "country": "US", + { + "energy_site_id": 67890, + "resource_type": "battery", + "site_name": "My Battery Home", + "id": "212dbc27-333c-45b1-81bb-31e2zd2fs2cm", + "gateway_id": "67890", + "asset_site_id": "67890", + "energy_left": 2864.7368421052633, + "total_pack_energy": 14070, + "percentage_charged": 20.360603000037408, + "battery_type": "ac_powerwall", + "backup_capable": True, + "storm_mode_enabled": True, + "powerwall_onboarding_settings_set": True, + "sync_grid_alert_enabled": True, + "breaker_alert_enabled": True, + "components": { + "battery": True, + "battery_type": "ac_powerwall", + "solar": True, + "solar_type": "pv_panel", + "grid": True, + "load_meter": True, + "market_type": "residential", + }, + "solar_power": 0, + "load_power": 0, + "grid_power": 0, + "battery_power": 0, }, -} +] SOLAR_COMBINED_DATA_NO_NAME = { "energy_site_id": 12345, @@ -754,7 +785,7 @@ def command_ok(): "island_status": "island_status_unknown", "storm_mode_active": False, "timestamp": "2022-07-28T17:11:27Z", - "wall_connectors": None + "wall_connectors": None, } SITE_STATE_UNKNOWN_GRID = { @@ -808,157 +839,55 @@ def command_ok(): "configurable": False, "grid_services_enabled": False, "customer_preferred_export_rule": "battery_ok", - "net_meter_mode": "battery_ok" + "net_meter_mode": "battery_ok", }, "grid_status": "Active", "backup": { "backup_reserve_percent": 100, "events": [ - { - "timestamp": "2022-07-12T06:56:55+10:00", - "duration": 38773 - }, - { - "timestamp": "2022-07-11T20:46:25+10:00", - "duration": 66479 - }, - { - "timestamp": "2022-06-29T11:35:43+10:00", - "duration": 842030 - }, - { - "timestamp": "2022-06-18T15:28:35+10:00", - "duration": 1013486 - }, - { - "timestamp": "2022-06-15T15:43:20+10:00", - "duration": 210737 - }, - { - "timestamp": "2022-06-10T08:26:12+10:00", - "duration": 47649 - }, - { - "timestamp": "2022-06-03T13:58:52+10:00", - "duration": 443079 - }, - { - "timestamp": "2022-05-15T10:46:58+10:00", - "duration": 31389950 - }, - { - "timestamp": "2022-05-14T15:33:38+10:00", - "duration": 1279604 - }, - { - "timestamp": "2022-05-07T19:39:07+10:00", - "duration": 901817 - }, - { - "timestamp": "2022-04-23T08:26:14+10:00", - "duration": 437693 - }, - { - "timestamp": "2022-04-22T19:14:33+10:00", - "duration": 757615 - }, - { - "timestamp": "2022-04-14T11:54:35+10:00", - "duration": 581358 - }, - { - "timestamp": "2022-04-06T22:26:41+10:00", - "duration": 65188 - }, - { - "timestamp": "2022-04-03T22:12:07+10:00", - "duration": 654161 - }, - { - "timestamp": "2022-04-03T21:57:36+10:00", - "duration": 798912 - }, - { - "timestamp": "2022-04-03T18:51:05+10:00", - "duration": 67764 - }, - { - "timestamp": "2022-04-03T17:22:58+10:00", - "duration": 641782 - }, - { - "timestamp": "2022-04-03T17:21:19+10:00", - "duration": 69942 - }, - { - "timestamp": "2022-04-03T06:34:17+10:00", - "duration": 232350 - }, - { - "timestamp": "2022-04-02T19:05:41+10:00", - "duration": 47104 - }, - { - "timestamp": "2022-04-02T09:35:18+10:00", - "duration": 258895 - }, - { - "timestamp": "2022-04-02T05:21:14+10:00", - "duration": 63814 - }, - { - "timestamp": "2022-04-01T11:59:57+10:00", - "duration": 586849 - }, - { - "timestamp": "2022-04-01T11:50:56+10:00", - "duration": 457199 - }, - { - "timestamp": "2022-04-01T11:48:21+10:00", - "duration": 51065 - }, - { - "timestamp": "2022-04-01T11:47:23+10:00", - "duration": 41783 - }, - { - "timestamp": "2022-04-01T11:01:46+10:00", - "duration": 73278 - }, - { - "timestamp": "2022-03-31T17:12:00+10:00", - "duration": 45838 - }, - { - "timestamp": "2022-03-24T16:28:07+10:00", - "duration": 122233 - }, - { - "timestamp": "2022-03-24T06:15:44+10:00", - "duration": 5932791 - }, - { - "timestamp": "2022-03-23T17:01:37+10:00", - "duration": 210322 - }, - { - "timestamp": "2022-03-23T16:11:27+10:00", - "duration": 2608373 - }, - { - "timestamp": "2022-03-21T21:04:54+10:00", - "duration": 296080 - } + {"timestamp": "2022-07-12T06:56:55+10:00", "duration": 38773}, + {"timestamp": "2022-07-11T20:46:25+10:00", "duration": 66479}, + {"timestamp": "2022-06-29T11:35:43+10:00", "duration": 842030}, + {"timestamp": "2022-06-18T15:28:35+10:00", "duration": 1013486}, + {"timestamp": "2022-06-15T15:43:20+10:00", "duration": 210737}, + {"timestamp": "2022-06-10T08:26:12+10:00", "duration": 47649}, + {"timestamp": "2022-06-03T13:58:52+10:00", "duration": 443079}, + {"timestamp": "2022-05-15T10:46:58+10:00", "duration": 31389950}, + {"timestamp": "2022-05-14T15:33:38+10:00", "duration": 1279604}, + {"timestamp": "2022-05-07T19:39:07+10:00", "duration": 901817}, + {"timestamp": "2022-04-23T08:26:14+10:00", "duration": 437693}, + {"timestamp": "2022-04-22T19:14:33+10:00", "duration": 757615}, + {"timestamp": "2022-04-14T11:54:35+10:00", "duration": 581358}, + {"timestamp": "2022-04-06T22:26:41+10:00", "duration": 65188}, + {"timestamp": "2022-04-03T22:12:07+10:00", "duration": 654161}, + {"timestamp": "2022-04-03T21:57:36+10:00", "duration": 798912}, + {"timestamp": "2022-04-03T18:51:05+10:00", "duration": 67764}, + {"timestamp": "2022-04-03T17:22:58+10:00", "duration": 641782}, + {"timestamp": "2022-04-03T17:21:19+10:00", "duration": 69942}, + {"timestamp": "2022-04-03T06:34:17+10:00", "duration": 232350}, + {"timestamp": "2022-04-02T19:05:41+10:00", "duration": 47104}, + {"timestamp": "2022-04-02T09:35:18+10:00", "duration": 258895}, + {"timestamp": "2022-04-02T05:21:14+10:00", "duration": 63814}, + {"timestamp": "2022-04-01T11:59:57+10:00", "duration": 586849}, + {"timestamp": "2022-04-01T11:50:56+10:00", "duration": 457199}, + {"timestamp": "2022-04-01T11:48:21+10:00", "duration": 51065}, + {"timestamp": "2022-04-01T11:47:23+10:00", "duration": 41783}, + {"timestamp": "2022-04-01T11:01:46+10:00", "duration": 73278}, + {"timestamp": "2022-03-31T17:12:00+10:00", "duration": 45838}, + {"timestamp": "2022-03-24T16:28:07+10:00", "duration": 122233}, + {"timestamp": "2022-03-24T06:15:44+10:00", "duration": 5932791}, + {"timestamp": "2022-03-23T17:01:37+10:00", "duration": 210322}, + {"timestamp": "2022-03-23T16:11:27+10:00", "duration": 2608373}, + {"timestamp": "2022-03-21T21:04:54+10:00", "duration": 296080}, ], "events_count": 0, - "total_events": 0 + "total_events": 0, }, "user_settings": { "storm_mode_enabled": True, "powerwall_onboarding_settings_set": True, "sync_grid_alert_enabled": False, - "breaker_alert_enabled": False + "breaker_alert_enabled": False, }, "default_real_mode": "backup", "operation": "backup", @@ -970,8 +899,8 @@ def command_ok(): "solar_power": 709, "grid_power": 2930, "battery_power": -3310, - "generator_power": 0 + "generator_power": 0, } ], - "battery_count": 1 -} \ No newline at end of file + "battery_count": 1, +} From c67f6de0f5e155f9aed00fdadff2a60f56922efc Mon Sep 17 00:00:00 2001 From: shred86 Date: Wed, 17 Aug 2022 19:10:11 -0700 Subject: [PATCH 12/84] Fix grid_status check --- teslajsonpy/controller.py | 41 ++++++++++++++++++++++++++++----------- 1 file changed, 30 insertions(+), 11 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index e99bed84..f4f888ca 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -1018,7 +1018,10 @@ async def _get_and_process_battery_data( data = None if data and data["response"]: response = data["response"]["power_reading"][0] - # Store the response using energysite_id since that's how it's retrieved + # Add grid_status to the response + # Already in data for non-Powerwall sites + response.update(data["response"]["grid_status"]) + # Use energysite_id since that's how it's retrieved self.__power[energysite_id] = response async with self.__update_lock: @@ -1199,11 +1202,6 @@ def charging_state(self, car_id: Text = None, vin: Text = None) -> Text: return self.get_charging_params(vin=vin).get("charging_state") return None - def get_power_params(self, site_id: Text) -> Dict: - """Return cached copy of power_params for site_id.""" - energysite_id = self._id_to_energysiteid(site_id) - return self.__power[energysite_id] - def get_state_params(self, car_id: Text = None, vin: Text = None) -> Dict: """Return cached copy of state_params for car_id. or all cars. @@ -1639,11 +1637,6 @@ def set_vehicle_id_vin(self, vehicle_id: Text, vin: Text) -> None: self.__vehicle_id_vin_map[vehicle_id] = vin self.__vin_vehicle_id_map[vin] = vehicle_id - def get_power(self, energysite_id: Text, power_type: Text) -> int: - """Return solar power.""" - - return self.__power[energysite_id][power_type] - @property def update_interval(self) -> float: """Return update_interval. @@ -1689,6 +1682,32 @@ def get_update_interval_vin(self, car_id: Text = None, vin: Text = None) -> int: return self._update_interval_vin.get(vin, self.update_interval) + def get_power(self, energysite_id: Text, power_type: Text) -> int: + """Return cached copy of power in Watts for specified power_type.""" + + return self.__power[energysite_id][power_type] + + def get_power_params(self, energysite_id: Text) -> Dict: + """Return cached copy of power_params for energysite_id.""" + energysite_id = self._id_to_energysiteid(energysite_id) + + data = self.__power[energysite_id] + + if data: + # Note: Some systems that pre-date Tesla aquisition of SolarCity + # will have `grid_status: Unknown`, but will have solar power values. + # At the same time, newer systems will report spurious reads of 0 Watts + # and grid status unknown. If solar power is 0 return null. + if ( + "grid_status" in data + and data["grid_status"] == "Unknown" + and data["solar_power"] == 0 + ): + _LOGGER.debug("Possible spurious energy site power read") + return + + return data + def _id_to_vin(self, car_id: Text) -> Optional[Text]: """Return vin for a car_id.""" return self.__id_vin_map.get(str(car_id)) From c11af2ac08100cd18692227e7fdd9597899c2574 Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 18 Aug 2022 12:53:26 -0700 Subject: [PATCH 13/84] Updates for rewrite changes --- teslajsonpy/controller.py | 75 ++++++++++++------------------ teslajsonpy/homeassistant/power.py | 8 ++-- 2 files changed, 35 insertions(+), 48 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index f4f888ca..5b738404 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -383,6 +383,7 @@ def __init__( self.__energysite_name = {} self.__energysite_type = {} self.__power = {} + self.cars = {} self.energysites = {} self.__id_energysiteid_map = {} self.__energysiteid_id_map = {} @@ -394,6 +395,7 @@ async def connect( wake_if_asleep: bool = False, filtered_vins: Optional[List[Text]] = None, mfa_code: Text = "", + skip_add: bool = False, ) -> Dict[Text, Text]: """Connect controller to Tesla. @@ -410,11 +412,11 @@ async def connect( if mfa_code: self.__connection.mfa_code = mfa_code - cars = await self.get_vehicles() + self.cars = await self.get_vehicles() self._last_attempted_update_time = round(time.time()) self.__update_lock = asyncio.Lock() - for car in cars: + for car in self.cars: vin = car["vin"] if filtered_vins and vin not in filtered_vins: _LOGGER.debug("Skipping car with VIN: %s", vin) @@ -438,15 +440,36 @@ async def connect( self.__config[vin] = {} self.__driving[vin] = {} self.__gui[vin] = {} - - self._add_car_components(car) + # This is temporary to provide backwards compatability with + # previous version of Home Assistant Tesla Custom Integration + if not skip_add: + self._add_car_components(car) self.energysites = await self.get_energysites() for energysite in self.energysites: energysite_id = energysite["energy_site_id"] + # Get site_config data for site name and update energysite dict + site_config = await self.get_site_config(energysite_id) + energysite.update(site_config) + # Set initial values to setup GridPowerSensor & LoadPowerSensor + # Actual values update immediately after setup when refresh is called + energysite["grid_power"] = 0 + energysite["load_power"] = 0 + + self.__id_energysiteid_map[energysite["id"]] = energysite_id + self.__energysiteid_id_map[energysite_id] = energysite["id"] + self.__energysite_name[energysite_id] = energysite.get( + "site_name", TESLA_DEFAULT_ENERGY_SITE_NAME + ) + self.__energysite_type[energysite_id] = energysite["solar_type"] + self.__power[energysite_id] = {"solar_power": energysite["solar_power"]} + self.__lock[energysite_id] = asyncio.Lock() - self._add_energysite_components(energysite) + # This is temporary to provide backwards compatability with + # previous version of Home Assistant Tesla Custom Integration + if not skip_add: + self._add_energysite_components(energysite) if not test_login: try: @@ -537,41 +560,13 @@ async def get_vehicles(self): @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) async def get_energysites(self): - """Get energy sites json from TeslaAPI and filter to solar or battery.""" - energysites = [ + """Get energy sites json from TeslaAPI and filter to solar.""" + return [ p for p in (await self.api("PRODUCT_LIST"))["response"] - if p.get("resource_type") == TESLA_RESOURCE_TYPE_SOLAR - or p.get("resource_type") == TESLA_RESOURCE_TYPE_BATTERY + if p.get("resource_type") == "solar" ] - for energysite in energysites: - energysite_id = energysite["energy_site_id"] - # Set initial values to initialize power sensors - # Actual values update immediately after setup when refresh is called - energysite["solar_power"] = 0 - energysite["load_power"] = 0 - energysite["grid_power"] = 0 - energysite["battery_power"] = 0 - - if energysite["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: - # Non-powerwall sites do not include "site_name" in "PRODUCT_LIST" endpoint - # Get "site_config" data for "site_name" and update energysite dict - site_config = await self.get_site_config(energysite_id) - energysite.update(site_config) - - self.__id_energysiteid_map[energysite["id"]] = energysite_id - self.__energysiteid_id_map[energysite_id] = energysite["id"] - self.__energysite_name[energysite_id] = energysite.get( - "site_name", TESLA_DEFAULT_ENERGY_SITE_NAME - ) - # Sites with Powerwall only contain "solar_type" in "components" - self.__energysite_type[energysite_id] = energysite["components"][ - "solar_type" - ] - - return energysites - @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) async def get_site_config(self, energysite_id): """Get site config json from TeslaAPI.""" @@ -1674,7 +1669,6 @@ def set_update_interval_vin( def get_update_interval_vin(self, car_id: Text = None, vin: Text = None) -> int: """Get update interval for specific vin or default if no vin specific.""" - if car_id and not vin: vin = self._id_to_vin(car_id) if vin is None or vin == "": @@ -1682,15 +1676,8 @@ def get_update_interval_vin(self, car_id: Text = None, vin: Text = None) -> int: return self._update_interval_vin.get(vin, self.update_interval) - def get_power(self, energysite_id: Text, power_type: Text) -> int: - """Return cached copy of power in Watts for specified power_type.""" - - return self.__power[energysite_id][power_type] - def get_power_params(self, energysite_id: Text) -> Dict: """Return cached copy of power_params for energysite_id.""" - energysite_id = self._id_to_energysiteid(energysite_id) - data = self.__power[energysite_id] if data: diff --git a/teslajsonpy/homeassistant/power.py b/teslajsonpy/homeassistant/power.py index 1818df95..9e41efd9 100644 --- a/teslajsonpy/homeassistant/power.py +++ b/teslajsonpy/homeassistant/power.py @@ -168,7 +168,7 @@ def refresh(self) -> None: This assumes the controller has already been updated """ super().refresh() - data = self._controller.get_power_params(self._id) + data = self._controller.get_power_params(self.energy_site_id) if data: # Note: Some systems that pre-date Tesla aquisition of SolarCity will have `grid_status: Unknown`, @@ -217,7 +217,7 @@ def refresh(self) -> None: This assumes the controller has already been updated """ super().refresh() - data = self._controller.get_power_params(self._id) + data = self._controller.get_power_params(self.energy_site_id) if data: self.__load_power = data["load_power"] @@ -251,7 +251,7 @@ def refresh(self) -> None: This assumes the controller has already been updated """ super().refresh() - data = self._controller.get_power_params(self._id) + data = self._controller.get_power_params(self.energy_site_id) if data: self.__grid_power = data["grid_power"] @@ -285,7 +285,7 @@ def refresh(self) -> None: This assumes the controller has already been updated """ super().refresh() - data = self._controller.get_power_params(self._id) + data = self._controller.get_power_params(self.energy_site_id) if data: self.__battery_power = data["battery_power"] From dc9692436160eafbc68a6b8be382c0f458c42341 Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 18 Aug 2022 13:40:12 -0700 Subject: [PATCH 14/84] Update test and add battery site code --- teslajsonpy/controller.py | 21 ++++++++++++++------- tests/tesla_mock.py | 4 ++-- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 5b738404..6be9275b 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -449,21 +449,28 @@ async def connect( for energysite in self.energysites: energysite_id = energysite["energy_site_id"] - # Get site_config data for site name and update energysite dict - site_config = await self.get_site_config(energysite_id) - energysite.update(site_config) - # Set initial values to setup GridPowerSensor & LoadPowerSensor + # Set initial values to initialize power sensors # Actual values update immediately after setup when refresh is called - energysite["grid_power"] = 0 + energysite["solar_power"] = 0 energysite["load_power"] = 0 + energysite["grid_power"] = 0 + energysite["battery_power"] = 0 + + if energysite["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: + # Non-powerwall sites do not include "site_name" in "PRODUCT_LIST" endpoint + # Get "site_config" data for "site_name" and update energysite dict + site_config = await self.get_site_config(energysite_id) + energysite.update(site_config) self.__id_energysiteid_map[energysite["id"]] = energysite_id self.__energysiteid_id_map[energysite_id] = energysite["id"] self.__energysite_name[energysite_id] = energysite.get( "site_name", TESLA_DEFAULT_ENERGY_SITE_NAME ) - self.__energysite_type[energysite_id] = energysite["solar_type"] - self.__power[energysite_id] = {"solar_power": energysite["solar_power"]} + # Sites with Powerwall only contain "solar_type" in "components" + self.__energysite_type[energysite_id] = energysite["components"][ + "solar_type" + ] self.__lock[energysite_id] = asyncio.Lock() # This is temporary to provide backwards compatability with diff --git a/tests/tesla_mock.py b/tests/tesla_mock.py index 13a721be..c266490c 100644 --- a/tests/tesla_mock.py +++ b/tests/tesla_mock.py @@ -51,13 +51,13 @@ def __init__(self, monkeypatch) -> None: Controller, "get_power_params", self.mock_get_power_params ) self._vehicle_product_list = copy.deepcopy(VEHICLE_PRODUCT_LIST) - self._site_config = copy.deepcopy(SITE_CONFIG) self._drive_state = copy.deepcopy(DRIVE_STATE) self._climate_state = copy.deepcopy(CLIMATE_STATE) self._charge_state = copy.deepcopy(CHARGE_STATE) self._gui_settings = copy.deepcopy(GUI_SETTINGS) self._vehicle_state = copy.deepcopy(VEHICLE_STATE) self._vehicle_config = copy.deepcopy(VEHICLE_CONFIG) + self._product_list = copy.deepcopy(ENERGYSITE_PRODUCT_LIST) self._solar_combined_data = copy.deepcopy(SOLAR_COMBINED_DATA) self._solar_combined_data_no_name = copy.deepcopy(SOLAR_COMBINED_DATA_NO_NAME) self._battery_combined_data = copy.deepcopy(BATTERY_COMBINED_DATA) @@ -196,7 +196,7 @@ async def controller_get_vehicles(self): async def controller_get_energysites(self): """Monkeypatch for controller.get_energysites().""" - return self._solar_combined_data + return self._product_list async def controller_get_site_config(self): """Monkeypatch for controller.get_site_config().""" From 53070ff60c51de991e6c0f03cc7ad85d83099711 Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 18 Aug 2022 17:31:48 -0700 Subject: [PATCH 15/84] Add check to only run if energysites exist --- teslajsonpy/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 6be9275b..5b0bf8db 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -1098,7 +1098,7 @@ async def _get_and_process_battery_data( cur_time - self.get_last_park_time(vin=vin), cur_time - self.get_last_wake_up_time(vin=vin), ) - if not car_id: + if self.energysites and not car_id: # do not update energy sites if car_id was a parameter. for energysite in self.energysites: energysite_id = energysite["energy_site_id"] From 4abf66a8e56a01368de0b2bdf717a25c891c6238 Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 18 Aug 2022 18:29:57 -0700 Subject: [PATCH 16/84] Add support for battery information --- teslajsonpy/controller.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 5b0bf8db..de0e2377 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -1026,6 +1026,24 @@ async def _get_and_process_battery_data( # Use energysite_id since that's how it's retrieved self.__power[energysite_id] = response + async def _get_and_process_battery_summary( + energysite_id: Text, battery_id: Text + ) -> None: + # Battery stats are 0 in BATTERY_DATA + # Must get from BATTERY_SUMMARY + async with self.__lock[battery_id]: + _LOGGER.debug("Updating energysite battery summary %s", battery_id) + try: + data = await self.api( + "BATTERY_SUMMARY", + path_vars={"battery_id": battery_id}, + wake_if_asleep=wake_if_asleep, + ) + except TeslaException: + data = None + if data and data["response"]: + self.__power[energysite_id] = data["response"] + async with self.__update_lock: cur_time = round(time.time()) # Update the online cars using get_vehicles() @@ -1109,6 +1127,9 @@ async def _get_and_process_battery_data( tasks.append( _get_and_process_battery_data(energysite_id, battery_id) ) + tasks.append( + _get_and_process_battery_summary(energysite_id, battery_id) + ) return any(await asyncio.gather(*tasks)) From 015a5d62f52921c31d191369b7b64fccb8da5c06 Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 18 Aug 2022 19:13:48 -0700 Subject: [PATCH 17/84] Temporary remove check causing issues --- teslajsonpy/controller.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index de0e2377..24eb92eb 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -1708,18 +1708,18 @@ def get_power_params(self, energysite_id: Text) -> Dict: """Return cached copy of power_params for energysite_id.""" data = self.__power[energysite_id] - if data: - # Note: Some systems that pre-date Tesla aquisition of SolarCity - # will have `grid_status: Unknown`, but will have solar power values. - # At the same time, newer systems will report spurious reads of 0 Watts - # and grid status unknown. If solar power is 0 return null. - if ( - "grid_status" in data - and data["grid_status"] == "Unknown" - and data["solar_power"] == 0 - ): - _LOGGER.debug("Possible spurious energy site power read") - return + # if data: + # # Note: Some systems that pre-date Tesla aquisition of SolarCity + # # will have `grid_status: Unknown`, but will have solar power values. + # # At the same time, newer systems will report spurious reads of 0 Watts + # # and grid status unknown. If solar power is 0 return null. + # if ( + # "grid_status" in data + # and data["grid_status"] == "Unknown" + # and data["solar_power"] == 0 + # ): + # _LOGGER.debug("Possible spurious energy site power read") + # return return data From 024c2929f033484f58e0bbe66bdd9851aed9e106 Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 18 Aug 2022 20:11:30 -0700 Subject: [PATCH 18/84] Fix self.__power being set to None --- teslajsonpy/controller.py | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 24eb92eb..d729cff3 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -1001,9 +1001,23 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: ) except TeslaException: data = None - if data and data["response"]: - response = data["response"] - self.__power[energysite_id] = response + + response = data["response"] + # Note: Some systems that pre-date Tesla aquisition of SolarCity + # and systems with a Tesla inverter (non-Powerwall) will have + # `grid_status: Unknown`, but will have solar power values. + # At the same time, newer systems maye report spurious reads of 0 Watts + # and grid status unknown. In this case, remove values but update + # self.__power with remaining data (grid and load power). + if ( + response["grid_status"] == "Unknown" + and response["solar_power"] == 0 + ): + _LOGGER.debug("Possible spurious energy site power read") + del response["grid_status"] + del response["solar_power"] + + self.__power[energysite_id].update(response) async def _get_and_process_battery_data( energysite_id: Text, battery_id: Text @@ -1706,22 +1720,7 @@ def get_update_interval_vin(self, car_id: Text = None, vin: Text = None) -> int: def get_power_params(self, energysite_id: Text) -> Dict: """Return cached copy of power_params for energysite_id.""" - data = self.__power[energysite_id] - - # if data: - # # Note: Some systems that pre-date Tesla aquisition of SolarCity - # # will have `grid_status: Unknown`, but will have solar power values. - # # At the same time, newer systems will report spurious reads of 0 Watts - # # and grid status unknown. If solar power is 0 return null. - # if ( - # "grid_status" in data - # and data["grid_status"] == "Unknown" - # and data["solar_power"] == 0 - # ): - # _LOGGER.debug("Possible spurious energy site power read") - # return - - return data + return self.__power[energysite_id] def _id_to_vin(self, car_id: Text) -> Optional[Text]: """Return vin for a car_id.""" From 1acc65ce728a1d28e8a1ac4e2f8dcae503b85965 Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 18 Aug 2022 20:49:28 -0700 Subject: [PATCH 19/84] Fix key error for specific case --- teslajsonpy/controller.py | 14 ++++++++------ teslajsonpy/homeassistant/power.py | 28 ++++++++++++---------------- 2 files changed, 20 insertions(+), 22 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index d729cff3..4d446225 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -449,12 +449,6 @@ async def connect( for energysite in self.energysites: energysite_id = energysite["energy_site_id"] - # Set initial values to initialize power sensors - # Actual values update immediately after setup when refresh is called - energysite["solar_power"] = 0 - energysite["load_power"] = 0 - energysite["grid_power"] = 0 - energysite["battery_power"] = 0 if energysite["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: # Non-powerwall sites do not include "site_name" in "PRODUCT_LIST" endpoint @@ -471,6 +465,14 @@ async def connect( self.__energysite_type[energysite_id] = energysite["components"][ "solar_type" ] + # Add energysite_id key to prevent a key error with get_power_params() + # and `_get_and_process_site_data` + self.__power[energysite_id] = { + "solar_power": 0, + "load_power": 0, + "grid_power": 0, + "battery_power": 0, + } self.__lock[energysite_id] = asyncio.Lock() # This is temporary to provide backwards compatability with diff --git a/teslajsonpy/homeassistant/power.py b/teslajsonpy/homeassistant/power.py index 9e41efd9..9c3a5fa3 100644 --- a/teslajsonpy/homeassistant/power.py +++ b/teslajsonpy/homeassistant/power.py @@ -83,6 +83,11 @@ def refresh(self) -> None: """ return + @property + def power_data(self): + """Return the coordinator controller power data.""" + return self.controller.get_power_params(self._energy_site_id) + class PowerSensor(EnergySiteDevice): """Home-assistant class of power sensors for Tesla Energy Sites (Solar Panels). @@ -139,7 +144,7 @@ def __init__(self, data, controller): """Initialize the solar panel sensor.""" super().__init__(data, controller) self._solar_type: Text = data["components"]["solar_type"] - self.__solar_power: float = data["solar_power"] + self.__solar_power: float = self.power_data["solar_power"] self.__generating_status: bool = None self.type = "solar panel" self.name = self._name() @@ -198,7 +203,7 @@ class LoadPowerSensor(PowerSensor): def __init__(self, data, controller): """Initialize the load power sensor.""" super().__init__(data, controller) - self.__load_power: float = data["load_power"] + self.__load_power: float = self.power_data["load_power"] self.type = "load power" self.name = self._name() self.uniq_name = self._uniq_name() @@ -217,10 +222,7 @@ def refresh(self) -> None: This assumes the controller has already been updated """ super().refresh() - data = self._controller.get_power_params(self.energy_site_id) - - if data: - self.__load_power = data["load_power"] + self.__load_power = self.power_data["load_power"] class GridPowerSensor(PowerSensor): @@ -232,7 +234,7 @@ class GridPowerSensor(PowerSensor): def __init__(self, data, controller): """Initialize the grid power sensor.""" super().__init__(data, controller) - self.__grid_power: float = data["grid_power"] + self.__grid_power: float = self.power_data["grid_power"] self.type = "grid power" self.name = self._name() self.uniq_name = self._uniq_name() @@ -251,10 +253,7 @@ def refresh(self) -> None: This assumes the controller has already been updated """ super().refresh() - data = self._controller.get_power_params(self.energy_site_id) - - if data: - self.__grid_power = data["grid_power"] + self.__grid_power = self.power_data["grid_power"] class BatteryPowerSensor(PowerSensor): @@ -266,7 +265,7 @@ class BatteryPowerSensor(PowerSensor): def __init__(self, data, controller): """Initialize the battery power sensor.""" super().__init__(data, controller) - self.__battery_power: float = data["battery_power"] + self.__battery_power: float = self.power_data["battery_power"] self.type = "battery power" self.name = self._name() self.uniq_name = self._uniq_name() @@ -285,7 +284,4 @@ def refresh(self) -> None: This assumes the controller has already been updated """ super().refresh() - data = self._controller.get_power_params(self.energy_site_id) - - if data: - self.__battery_power = data["battery_power"] + self.__battery_power = self.power_data["battery_power"] From 7f5476004b003d7463248e68ac1742acd51a8463 Mon Sep 17 00:00:00 2001 From: shred86 Date: Fri, 19 Aug 2022 09:01:02 -0700 Subject: [PATCH 20/84] Add accidently deleted check --- teslajsonpy/controller.py | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 4d446225..7fb293f3 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -1003,23 +1003,23 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: ) except TeslaException: data = None + if data and data["response"]: + response = data["response"] + # Note: Some systems that pre-date Tesla aquisition of SolarCity + # and systems with a Tesla inverter (non-Powerwall) will have + # `grid_status: Unknown`, but will have solar power values. + # At the same time, newer systems maye report spurious reads of 0 Watts + # and grid status unknown. In this case, remove values but update + # self.__power with remaining data (grid and load power). + if ( + response["grid_status"] == "Unknown" + and response["solar_power"] == 0 + ): + _LOGGER.debug("Possible spurious energy site power read") + del response["grid_status"] + del response["solar_power"] - response = data["response"] - # Note: Some systems that pre-date Tesla aquisition of SolarCity - # and systems with a Tesla inverter (non-Powerwall) will have - # `grid_status: Unknown`, but will have solar power values. - # At the same time, newer systems maye report spurious reads of 0 Watts - # and grid status unknown. In this case, remove values but update - # self.__power with remaining data (grid and load power). - if ( - response["grid_status"] == "Unknown" - and response["solar_power"] == 0 - ): - _LOGGER.debug("Possible spurious energy site power read") - del response["grid_status"] - del response["solar_power"] - - self.__power[energysite_id].update(response) + self.__power[energysite_id].update(response) async def _get_and_process_battery_data( energysite_id: Text, battery_id: Text From 38e8b8337d4fd5228e9055f310a24dc4190866c5 Mon Sep 17 00:00:00 2001 From: shred86 Date: Fri, 19 Aug 2022 17:59:57 -0700 Subject: [PATCH 21/84] Add energysite objects --- teslajsonpy/const.py | 12 +- teslajsonpy/controller.py | 103 ++++++++------- teslajsonpy/energy.py | 123 ++++++++++++++++++ teslajsonpy/homeassistant/power.py | 18 +-- .../homeassistant/test_power_sensor.py | 26 ++-- 5 files changed, 206 insertions(+), 76 deletions(-) create mode 100644 teslajsonpy/energy.py diff --git a/teslajsonpy/const.py b/teslajsonpy/const.py index c29fe018..3d79f7e6 100644 --- a/teslajsonpy/const.py +++ b/teslajsonpy/const.py @@ -17,8 +17,10 @@ WS_URL = "wss://streaming.vn.teslamotors.com/streaming" TESLA_PRODUCT_TYPE_VEHICLES = "vehicles" -TESLA_PRODUCT_TYPE_ENERGY_SITES = "energy_sites" -TESLA_PRODUCT_TYPE_POWERWALLS = "powerwalls" -TESLA_DEFAULT_ENERGY_SITE_NAME = "My Home" -TESLA_RESOURCE_TYPE_SOLAR = "solar" -TESLA_RESOURCE_TYPE_BATTERY = "battery" + +PRODUCT_TYPE_ENERGY_SITES = "energy_sites" +PRODUCT_TYPE_POWERWALLS = "powerwalls" +DEFAULT_ENERGY_SITE_NAME = "My Home" +RESOURCE_TYPE = "resource_type" +RESOURCE_TYPE_SOLAR = "solar" +RESOURCE_TYPE_BATTERY = "battery" diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 7fb293f3..52ff5d68 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -28,11 +28,11 @@ ONLINE_INTERVAL, UPDATE_INTERVAL, SLEEP_INTERVAL, - TESLA_PRODUCT_TYPE_ENERGY_SITES, + PRODUCT_TYPE_ENERGY_SITES, TESLA_PRODUCT_TYPE_VEHICLES, - TESLA_DEFAULT_ENERGY_SITE_NAME, - TESLA_RESOURCE_TYPE_SOLAR, - TESLA_RESOURCE_TYPE_BATTERY, + RESOURCE_TYPE, + RESOURCE_TYPE_SOLAR, + RESOURCE_TYPE_BATTERY, ) from teslajsonpy.exceptions import should_giveup, RetryLimitError, TeslaException from teslajsonpy.homeassistant.battery_sensor import Battery, Range @@ -75,6 +75,8 @@ VehicleStateDataSensor, ) +from teslajsonpy.energy import SolarSite, PowerwallSite, SolarPowerwallSite + _LOGGER = logging.getLogger(__name__) @@ -177,9 +179,7 @@ def valid_result(result): else: car_id = args[0] if not kwargs.get("vehicle_id") else kwargs.get("vehicle_id") is_wake_command = len(args) >= 2 and args[1].lower() == "wake_up" - is_energysite_command = ( - kwargs.get("product_type") == TESLA_PRODUCT_TYPE_ENERGY_SITES - ) + is_energysite_command = kwargs.get("product_type") == PRODUCT_TYPE_ENERGY_SITES result = None if ( instance._id_to_vin(car_id) is None @@ -380,14 +380,11 @@ def __init__( self.__update_state = {} self.enable_websocket = enable_websocket self.polling_policy = polling_policy - self.__energysite_name = {} - self.__energysite_type = {} - self.__power = {} self.cars = {} - self.energysites = {} - self.__id_energysiteid_map = {} - self.__energysiteid_id_map = {} self.endpoints = {} + self.energysites = {} + self.__energysites = {} + self.__power_data = {} async def connect( self, @@ -445,29 +442,17 @@ async def connect( if not skip_add: self._add_car_components(car) - self.energysites = await self.get_energysites() + self.__energysites = await self.get_energysites() - for energysite in self.energysites: + for energysite in self.__energysites: energysite_id = energysite["energy_site_id"] - if energysite["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: - # Non-powerwall sites do not include "site_name" in "PRODUCT_LIST" endpoint - # Get "site_config" data for "site_name" and update energysite dict + if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: + # Non-powerwall sites "site_name" in "SITE_DATA" endpoint site_config = await self.get_site_config(energysite_id) energysite.update(site_config) - self.__id_energysiteid_map[energysite["id"]] = energysite_id - self.__energysiteid_id_map[energysite_id] = energysite["id"] - self.__energysite_name[energysite_id] = energysite.get( - "site_name", TESLA_DEFAULT_ENERGY_SITE_NAME - ) - # Sites with Powerwall only contain "solar_type" in "components" - self.__energysite_type[energysite_id] = energysite["components"][ - "solar_type" - ] - # Add energysite_id key to prevent a key error with get_power_params() - # and `_get_and_process_site_data` - self.__power[energysite_id] = { + self.__power_data[energysite_id] = { "solar_power": 0, "load_power": 0, "grid_power": 0, @@ -480,6 +465,8 @@ async def connect( if not skip_add: self._add_energysite_components(energysite) + self._generate_energysite_objects() + if not test_login: try: await self.update(wake_if_asleep=wake_if_asleep) @@ -573,7 +560,7 @@ async def get_energysites(self): return [ p for p in (await self.api("PRODUCT_LIST"))["response"] - if p.get("resource_type") == "solar" + if p.get(RESOURCE_TYPE) == RESOURCE_TYPE_SOLAR ] @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) @@ -743,6 +730,32 @@ async def command( product_type=product_type, ) + def _generate_energysite_objects(self) -> None: + """Generate energy site objects.""" + for energysite in self.__energysites: + energysite_id = energysite["energy_site_id"] + # Solar only systems (no Powerwalls) are listed as "solar" + if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: + self.energysites[energysite_id] = SolarSite( + energysite, self.__power_data[energysite_id] + ) + # Solar with Powerwall are listed as "battery" + if ( + energysite[RESOURCE_TYPE] == RESOURCE_TYPE_BATTERY + and energysite["components"]["solar"] + ): + self.energysites[energysite_id] = SolarPowerwallSite( + energysite, self.__power_data[energysite_id] + ) + # Assumed Powerwall only (no solar) is listed as "battery" + if ( + energysite[RESOURCE_TYPE] == RESOURCE_TYPE_BATTERY + and not energysite["components"]["solar"] + ): + self.energysites[energysite_id] = PowerwallSite( + energysite, self.__power_data[energysite_id] + ) + def get_homeassistant_components(self): """Return list of Tesla components for Home Assistant setup. @@ -754,7 +767,7 @@ def _add_energysite_components(self, energysite): self.__components.append(SolarPowerSensor(energysite, self)) self.__components.append(LoadPowerSensor(energysite, self)) self.__components.append(GridPowerSensor(energysite, self)) - if energysite["resource_type"] == TESLA_RESOURCE_TYPE_BATTERY: + if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_BATTERY: self.__components.append(BatteryPowerSensor(energysite, self)) def _add_car_components(self, car): @@ -1010,7 +1023,7 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: # `grid_status: Unknown`, but will have solar power values. # At the same time, newer systems maye report spurious reads of 0 Watts # and grid status unknown. In this case, remove values but update - # self.__power with remaining data (grid and load power). + # self.__power_data with remaining data (grid and load power). if ( response["grid_status"] == "Unknown" and response["solar_power"] == 0 @@ -1019,7 +1032,7 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: del response["grid_status"] del response["solar_power"] - self.__power[energysite_id].update(response) + self.__power_data[energysite_id].update(response) async def _get_and_process_battery_data( energysite_id: Text, battery_id: Text @@ -1040,7 +1053,7 @@ async def _get_and_process_battery_data( # Already in data for non-Powerwall sites response.update(data["response"]["grid_status"]) # Use energysite_id since that's how it's retrieved - self.__power[energysite_id] = response + self.__power_data[energysite_id] = response async def _get_and_process_battery_summary( energysite_id: Text, battery_id: Text @@ -1058,7 +1071,7 @@ async def _get_and_process_battery_summary( except TeslaException: data = None if data and data["response"]: - self.__power[energysite_id] = data["response"] + self.__power_data[energysite_id] = data["response"] async with self.__update_lock: cur_time = round(time.time()) @@ -1132,14 +1145,14 @@ async def _get_and_process_battery_summary( cur_time - self.get_last_park_time(vin=vin), cur_time - self.get_last_wake_up_time(vin=vin), ) - if self.energysites and not car_id: + if self.__energysites and not car_id: # do not update energy sites if car_id was a parameter. - for energysite in self.energysites: - energysite_id = energysite["energy_site_id"] - if energysite["resource_type"] == TESLA_RESOURCE_TYPE_SOLAR: + for energysite in self.energysites.values(): + energysite_id = energysite.energysite_id + if energysite.resource_type == RESOURCE_TYPE_SOLAR: tasks.append(_get_and_process_site_data(energysite_id)) - if energysite["resource_type"] == TESLA_RESOURCE_TYPE_BATTERY: - battery_id = energysite["id"] + if energysite.resource_type == RESOURCE_TYPE_BATTERY: + battery_id = energysite.id tasks.append( _get_and_process_battery_data(energysite_id, battery_id) ) @@ -1722,7 +1735,7 @@ def get_update_interval_vin(self, car_id: Text = None, vin: Text = None) -> int: def get_power_params(self, energysite_id: Text) -> Dict: """Return cached copy of power_params for energysite_id.""" - return self.__power[energysite_id] + return self.__power_data[energysite_id] def _id_to_vin(self, car_id: Text) -> Optional[Text]: """Return vin for a car_id.""" @@ -1744,10 +1757,6 @@ def vin_to_vehicle_id(self, vin: Text) -> Optional[Text]: """Return vehicle_id for a vin.""" return self.__vin_vehicle_id_map.get(vin) - def _id_to_energysiteid(self, site_id: Text) -> Optional[Text]: - """Return energysiteid for a site_id.""" - return self.__id_energysiteid_map.get(site_id) - def _update_id(self, car_id: Text) -> Optional[Text]: """Update the car_id for a vin.""" new_car_id = self.__vin_id_map.get(self._id_to_vin(car_id)) diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py new file mode 100644 index 00000000..3190f069 --- /dev/null +++ b/teslajsonpy/energy.py @@ -0,0 +1,123 @@ +"""Tesla Energy energy site.""" + +from teslajsonpy.const import ( + RESOURCE_TYPE, + DEFAULT_ENERGY_SITE_NAME, +) + + +class EnergySite: + """Base class to represents a Tesla Energy site.""" + + def __init__(self, energysite, power_data) -> None: + """Initialize energy site.""" + self._energy_site = energysite + self._power_data = power_data + + @property + def energysite_id(self) -> int: + """Return energy site id (aka site_id).""" + return self._energy_site["energy_site_id"] + + @property + def has_load_meter(self) -> int: + """Return True if energy site has a load meter.""" + return self._energy_site["components"]["load_meter"] + + @property + def id(self) -> int: + """Return id (aka battery_id).""" + return self._energy_site["id"] + + @property + def resource_type(self) -> int: + """Return energy site type.""" + return self._energy_site[RESOURCE_TYPE] + + @property + def site_name(self) -> int: + """Return energy site name.""" + # "site_name" not a valid key if name never set in Tesla app + return self._energy_site.get("site_name", DEFAULT_ENERGY_SITE_NAME) + + +class SolarSite(EnergySite): + """Represents a Tesla Energy Solar site. + + This class shouldn't be instantiated directly; it will be instantiated + by :meth:`teslajsonpy.controller.generate_energysite_objects`. + """ + + def __init__(self, energysite, power_data) -> None: + super().__init__(energysite, power_data) + + @property + def grid_power(self) -> int: + """Return grid power in Watts.""" + # Add check to see if site has power metering? + return self._power_data["grid_power"] + + @property + def load_power(self) -> int: + """Return load power in Watts.""" + # Add check to see if site has power metering? + return self._power_data["load_power"] + + @property + def solar_power(self) -> int: + """Return solar power in Watts.""" + return self._power_data["solar_power"] + + @property + def solar_type(self) -> int: + """Return type of solar (e.g. pv_panels or roof).""" + return self._energy_site["components"]["solar_type"] + + +class PowerwallSite(EnergySite): + """Represents a Tesla Energy Powerwall site. + + This class shouldn't be instantiated directly; it will be instantiated + by :meth:`teslajsonpy.controller.generate_energy_site_objects`. + """ + + def __init__(self, energysite, power_data) -> None: + super().__init__(energysite, power_data) + + @property + def battery_percent(self) -> int: + """Return battery charge level percentage.""" + # Add check to see if site has power metering? + return self._power_data["battery_percentage"] + + @property + def battery_power(self) -> int: + """Return battery power in Watts.""" + return self._power_data["battery_power"] + + @property + def grid_power(self) -> int: + # Grid and load power are the same in SolarSite because of how we store + # the data. It comes from two different endpoints but we stored in self.__power_data + return self._power_data["grid_power"] + + @property + def load_power(self) -> int: + """Return load power in Watts.""" + return self._power_data["load_power"] + + def set_operation_mode() -> None: + """Set operation mode of Powerwall.""" + # Implement POST request to set Powerwall operation mode + return + + +class SolarPowerwallSite(PowerwallSite, SolarSite): + """Represents a Tesla Energy Solar site with Powerwall(s). + + This class shouldn't be instantiated directly; it will be instantiated + by :meth:`teslajsonpy.controller.generate_energy_site_objects`. + """ + + def __init__(self, energysite, power_data) -> None: + super().__init__(energysite, power_data) diff --git a/teslajsonpy/homeassistant/power.py b/teslajsonpy/homeassistant/power.py index 9c3a5fa3..13038be4 100644 --- a/teslajsonpy/homeassistant/power.py +++ b/teslajsonpy/homeassistant/power.py @@ -7,7 +7,7 @@ import logging from typing import Dict, Text -from teslajsonpy.const import TESLA_DEFAULT_ENERGY_SITE_NAME +from teslajsonpy.const import DEFAULT_ENERGY_SITE_NAME _LOGGER = logging.getLogger(__name__) @@ -35,8 +35,8 @@ def __init__(self, data, controller): """ self._id: int = data["id"] - self._energy_site_id: int = data["energy_site_id"] - self._site_name: Text = data.get("site_name", TESLA_DEFAULT_ENERGY_SITE_NAME) + self._energysite_id: int = data["energy_site_id"] + self._site_name: Text = data.get("site_name", DEFAULT_ENERGY_SITE_NAME) self._controller = controller self.should_poll: bool = True self.type: Text = "device" @@ -47,16 +47,16 @@ def _name(self) -> Text: return f"{self._site_name} {self.type}" def _uniq_name(self) -> Text: - return f"{self._energy_site_id} {self.type}" + return f"{self._energysite_id} {self.type}" def id(self) -> int: # pylint: disable=invalid-name """Return the id.""" return self._id - def energy_site_id(self) -> int: - """Return the energy_site_id.""" - return self._energy_site_id + def energysite_id(self) -> int: + """Return the energysite_id.""" + return self._energysite_id def site_name(self) -> Text: """Return the site name.""" @@ -86,7 +86,7 @@ def refresh(self) -> None: @property def power_data(self): """Return the coordinator controller power data.""" - return self.controller.get_power_params(self._energy_site_id) + return self._controller.get_power_params(self._energysite_id) class PowerSensor(EnergySiteDevice): @@ -173,7 +173,7 @@ def refresh(self) -> None: This assumes the controller has already been updated """ super().refresh() - data = self._controller.get_power_params(self.energy_site_id) + data = self._controller.get_power_params(self.energysite_id) if data: # Note: Some systems that pre-date Tesla aquisition of SolarCity will have `grid_status: Unknown`, diff --git a/tests/unit_tests/homeassistant/test_power_sensor.py b/tests/unit_tests/homeassistant/test_power_sensor.py index 9cb39114..d7440ca9 100644 --- a/tests/unit_tests/homeassistant/test_power_sensor.py +++ b/tests/unit_tests/homeassistant/test_power_sensor.py @@ -20,9 +20,12 @@ async def test_energysite_setup(monkeypatch): _controller = Controller(None) await _controller.connect() + solar_site = _controller.energysites[12345] + powerwall_site = _controller.energysites[67890] + assert _controller.energysites is not None - assert _controller.energysites[0]["energy_site_id"] == 12345 - assert _controller.energysites[1]["energy_site_id"] == 67890 + assert solar_site.resource_type == "solar" + assert powerwall_site.resource_type == "battery" @pytest.mark.asyncio @@ -35,15 +38,13 @@ async def test_solar_power_sensor(monkeypatch): _sensor = SolarPowerSensor(_data, _controller) assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" - assert _sensor.uniq_name == f"{_sensor._energy_site_id} {_sensor.type}" - assert _sensor.get_power() == 0 + assert _sensor.get_power() == 7720 # Test a battery site (Powerwall) _data = _mock.data_request_battery_combined_data() _sensor = SolarPowerSensor(_data, _controller) assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" - assert _sensor.uniq_name == f"{_sensor._energy_site_id} {_sensor.type}" - assert _sensor.get_power() == 0 + assert _sensor.get_power() == 7720 @pytest.mark.asyncio @@ -56,15 +57,13 @@ async def test_load_power_sensor(monkeypatch): _sensor = LoadPowerSensor(_data, _controller) assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" - assert _sensor.uniq_name == f"{_sensor._energy_site_id} {_sensor.type}" - assert _sensor.get_power() == 0 + assert _sensor.get_power() == 4517.14990234375 # Test a battery site (Powerwall) _data = _mock.data_request_battery_combined_data() _sensor = LoadPowerSensor(_data, _controller) assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" - assert _sensor.uniq_name == f"{_sensor._energy_site_id} {_sensor.type}" - assert _sensor.get_power() == 0 + assert _sensor.get_power() == 4517.14990234375 @pytest.mark.asyncio @@ -77,15 +76,13 @@ async def test_grid_power_sensor(monkeypatch): _sensor = GridPowerSensor(_data, _controller) assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" - assert _sensor.uniq_name == f"{_sensor._energy_site_id} {_sensor.type}" - assert _sensor.get_power() == 0 + assert _sensor.get_power() == -3202.85009765625 # Test a battery site (Powerwall) _data = _mock.data_request_battery_combined_data() _sensor = GridPowerSensor(_data, _controller) assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" - assert _sensor.uniq_name == f"{_sensor._energy_site_id} {_sensor.type}" - assert _sensor.get_power() == 0 + assert _sensor.get_power() == -3202.85009765625 @pytest.mark.asyncio @@ -97,7 +94,6 @@ async def test_battery_power_sensor(monkeypatch): _sensor = BatteryPowerSensor(_data, _controller) assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" - assert _sensor.uniq_name == f"{_sensor._energy_site_id} {_sensor.type}" assert _sensor.get_power() == 0 From 1391f3df7acbc12bf089206cce570df04fddab88 Mon Sep 17 00:00:00 2001 From: shred86 Date: Fri, 19 Aug 2022 18:46:43 -0700 Subject: [PATCH 22/84] Revert some random change --- teslajsonpy/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 52ff5d68..6b9e8950 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -1690,7 +1690,7 @@ def set_vehicle_id_vin(self, vehicle_id: Text, vin: Text) -> None: self.__vin_vehicle_id_map[vin] = vehicle_id @property - def update_interval(self) -> float: + def update_interval(self) -> int: """Return update_interval. Returns From 3158898afa3caa351b87ed20b9d52bb5f94e3645 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sat, 20 Aug 2022 17:21:29 -0700 Subject: [PATCH 23/84] Initial commit adding car module --- teslajsonpy/car.py | 561 ++++++++++++++++++++++++++++++++++++++ teslajsonpy/controller.py | 53 +++- teslajsonpy/energy.py | 39 ++- 3 files changed, 627 insertions(+), 26 deletions(-) create mode 100644 teslajsonpy/car.py diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py new file mode 100644 index 00000000..7620cbce --- /dev/null +++ b/teslajsonpy/car.py @@ -0,0 +1,561 @@ +"""Tesla car.""" +import logging + +from teslajsonpy.exceptions import HomelinkError + +_LOGGER = logging.getLogger(__name__) + + +class TeslaCar: + """Base class to represents a Tesla car.""" + + def __init__(self, car, controller) -> None: + """Initialize EnergySite.""" + self._car = car + # Temporary access to controller for now for rewrite + self._controller = controller + + @property + def display_name(self) -> dict: + """Return State Data.""" + return self._car["display_name"] + + @property + def id(self) -> dict: + """Return State Data.""" + return self._car["id"] + + @property + def state(self) -> dict: + """Return State Data.""" + return self._car["state"] + + @property + def vehicle_id(self) -> dict: + """Return State Data.""" + return self._car["vehicle_id"] + + @property + def vin(self) -> dict: + """Return State Data.""" + return self._car["vin"] + + @property + def battery_level(self) -> int: + """Return car battery level.""" + return self._controller.get_charging_params(vin=self.vin).get("battery_level") + + @property + def battery_range(self) -> int: + """Return car battery range.""" + return self._controller.get_charging_params(vin=self.vin).get("battery_range") + + @property + def charger_actual_current(self) -> dict: + """Return charger actual current.""" + return self._controller.get_charging_params(vin=self.vin).get( + "charger_actual_current" + ) + + @property + def charge_current_request(self) -> dict: + """Return charge current request.""" + return self._controller.get_charging_params(vin=self.vin).get( + "charge_current_request" + ) + + @property + def charge_current_request_max(self) -> dict: + """Return charge current request max.""" + return self._controller.get_charging_params(vin=self.vin).get( + "charge_current_request_max" + ) + + @property + def charge_port_latch(self) -> dict: + """Return charger port latch state. + + "Engaged" + Other states? + """ + return self._controller.get_charging_params(vin=self.vin).get("charge_port_latch") + + @property + def charge_energy_added(self) -> dict: + """Return charge energy added.""" + return self._controller.get_charging_params(vin=self.vin).get( + "charge_energy_added" + ) + + @property + def charge_limit_soc(self) -> dict: + """Return charge limit soc.""" + return self._controller.get_charging_params(vin=self.vin).get( + "charge_limit_soc" + ) + + @property + def charge_miles_added_ideal(self) -> dict: + """Return charge ideal miles added.""" + return self._controller.get_charging_params(vin=self.vin).get( + "charge_miles_added_ideal" + ) + + @property + def charge_miles_added_rated(self) -> dict: + """Return charge rated miles added.""" + return self._controller.get_charging_params(vin=self.vin).get( + "charge_miles_added_rated" + ) + + @property + def charger_phases(self) -> dict: + """Return charger phase.""" + return self._controller.get_charging_params(vin=self.vin).get("charger_phases") + + @property + def charger_power(self) -> dict: + """Return charger power.""" + return self._controller.get_charging_params(vin=self.vin).get("charger_power") + + @property + def charge_rate(self) -> dict: + """Return charge rate.""" + return self._controller.get_charging_params(vin=self.vin)["charge_rate"] + + @property + def charging_state(self) -> dict: + """Return charging state.""" + return self._controller.get_charging_params(vin=self.vin).get( + "charging_state" + ) + + @property + def charger_voltage(self) -> dict: + """Return charger voltage.""" + return self._controller.get_charging_params(vin=self.vin).get("charger_voltage") + + @property + def climate_keeper_mode(self) -> dict: + """Return climate keeper mode mode. + + Returns string "dog", "camp" or "on", "off" + API call not supported on all Tesla models. + """ + return self._controller.get_climate_params(vin=self.vin).get("climate_keeper_mode", "") + + @property + def conn_charge_cable(self) -> dict: + """Return charge cable connection.""" + return self._controller.get_charging_params(vin=self.vin).get("conn_charge_cable") + + @property + def defrost_mode(self) -> dict: + """Return defrost mode. + + On: 2 + Off: 0 + """ + return self._controller.get_climate_params(vin=self.vin).get("defrost_mode", 0) + + @property + def driver_temp_setting(self) -> dict: + """Return driver temperature setting.""" + return self._controller.get_climate_params(vin=self.vin).get("driver_temp_setting") + + @property + def fast_charger_present(self) -> dict: + """Return fast charger present.""" + return self._controller.get_charging_params(vin=self.vin).get("fast_charger_present") + + @property + def fast_charger_brand(self) -> dict: + """Return fast charger brand.""" + return self._controller.get_charging_params(vin=self.vin).get("fast_charger_brand") + + @property + def fast_charger_type(self) -> dict: + """Return fast charger type.""" + return self._controller.get_charging_params(vin=self.vin).get("fast_charger_type") + + @property + def gui_distance_units(self) -> dict: + """Return gui distance units.""" + # Why set default to mi/hr? + return self._controller.get_gui_params(vin=self.vin).get( + "gui_distance_units", "mi/hr" + ) + + @property + def gui_range_display(self) -> int: + """Return range display.""" + return self._controller.get_gui_params(vin=self.vin).get("gui_range_display") + + @property + def heading(self) -> str: + """Return heading.""" + return self._controller.get_drive_params(vin=self.vin).get("heading") + + @property + def homelink_device_count(self) -> int: + """Return Homelink device count.""" + return self._controller.get_state_params(vin=self.vin)["homelink_device_count"] + + @property + def homelink_nearby(self) -> dict: + """Return Homelink nearby.""" + return self._controller.get_state_params(vin=self.vin)["homelink_nearby"] + + @property + def ideal_battery_range(self) -> int: + """Return car ideal battery range.""" + return self._controller.get_charging_params(vin=self.vin)["ideal_battery_range"] + + @property + def inside_temp(self) -> dict: + """Return inside temperature.""" + return self._controller.get_climate_params(vin=self.vin).get("inside_temp") + + @property + def is_charge_port_door_open(self) -> dict: + """Return charger port door open.""" + return self._controller.get_charging_params(vin=self.vin).get("charge_port_door_open") + + @property + def is_climate_on(self) -> dict: + """Return climate is on.""" + return self._controller.get_climate_params(vin=self.vin).get("is_climate_on", False) + + @property + def is_frunk_locked(self) -> bool: + """Return car frunk is locked. + + Locked: 0 + Unlocked: 255 + """ + response = self._controller.get_state_params(vin=self.vin).get("ft") + + if response == 0: + return True + if response == 255: + return False + + @property + def is_locked(self) -> bool: + """Return car is locked.""" + return self._controller.get_state_params(vin=self.vin).get("locked") + + @property + def is_trunk_locked(self) -> int: + """Return car trunk is locked. + + Locked: 0 + Unlocked: 255 + """ + response = self._controller.get_state_params(vin=self.vin).get("rt") + + if response == 0: + return True + if response == 255: + return False + + @property + def is_on(self) -> dict: + """Return car is on.""" + return self._controller.car_online[self.vin] + + @property + def longitude(self) -> str: + """Return longitude.""" + return self._controller.get_drive_params(vin=self.vin).get("longitude") + + @property + def latitude(self) -> str: + """Return latitude.""" + return self._controller.get_drive_params(vin=self.vin).get("latitude") + + @property + def max_avail_temp(self) -> dict: + """Return max available temperature.""" + return self._controller.get_climate_params(vin=self.vin).get("max_avail_temp") + + @property + def min_avail_temp(self) -> dict: + """Return min available temperature.""" + return self._controller.get_climate_params(vin=self.vin).get("min_avail_temp") + + @property + def native_heading(self) -> str: + """Return native heading.""" + return self._controller.get_drive_params(vin=self.vin).get("native_heading") + + @property + def native_location_supported(self) -> str: + """Return native location supported.""" + return self._controller.get_drive_params(vin=self.vin).get("native_location_supported") + + @property + def native_longitude(self) -> str: + """Return native longitude.""" + return self._controller.get_drive_params(vin=self.vin).get("native_longitude") + + @property + def native_latitude(self) -> str: + """Return native latitude.""" + return self._controller.get_drive_params(vin=self.vin).get("native_latitude") + + @property + def odometer(self) -> float: + """Return odometer.""" + return self._controller.get_state_params(vin=self.vin)["odometer"] + + @property + def outside_temp(self) -> float: + """Return outside temperature.""" + return self._controller.get_climate_params(vin=self.vin).get("outside_temp") + + @property + def speed(self) -> str: + """Return speed.""" + return self._controller.get_drive_params(vin=self.vin).get("speed") + + @property + def shift_state(self) -> str: + """Return shift state.""" + return self._controller.get_drive_params(vin=self.vin).get("shift_state") + + @property + def time_to_full_charge(self) -> float: + """Return time to full charge.""" + return self._controller.get_charging_params(vin=self.vin).get( + "time_to_full_charge" + ) + + async def _send_command( + self, name: str, *, path_vars: dict, wake_if_asleep: bool = False, **kwargs + ): + """Wrapper for sending commands to the Tesla API.""" + _LOGGER.debug("Sending command: %s", name) + data = await self._controller.api( + name, path_vars=path_vars, wake_if_asleep=wake_if_asleep, **kwargs + ) + _LOGGER.debug("Response from command %s: %s", name, data) + return data + + def _get_lat_long(self): + """Get current latitude and longitude.""" + lat = None + long = None + + if self.native_location_supported: + long = self.native_longitude + lat = self.native_latitude + else: + long = self.longitude + lat = self.latitude + + return lat, long + + async def charge_port_door_close(self) -> None: + """Send command to close charge port door.""" + data = await self._send_command( + "CHARGE_PORT_DOOR_CLOSE", + path_vars={"vehicle_id": self.id}, + wake_if_asleep=True, + ) + + if data and data["response"]["result"]: + params = { + "charge_port_door_open": False + } + self._controller.update_state_params(vin=self.vin, params=params) + + async def charge_port_door_open(self) -> None: + """Send command to open charge port door.""" + data = await self._send_command( + "CHARGE_PORT_DOOR_OPEN", + path_vars={"vehicle_id": self.id}, + wake_if_asleep=True, + ) + + if data and data["response"]["result"]: + params = { + "charge_port_door_open": True + } + self._controller.update_state_params(vin=self.vin, params=params) + + async def flash_lights(self) -> None: + """Send command to flash lights.""" + await self._send_command( + "FLASH_LIGHTS", + path_vars={"vehicle_id": self.id}, + on=True, + wake_if_asleep=True, + ) + + async def honk_horn(self) -> None: + """Send command to honk horn.""" + await self._send_command( + "HONK_HORN", + path_vars={"vehicle_id": self.id}, + on=True, + wake_if_asleep=True, + ) + + async def lock(self): + """Send lock command.""" + data = await self._send_command( + "LOCK", + path_vars={"vehicle_id": self.id}, + wake_if_asleep=True, + ) + if data and data["response"]["result"]: + params = { + "locked": True + } + self._controller.update_state_params(vin=self.vin, params=params) + + async def set_climate_keeper_mode(self, keeper_id) -> None: + """Send command to set climate keeper mode. + + Keep On: 1 + Dog Mode: 2 + Camp Mode: 3 + """ + await self._send_command( + "SET_CLIMATE_KEEPER_MODE", + path_vars={"vehicle_id": self.id}, + climate_keeper_mode=keeper_id, + wake_if_asleep=True, + ) + + async def set_max_defrost(self, state: bool) -> None: + """Send command to set max defrost. + + On: 2 + Off: 0 + """ + await self._send_command( + "MAX_DEFROST", + path_vars={"vehicle_id": self.id}, + on=state, + wake_if_asleep=True, + ) + + async def set_temperature(self, temp) -> dict: + """Send command to set temperature.""" + data = await self._send_command( + "CHANGE_CLIMATE_TEMPERATURE_SETTING", + path_vars={"vehicle_id": self.id}, + driver_temp=temp, + passenger_temp=temp, + wake_if_asleep=True, + ) + if data and data["response"]["result"]: + params = { + "driver_temp_setting": temp + } + + self._controller.update_climate_params(vin=self.vin, params=params) + + async def set_hvac_mode(self, on_off: str) -> None: + """Send command to set HVAC mode.""" + # Better name for on_off? + if on_off == "off": + await self._send_command( + "CLIMATE_OFF", + path_vars={"vehicle_id": self.id}, + wake_if_asleep=True, + ) + elif on_off == "on": + await self._send_command( + "CLIMATE_ON", + path_vars={"vehicle_id": self.id}, + wake_if_asleep=True, + ) + + async def wake_up(self) -> None: + """Send command to wake up.""" + await self._send_command( + "WAKE_UP", + path_vars={"vehicle_id": self.id}, + wake_if_asleep=True, + ) + + async def toggle_trunk(self): + """Actuate rear trunk lock.""" + data = await self._send_command( + "ACTUATE_TRUNK", + path_vars={"vehicle_id": self.id}, + which_trunk="rear", + wake_if_asleep=True, + ) + if data and data["response"]["result"]: + if self.is_trunk_locked: + params = { + "rt": 0 + } + self._controller.update_state_params(vin=self.vin, params=params) + if not self.is_trunk_locked: + params = { + "rt": 255 + } + self._controller.update_state_params(vin=self.vin, params=params) + + async def toggle_frunk(self): + """Actuate front trunk lock.""" + data = await self._send_command( + "ACTUATE_TRUNK", + path_vars={"vehicle_id": self.id}, + which_trunk="front", + wake_if_asleep=True, + ) + if data and data["response"]["result"]: + if self.is_frunk_locked: + params = { + "ft": 0 + } + self._controller.update_state_params(vin=self.vin, params=params) + if not self.is_frunk_locked: + params = { + "ft": 255 + } + self._controller.update_state_params(vin=self.vin, params=params) + + async def trigger_homelink(self): + """Send command to trigger homelink.""" + if self.homelink_device_count is None: + raise HomelinkError(f"No homelink devices added to {self.display_name}.") + + if self.homelink_nearby is not True: + raise HomelinkError(f"No homelink devices near {self.display_name}.") + + lat, long = self._get_lat_long() + + data = await self._send_command( + "TRIGGER_HOMELINK", + path_vars={"vehicle_id": self.id}, + lat=lat, + lon=long, + wake_if_asleep=True, + ) + + if data and data.get("response"): + _LOGGER.debug("Homelink response: %s", data.get("response")) + result = data["response"].get("result") + reason = data["response"].get("reason") + if result is False: + raise HomelinkError(f"Error calling trigger_homelink: {reason}") + + async def unlock(self): + """Send unlock command.""" + data = await self._send_command( + "UNLOCK", + path_vars={"vehicle_id": self.id}, + wake_if_asleep=True, + ) + if data and data["response"]["result"]: + params = { + "locked": False + } + self._controller.update_state_params(vin=self.vin, params=params) \ No newline at end of file diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 6b9e8950..f9f2be5f 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -75,6 +75,7 @@ VehicleStateDataSensor, ) +from teslajsonpy.car import TeslaCar from teslajsonpy.energy import SolarSite, PowerwallSite, SolarPowerwallSite _LOGGER = logging.getLogger(__name__) @@ -381,6 +382,7 @@ def __init__( self.enable_websocket = enable_websocket self.polling_policy = polling_policy self.cars = {} + self.cars_raw = {} self.endpoints = {} self.energysites = {} self.__energysites = {} @@ -409,11 +411,13 @@ async def connect( if mfa_code: self.__connection.mfa_code = mfa_code - self.cars = await self.get_vehicles() + + self.cars_raw = await self.get_vehicles() + self._last_attempted_update_time = round(time.time()) self.__update_lock = asyncio.Lock() - for car in self.cars: + for car in self.cars_raw: vin = car["vin"] if filtered_vins and vin not in filtered_vins: _LOGGER.debug("Skipping car with VIN: %s", vin) @@ -437,10 +441,8 @@ async def connect( self.__config[vin] = {} self.__driving[vin] = {} self.__gui[vin] = {} - # This is temporary to provide backwards compatability with - # previous version of Home Assistant Tesla Custom Integration - if not skip_add: - self._add_car_components(car) + + self._generate_car_objects() self.__energysites = await self.get_energysites() @@ -460,10 +462,6 @@ async def connect( } self.__lock[energysite_id] = asyncio.Lock() - # This is temporary to provide backwards compatability with - # previous version of Home Assistant Tesla Custom Integration - if not skip_add: - self._add_energysite_components(energysite) self._generate_energysite_objects() @@ -560,7 +558,7 @@ async def get_energysites(self): return [ p for p in (await self.api("PRODUCT_LIST"))["response"] - if p.get(RESOURCE_TYPE) == RESOURCE_TYPE_SOLAR + if p.get(RESOURCE_TYPE) == RESOURCE_TYPE_SOLAR or p.get(RESOURCE_TYPE) == RESOURCE_TYPE_BATTERY ] @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) @@ -730,6 +728,12 @@ async def command( product_type=product_type, ) + def _generate_car_objects(self) -> None: + """Generate car objects.""" + for car in self.cars_raw: + vin = car["vin"] + self.cars[vin] = TeslaCar(car, self) + def _generate_energysite_objects(self) -> None: """Generate energy site objects.""" for energysite in self.__energysites: @@ -737,7 +741,7 @@ def _generate_energysite_objects(self) -> None: # Solar only systems (no Powerwalls) are listed as "solar" if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: self.energysites[energysite_id] = SolarSite( - energysite, self.__power_data[energysite_id] + self.api, energysite, self.__power_data[energysite_id] ) # Solar with Powerwall are listed as "battery" if ( @@ -745,7 +749,7 @@ def _generate_energysite_objects(self) -> None: and energysite["components"]["solar"] ): self.energysites[energysite_id] = SolarPowerwallSite( - energysite, self.__power_data[energysite_id] + self.api, energysite, self.__power_data[energysite_id] ) # Assumed Powerwall only (no solar) is listed as "battery" if ( @@ -753,7 +757,7 @@ def _generate_energysite_objects(self) -> None: and not energysite["components"]["solar"] ): self.energysites[energysite_id] = PowerwallSite( - energysite, self.__power_data[energysite_id] + self.api, energysite, self.__power_data[energysite_id] ) def get_homeassistant_components(self): @@ -1200,6 +1204,17 @@ def set_climate_params( if vin: self.__climate[vin] = params + def update_climate_params( + self, car_id: Text = None, vin: Text = None, params: Dict = None + ) -> None: + """Set climate_params for car_id.""" + # Used to update params in self.__climate for TeslaCar.set_temperature + params = params or {} + if car_id and not vin: + vin = self._id_to_vin(car_id) + if vin: + self.__climate[vin].update(params) + def is_climate_on(self, car_id: Text = None, vin: Text = None) -> bool: """Return true if climate is on.""" if car_id and not vin: @@ -1292,6 +1307,16 @@ def set_state_params( if vin: self.__state[vin] = params + def update_state_params( + self, car_id: Text = None, vin: Text = None, params: Dict = None + ) -> None: + """Update state_params for car_id.""" + params = params or {} + if car_id and not vin: + vin = self._id_to_vin(car_id) + if vin: + self.__state[vin].update(params) + def is_sentry_mode_on(self, car_id: Text = None, vin: Text = None) -> bool: """Return true if sentry_mode is on.""" if car_id and not vin: diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index 3190f069..c001854a 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -9,8 +9,9 @@ class EnergySite: """Base class to represents a Tesla Energy site.""" - def __init__(self, energysite, power_data) -> None: - """Initialize energy site.""" + def __init__(self, api, energysite, power_data) -> None: + """Initialize EnergySite.""" + self._api = api self._energy_site = energysite self._power_data = power_data @@ -48,8 +49,9 @@ class SolarSite(EnergySite): by :meth:`teslajsonpy.controller.generate_energysite_objects`. """ - def __init__(self, energysite, power_data) -> None: - super().__init__(energysite, power_data) + def __init__(self, api, energysite, power_data) -> None: + """Initialize SolarSite.""" + super().__init__(api, energysite, power_data) @property def grid_power(self) -> int: @@ -81,8 +83,11 @@ class PowerwallSite(EnergySite): by :meth:`teslajsonpy.controller.generate_energy_site_objects`. """ - def __init__(self, energysite, power_data) -> None: - super().__init__(energysite, power_data) + def __init__(self, api, energysite, power_data) -> None: + """Initialize PowerwallSite.""" + super().__init__(api, energysite, power_data) + # self.__default_real_mode = None + # self.__backup_reserve_percent = None @property def battery_percent(self) -> int: @@ -106,10 +111,19 @@ def load_power(self) -> int: """Return load power in Watts.""" return self._power_data["load_power"] - def set_operation_mode() -> None: - """Set operation mode of Powerwall.""" - # Implement POST request to set Powerwall operation mode - return + # async def set_operation_mode(self, real_mode, backup_reserve_percent) -> None: + # """Set operation mode of Powerwall.""" + # # real_mode - self_consumption, backup, autonomous + # # backup_reserve_percent - 1-100 + # data = await self._api( + # "BATTERY_OPERATION_MODE", + # path_vars={"battery_id": self.id}, + # default_real_mode=real_mode, + # backup_reserve_percent=int(backup_reserve_percent), + # ) + # if data and data["response"]["result"]: + # self.__default_real_mode = real_mode + # self.__backup_reserve_percent = backup_reserve_percent class SolarPowerwallSite(PowerwallSite, SolarSite): @@ -119,5 +133,6 @@ class SolarPowerwallSite(PowerwallSite, SolarSite): by :meth:`teslajsonpy.controller.generate_energy_site_objects`. """ - def __init__(self, energysite, power_data) -> None: - super().__init__(energysite, power_data) + def __init__(self, api, energysite, power_data) -> None: + """Initialize SolarPowerwallSite.""" + super().__init__(api, energysite, power_data) From 4890ed03fb11ccbedf3e279a921be51e0ec80a8d Mon Sep 17 00:00:00 2001 From: shred86 Date: Sat, 20 Aug 2022 23:18:13 -0700 Subject: [PATCH 24/84] More properties and methods --- teslajsonpy/car.py | 307 ++++++++++++++++++++++++++++++++++---- teslajsonpy/const.py | 2 + teslajsonpy/controller.py | 12 +- 3 files changed, 288 insertions(+), 33 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 7620cbce..93051c87 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -6,6 +6,23 @@ _LOGGER = logging.getLogger(__name__) +CABIN_OPTIONS = [ + "Off", + "No A/C", + "On", +] + +SEAT_ID_MAP = { + "left": 0, + "right": 1, + "rear_left": 2, + "rear_center": 4, + "rear_right": 5, + "third_row_left": 6, + "third_row_right": 7, +} + + class TeslaCar: """Base class to represents a Tesla car.""" @@ -16,30 +33,35 @@ def __init__(self, car, controller) -> None: self._controller = controller @property - def display_name(self) -> dict: + def display_name(self) -> str: """Return State Data.""" return self._car["display_name"] @property - def id(self) -> dict: + def id(self) -> str: """Return State Data.""" return self._car["id"] @property - def state(self) -> dict: + def state(self) -> str: """Return State Data.""" return self._car["state"] @property - def vehicle_id(self) -> dict: + def vehicle_id(self) -> str: """Return State Data.""" return self._car["vehicle_id"] @property - def vin(self) -> dict: + def vin(self) -> str: """Return State Data.""" return self._car["vin"] + @property + def data_available(self) -> int: + """Return if data is available.""" + return self._controller.get_state_params(vin=self.vin) + @property def battery_level(self) -> int: """Return car battery level.""" @@ -50,6 +72,22 @@ def battery_range(self) -> int: """Return car battery range.""" return self._controller.get_charging_params(vin=self.vin).get("battery_range") + @property + def cabin_overheat_protection(self) -> dict: + """Return cabin overheat protection.""" + return self._controller.get_climate_params(vin=self.vin).get("cabin_overheat_protection") + + @property + def car_type(self) -> int: + """Return car type.""" + # This is actually listed in PRODUCT_LIST + return f"Model {str(self.vin[3]).upper()}" + + @property + def car_version(self) -> int: + """Return installed car software version.""" + return self._controller.get_state_params(vin=self.vin)["car_version"] + @property def charger_actual_current(self) -> dict: """Return charger actual current.""" @@ -65,14 +103,14 @@ def charge_current_request(self) -> dict: ) @property - def charge_current_request_max(self) -> dict: + def charge_current_request_max(self) -> float: """Return charge current request max.""" return self._controller.get_charging_params(vin=self.vin).get( "charge_current_request_max" ) @property - def charge_port_latch(self) -> dict: + def charge_port_latch(self) -> str: """Return charger port latch state. "Engaged" @@ -81,19 +119,33 @@ def charge_port_latch(self) -> dict: return self._controller.get_charging_params(vin=self.vin).get("charge_port_latch") @property - def charge_energy_added(self) -> dict: + def charge_energy_added(self) -> float: """Return charge energy added.""" return self._controller.get_charging_params(vin=self.vin).get( "charge_energy_added" ) @property - def charge_limit_soc(self) -> dict: + def charge_limit_soc(self) -> float: """Return charge limit soc.""" return self._controller.get_charging_params(vin=self.vin).get( "charge_limit_soc" ) + @property + def charge_limit_soc_max(self) -> float: + """Return charge limit soc max.""" + return self._controller.get_charging_params(vin=self.vin).get( + "charge_limit_soc_max" + ) + + @property + def charge_limit_soc_min(self) -> float: + """Return charge limit soc min.""" + return self._controller.get_charging_params(vin=self.vin).get( + "charge_limit_soc_min" + ) + @property def charge_miles_added_ideal(self) -> dict: """Return charge ideal miles added.""" @@ -124,7 +176,7 @@ def charge_rate(self) -> dict: return self._controller.get_charging_params(vin=self.vin)["charge_rate"] @property - def charging_state(self) -> dict: + def charging_state(self) -> str: """Return charging state.""" return self._controller.get_charging_params(vin=self.vin).get( "charging_state" @@ -199,17 +251,17 @@ def heading(self) -> str: @property def homelink_device_count(self) -> int: """Return Homelink device count.""" - return self._controller.get_state_params(vin=self.vin)["homelink_device_count"] + return self._controller.get_state_params(vin=self.vin).get("homelink_device_count") @property def homelink_nearby(self) -> dict: """Return Homelink nearby.""" - return self._controller.get_state_params(vin=self.vin)["homelink_nearby"] + return self._controller.get_state_params(vin=self.vin).get("homelink_nearby") @property def ideal_battery_range(self) -> int: """Return car ideal battery range.""" - return self._controller.get_charging_params(vin=self.vin)["ideal_battery_range"] + return self._controller.get_charging_params(vin=self.vin).get("ideal_battery_range") @property def inside_temp(self) -> dict: @@ -245,6 +297,11 @@ def is_locked(self) -> bool: """Return car is locked.""" return self._controller.get_state_params(vin=self.vin).get("locked") + @property + def is_steering_wheel_heater_on(self) -> bool: + """Return steering wheel heater.""" + return self._controller.get_climate_params(vin=self.vin).get("steering_wheel_heater") + @property def is_trunk_locked(self) -> int: """Return car trunk is locked. @@ -315,15 +372,35 @@ def outside_temp(self) -> float: return self._controller.get_climate_params(vin=self.vin).get("outside_temp") @property - def speed(self) -> str: - """Return speed.""" - return self._controller.get_drive_params(vin=self.vin).get("speed") + def sentry_mode(self) -> bool: + """Return sentry mode.""" + return self._controller.get_state_params(vin=self.vin).get("sentry_mode") + + @property + def sentry_mode_available(self) -> bool: + """Return sentry mode available.""" + return self._controller.get_state_params(vin=self.vin).get("sentry_mode_available") @property def shift_state(self) -> str: """Return shift state.""" return self._controller.get_drive_params(vin=self.vin).get("shift_state") + @property + def speed(self) -> str: + """Return speed.""" + return self._controller.get_drive_params(vin=self.vin).get("speed") + + @property + def software_update(self) -> dict: + """Return software update version information.""" + return self._controller.get_state_params(vin=self.vin).get("software_update", {}) + + @property + def third_row_seats(self) -> bool: + """Return third row seats option.""" + return self._controller.get_state_params(vin=self.vin).get("third_row_seats") + @property def time_to_full_charge(self) -> float: """Return time to full charge.""" @@ -356,6 +433,21 @@ def _get_lat_long(self): return lat, long + async def change_charge_limit(self, value: float) -> None: + """Send command to change charge limit.""" + data = await self._send_command( + "CHANGE_CHARGE_LIMIT", + path_vars={"vehicle_id": self.id}, + percent=int(value), + wake_if_asleep=True, + ) + + if data and data["response"]["result"] is True: + params = { + "charge_limit_soc": int(value) + } + self._controller.update_charging_params(vin=self.vin, params=params) + async def charge_port_door_close(self) -> None: """Send command to close charge port door.""" data = await self._send_command( @@ -409,12 +501,105 @@ async def lock(self): path_vars={"vehicle_id": self.id}, wake_if_asleep=True, ) - if data and data["response"]["result"]: + if data and data["response"]["result"] is True: params = { "locked": True } self._controller.update_state_params(vin=self.vin, params=params) + async def remote_seat_heater_request(self, level: int, seat_id: int) -> None: + """Send command to change seat heat. + + Levels: + -Off: 0 + -Low: 1 + -Medium: 2 + -High: 3 + + Seat ID: + -Left: 0 + -Right": 1 + -Rear_left": 2 + -Rear_center": 4 + -Rear_right": 5 + -Third_row_left": 6 + -Third_row_right": 7 + """ + + data = await self._send_command( + "REMOTE_SEAT_HEATER_REQUEST", + path_vars={"vehicle_id": self.id}, + heater=seat_id, + level=level, + wake_if_asleep=True, + ) + if data and data["response"]["result"]: + params = { + f"seat_{seat_id}_heater": level + } + self._controller.update_climate_params(vin=self.vin, params=params) + + def get_seat_heater_status(self, seat_id) -> int: + """Return status of seat heater for a given seat.""" + seat_id = f"seat_{seat_id}_heater" + return self._controller.get_climate_params(vin=self.vin).get(seat_id) + + async def schedule_software_update(self, offset_sec=0) -> None: + """Send command to install software update.""" + await self._coordinator.controller.api( + "SCHEDULE_SOFTWARE_UPDATE", + path_vars={"vehicle_id": self.id}, + offset_sec=offset_sec, + wake_if_asleep=True, + ) + + async def set_charging_amps(self, value: float) -> None: + """Send command to set charging amps.""" + data = await self._send_command( + "CHARGING_AMPS", + path_vars={"vehicle_id": self.id}, + charging_amps=int(value), + wake_if_asleep=True, + ) + + if data and data["response"]["result"] is True: + params = { + "charge_amps": int(value) + } + self._controller.update_charging_params(vin=self.vin, params=params) + + async def set_cabin_overheat_protection(self, option: str) -> None: + """Send command to set cabin overheat protection. + + Options: + -"Off" + -"No A/C" + -"On" + """ + + if option == "Off": + body_on = False + fan_only = False + elif option == "No A/C": + body_on = True + fan_only = True + elif option == "On": + body_on = True + fan_only = False + + data = await self._send_command( + "SET_CABIN_OVERHEAT_PROTECTION", + path_vars={"vehicle_id": self.id}, + on=body_on, + fan_only=fan_only, + wake_if_asleep=True, + ) + if data and data["response"]["result"]: + params = { + "cabin_overheat_protection": option + } + self._controller.update_climate_params(vin=self.vin, params=params) + async def set_climate_keeper_mode(self, keeper_id) -> None: """Send command to set climate keeper mode. @@ -429,6 +614,37 @@ async def set_climate_keeper_mode(self, keeper_id) -> None: wake_if_asleep=True, ) + async def set_heated_steering_wheel(self, value: bool) -> None: + """Send command to set heated steering wheel.""" + data = await self._send_command( + "REMOTE_STEERING_WHEEL_HEATER_REQUEST", + path_vars={"vehicle_id": self.id}, + on=value, + wake_if_asleep=True, + ) + + if data and data["response"]["result"]: + params = { + "steering_wheel_heater": value + } + self._controller.update_climate_params(vin=self.vin, params=params) + + async def set_hvac_mode(self, on_off: str) -> None: + """Send command to set HVAC mode.""" + # Better name for on_off? + if on_off == "off": + await self._send_command( + "CLIMATE_OFF", + path_vars={"vehicle_id": self.id}, + wake_if_asleep=True, + ) + elif on_off == "on": + await self._send_command( + "CLIMATE_ON", + path_vars={"vehicle_id": self.id}, + wake_if_asleep=True, + ) + async def set_max_defrost(self, state: bool) -> None: """Send command to set max defrost. @@ -442,6 +658,21 @@ async def set_max_defrost(self, state: bool) -> None: wake_if_asleep=True, ) + async def set_sentry_mode(self, value: bool) -> None: + """Send command to set sentry mode.""" + data = await self._send_command( + "SET_SENTRY_MODE", + path_vars={"vehicle_id": self.id}, + on=value, + wake_if_asleep=True, + ) + + if data and data["response"]["result"]: + params = { + "sentry_mode": value + } + self._controller.update_state_params(vin=self.vin, params=params) + async def set_temperature(self, temp) -> dict: """Send command to set temperature.""" data = await self._send_command( @@ -458,21 +689,33 @@ async def set_temperature(self, temp) -> dict: self._controller.update_climate_params(vin=self.vin, params=params) - async def set_hvac_mode(self, on_off: str) -> None: - """Send command to set HVAC mode.""" - # Better name for on_off? - if on_off == "off": - await self._send_command( - "CLIMATE_OFF", - path_vars={"vehicle_id": self.id}, - wake_if_asleep=True, - ) - elif on_off == "on": - await self._send_command( - "CLIMATE_ON", - path_vars={"vehicle_id": self.id}, - wake_if_asleep=True, - ) + async def start_charge(self): + """Send command to start charge.""" + data = await self._send_command( + "START_CHARGE", + path_vars={"vehicle_id": self.id}, + wake_if_asleep=True, + ) + + if data and data["response"]["result"] is True: + params = { + "charging_state": "Charging" + } + self._controller.update_charging_params(vin=self.vin, params=params) + + async def stop_charge(self): + """Send command to start charge.""" + data = await self._send_command( + "STOP_CHARGE", + path_vars={"vehicle_id": self.id}, + wake_if_asleep=True, + ) + + if data and data["response"]["result"] is True: + params = { + "charging_state": None + } + self._controller.update_charging_params(vin=self.vin, params=params) async def wake_up(self) -> None: """Send command to wake up.""" diff --git a/teslajsonpy/const.py b/teslajsonpy/const.py index 3d79f7e6..caa3a00c 100644 --- a/teslajsonpy/const.py +++ b/teslajsonpy/const.py @@ -18,6 +18,8 @@ TESLA_PRODUCT_TYPE_VEHICLES = "vehicles" +CHARGE_CURRENT_MIN = 5 + PRODUCT_TYPE_ENERGY_SITES = "energy_sites" PRODUCT_TYPE_POWERWALLS = "powerwalls" DEFAULT_ENERGY_SITE_NAME = "My Home" diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index f9f2be5f..7c0c1b32 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -554,7 +554,7 @@ async def get_vehicles(self): @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) async def get_energysites(self): - """Get energy sites json from TeslaAPI and filter to solar.""" + """Get energy sites json from TeslaAPI and filter to solar or battery sites.""" return [ p for p in (await self.api("PRODUCT_LIST"))["response"] @@ -1261,6 +1261,16 @@ def set_charging_params( if vin: self.__charging[vin] = params + def update_charging_params( + self, car_id: Text = None, vin: Text = None, params: Dict = None + ) -> None: + """Update charging_params for car_id.""" + params = params or {} + if car_id and not vin: + vin = self._id_to_vin(car_id) + if vin: + self.__charging[vin].update(params) + def charging_state(self, car_id: Text = None, vin: Text = None) -> Text: """Return charging state for a single vehicle.""" if car_id and not vin: From 8772e8a65aba828f5dd92aa786960d5fafc8a3f4 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 21 Aug 2022 14:49:16 -0700 Subject: [PATCH 25/84] Clean up typing return and connect method --- teslajsonpy/car.py | 280 ++++++++++++++--------------- teslajsonpy/const.py | 2 +- teslajsonpy/controller.py | 64 +++---- teslajsonpy/energy.py | 61 ++++--- teslajsonpy/homeassistant/power.py | 4 +- 5 files changed, 202 insertions(+), 209 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 93051c87..a79128c0 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -5,23 +5,16 @@ _LOGGER = logging.getLogger(__name__) - -CABIN_OPTIONS = [ - "Off", - "No A/C", - "On", +SEAT_NAME_MAP = [ + "left", + "right", + "rear_left", + "rear_center", + "rear_right", + "third_row_left", + "third_row_right", ] -SEAT_ID_MAP = { - "left": 0, - "right": 1, - "rear_left": 2, - "rear_center": 4, - "rear_right": 5, - "third_row_left": 6, - "third_row_right": 7, -} - class TeslaCar: """Base class to represents a Tesla car.""" @@ -38,7 +31,7 @@ def display_name(self) -> str: return self._car["display_name"] @property - def id(self) -> str: + def id(self) -> int: """Return State Data.""" return self._car["id"] @@ -48,7 +41,7 @@ def state(self) -> str: return self._car["state"] @property - def vehicle_id(self) -> str: + def vehicle_id(self) -> int: """Return State Data.""" return self._car["vehicle_id"] @@ -58,52 +51,54 @@ def vin(self) -> str: return self._car["vin"] @property - def data_available(self) -> int: + def data_available(self) -> bool: """Return if data is available.""" return self._controller.get_state_params(vin=self.vin) @property - def battery_level(self) -> int: + def battery_level(self) -> float: """Return car battery level.""" return self._controller.get_charging_params(vin=self.vin).get("battery_level") @property - def battery_range(self) -> int: + def battery_range(self) -> float: """Return car battery range.""" return self._controller.get_charging_params(vin=self.vin).get("battery_range") @property - def cabin_overheat_protection(self) -> dict: + def cabin_overheat_protection(self) -> str: """Return cabin overheat protection.""" - return self._controller.get_climate_params(vin=self.vin).get("cabin_overheat_protection") + return self._controller.get_climate_params(vin=self.vin).get( + "cabin_overheat_protection" + ) @property - def car_type(self) -> int: + def car_type(self) -> str: """Return car type.""" # This is actually listed in PRODUCT_LIST return f"Model {str(self.vin[3]).upper()}" @property - def car_version(self) -> int: + def car_version(self) -> str: """Return installed car software version.""" return self._controller.get_state_params(vin=self.vin)["car_version"] @property - def charger_actual_current(self) -> dict: + def charger_actual_current(self) -> int: """Return charger actual current.""" return self._controller.get_charging_params(vin=self.vin).get( "charger_actual_current" ) @property - def charge_current_request(self) -> dict: + def charge_current_request(self) -> int: """Return charge current request.""" return self._controller.get_charging_params(vin=self.vin).get( "charge_current_request" ) @property - def charge_current_request_max(self) -> float: + def charge_current_request_max(self) -> int: """Return charge current request max.""" return self._controller.get_charging_params(vin=self.vin).get( "charge_current_request_max" @@ -116,7 +111,9 @@ def charge_port_latch(self) -> str: "Engaged" Other states? """ - return self._controller.get_charging_params(vin=self.vin).get("charge_port_latch") + return self._controller.get_charging_params(vin=self.vin).get( + "charge_port_latch" + ) @property def charge_energy_added(self) -> float: @@ -126,83 +123,85 @@ def charge_energy_added(self) -> float: ) @property - def charge_limit_soc(self) -> float: + def charge_limit_soc(self) -> int: """Return charge limit soc.""" return self._controller.get_charging_params(vin=self.vin).get( "charge_limit_soc" ) @property - def charge_limit_soc_max(self) -> float: + def charge_limit_soc_max(self) -> int: """Return charge limit soc max.""" return self._controller.get_charging_params(vin=self.vin).get( "charge_limit_soc_max" ) @property - def charge_limit_soc_min(self) -> float: + def charge_limit_soc_min(self) -> int: """Return charge limit soc min.""" return self._controller.get_charging_params(vin=self.vin).get( "charge_limit_soc_min" ) @property - def charge_miles_added_ideal(self) -> dict: + def charge_miles_added_ideal(self) -> float: """Return charge ideal miles added.""" return self._controller.get_charging_params(vin=self.vin).get( "charge_miles_added_ideal" ) @property - def charge_miles_added_rated(self) -> dict: + def charge_miles_added_rated(self) -> float: """Return charge rated miles added.""" return self._controller.get_charging_params(vin=self.vin).get( "charge_miles_added_rated" ) @property - def charger_phases(self) -> dict: + def charger_phases(self) -> int: """Return charger phase.""" return self._controller.get_charging_params(vin=self.vin).get("charger_phases") @property - def charger_power(self) -> dict: + def charger_power(self) -> int: """Return charger power.""" return self._controller.get_charging_params(vin=self.vin).get("charger_power") @property - def charge_rate(self) -> dict: + def charge_rate(self) -> str: """Return charge rate.""" return self._controller.get_charging_params(vin=self.vin)["charge_rate"] @property def charging_state(self) -> str: """Return charging state.""" - return self._controller.get_charging_params(vin=self.vin).get( - "charging_state" - ) + return self._controller.get_charging_params(vin=self.vin).get("charging_state") @property - def charger_voltage(self) -> dict: + def charger_voltage(self) -> int: """Return charger voltage.""" return self._controller.get_charging_params(vin=self.vin).get("charger_voltage") @property - def climate_keeper_mode(self) -> dict: + def climate_keeper_mode(self) -> str: """Return climate keeper mode mode. Returns string "dog", "camp" or "on", "off" API call not supported on all Tesla models. """ - return self._controller.get_climate_params(vin=self.vin).get("climate_keeper_mode", "") + return self._controller.get_climate_params(vin=self.vin).get( + "climate_keeper_mode", "" + ) @property - def conn_charge_cable(self) -> dict: + def conn_charge_cable(self) -> str: """Return charge cable connection.""" - return self._controller.get_charging_params(vin=self.vin).get("conn_charge_cable") + return self._controller.get_charging_params(vin=self.vin).get( + "conn_charge_cable" + ) @property - def defrost_mode(self) -> dict: + def defrost_mode(self) -> int: """Return defrost mode. On: 2 @@ -211,27 +210,35 @@ def defrost_mode(self) -> dict: return self._controller.get_climate_params(vin=self.vin).get("defrost_mode", 0) @property - def driver_temp_setting(self) -> dict: + def driver_temp_setting(self) -> float: """Return driver temperature setting.""" - return self._controller.get_climate_params(vin=self.vin).get("driver_temp_setting") + return self._controller.get_climate_params(vin=self.vin).get( + "driver_temp_setting" + ) @property - def fast_charger_present(self) -> dict: + def fast_charger_present(self) -> bool: """Return fast charger present.""" - return self._controller.get_charging_params(vin=self.vin).get("fast_charger_present") + return self._controller.get_charging_params(vin=self.vin).get( + "fast_charger_present" + ) @property - def fast_charger_brand(self) -> dict: + def fast_charger_brand(self) -> str: """Return fast charger brand.""" - return self._controller.get_charging_params(vin=self.vin).get("fast_charger_brand") + return self._controller.get_charging_params(vin=self.vin).get( + "fast_charger_brand" + ) @property - def fast_charger_type(self) -> dict: + def fast_charger_type(self) -> str: """Return fast charger type.""" - return self._controller.get_charging_params(vin=self.vin).get("fast_charger_type") + return self._controller.get_charging_params(vin=self.vin).get( + "fast_charger_type" + ) @property - def gui_distance_units(self) -> dict: + def gui_distance_units(self) -> str: """Return gui distance units.""" # Why set default to mi/hr? return self._controller.get_gui_params(vin=self.vin).get( @@ -239,47 +246,55 @@ def gui_distance_units(self) -> dict: ) @property - def gui_range_display(self) -> int: + def gui_range_display(self) -> str: """Return range display.""" return self._controller.get_gui_params(vin=self.vin).get("gui_range_display") @property - def heading(self) -> str: + def heading(self) -> int: """Return heading.""" return self._controller.get_drive_params(vin=self.vin).get("heading") @property def homelink_device_count(self) -> int: """Return Homelink device count.""" - return self._controller.get_state_params(vin=self.vin).get("homelink_device_count") + return self._controller.get_state_params(vin=self.vin).get( + "homelink_device_count" + ) @property - def homelink_nearby(self) -> dict: + def homelink_nearby(self) -> bool: """Return Homelink nearby.""" return self._controller.get_state_params(vin=self.vin).get("homelink_nearby") @property - def ideal_battery_range(self) -> int: + def ideal_battery_range(self) -> float: """Return car ideal battery range.""" - return self._controller.get_charging_params(vin=self.vin).get("ideal_battery_range") + return self._controller.get_charging_params(vin=self.vin).get( + "ideal_battery_range" + ) @property - def inside_temp(self) -> dict: + def inside_temp(self) -> float: """Return inside temperature.""" return self._controller.get_climate_params(vin=self.vin).get("inside_temp") @property - def is_charge_port_door_open(self) -> dict: + def is_charge_port_door_open(self) -> bool: """Return charger port door open.""" - return self._controller.get_charging_params(vin=self.vin).get("charge_port_door_open") + return self._controller.get_charging_params(vin=self.vin).get( + "charge_port_door_open" + ) @property - def is_climate_on(self) -> dict: + def is_climate_on(self) -> bool: """Return climate is on.""" - return self._controller.get_climate_params(vin=self.vin).get("is_climate_on", False) + return self._controller.get_climate_params(vin=self.vin).get( + "is_climate_on", False + ) @property - def is_frunk_locked(self) -> bool: + def is_frunk_locked(self) -> int: """Return car frunk is locked. Locked: 0 @@ -300,7 +315,9 @@ def is_locked(self) -> bool: @property def is_steering_wheel_heater_on(self) -> bool: """Return steering wheel heater.""" - return self._controller.get_climate_params(vin=self.vin).get("steering_wheel_heater") + return self._controller.get_climate_params(vin=self.vin).get( + "steering_wheel_heater" + ) @property def is_trunk_locked(self) -> int: @@ -317,47 +334,50 @@ def is_trunk_locked(self) -> int: return False @property - def is_on(self) -> dict: + def is_on(self) -> bool: """Return car is on.""" return self._controller.car_online[self.vin] @property - def longitude(self) -> str: + def longitude(self) -> float: """Return longitude.""" return self._controller.get_drive_params(vin=self.vin).get("longitude") @property - def latitude(self) -> str: + def latitude(self) -> float: """Return latitude.""" return self._controller.get_drive_params(vin=self.vin).get("latitude") @property - def max_avail_temp(self) -> dict: + def max_avail_temp(self) -> float: """Return max available temperature.""" return self._controller.get_climate_params(vin=self.vin).get("max_avail_temp") @property - def min_avail_temp(self) -> dict: + def min_avail_temp(self) -> float: """Return min available temperature.""" return self._controller.get_climate_params(vin=self.vin).get("min_avail_temp") @property - def native_heading(self) -> str: + def native_heading(self) -> int: """Return native heading.""" + # Not seeing this in the JSON response return self._controller.get_drive_params(vin=self.vin).get("native_heading") @property - def native_location_supported(self) -> str: + def native_location_supported(self) -> int: """Return native location supported.""" - return self._controller.get_drive_params(vin=self.vin).get("native_location_supported") + return self._controller.get_drive_params(vin=self.vin).get( + "native_location_supported" + ) @property - def native_longitude(self) -> str: + def native_longitude(self) -> float: """Return native longitude.""" return self._controller.get_drive_params(vin=self.vin).get("native_longitude") @property - def native_latitude(self) -> str: + def native_latitude(self) -> float: """Return native latitude.""" return self._controller.get_drive_params(vin=self.vin).get("native_latitude") @@ -379,7 +399,9 @@ def sentry_mode(self) -> bool: @property def sentry_mode_available(self) -> bool: """Return sentry mode available.""" - return self._controller.get_state_params(vin=self.vin).get("sentry_mode_available") + return self._controller.get_state_params(vin=self.vin).get( + "sentry_mode_available" + ) @property def shift_state(self) -> str: @@ -387,17 +409,19 @@ def shift_state(self) -> str: return self._controller.get_drive_params(vin=self.vin).get("shift_state") @property - def speed(self) -> str: + def speed(self) -> float: """Return speed.""" return self._controller.get_drive_params(vin=self.vin).get("speed") @property def software_update(self) -> dict: """Return software update version information.""" - return self._controller.get_state_params(vin=self.vin).get("software_update", {}) + return self._controller.get_state_params(vin=self.vin).get( + "software_update", {} + ) @property - def third_row_seats(self) -> bool: + def third_row_seats(self) -> str: """Return third row seats option.""" return self._controller.get_state_params(vin=self.vin).get("third_row_seats") @@ -410,7 +434,7 @@ def time_to_full_charge(self) -> float: async def _send_command( self, name: str, *, path_vars: dict, wake_if_asleep: bool = False, **kwargs - ): + ) -> dict: """Wrapper for sending commands to the Tesla API.""" _LOGGER.debug("Sending command: %s", name) data = await self._controller.api( @@ -419,7 +443,7 @@ async def _send_command( _LOGGER.debug("Response from command %s: %s", name, data) return data - def _get_lat_long(self): + def _get_lat_long(self) -> float: """Get current latitude and longitude.""" lat = None long = None @@ -443,9 +467,7 @@ async def change_charge_limit(self, value: float) -> None: ) if data and data["response"]["result"] is True: - params = { - "charge_limit_soc": int(value) - } + params = {"charge_limit_soc": int(value)} self._controller.update_charging_params(vin=self.vin, params=params) async def charge_port_door_close(self) -> None: @@ -456,10 +478,8 @@ async def charge_port_door_close(self) -> None: wake_if_asleep=True, ) - if data and data["response"]["result"]: - params = { - "charge_port_door_open": False - } + if data and data["response"]["result"] is True: + params = {"charge_port_door_open": False} self._controller.update_state_params(vin=self.vin, params=params) async def charge_port_door_open(self) -> None: @@ -470,10 +490,8 @@ async def charge_port_door_open(self) -> None: wake_if_asleep=True, ) - if data and data["response"]["result"]: - params = { - "charge_port_door_open": True - } + if data and data["response"]["result"] is True: + params = {"charge_port_door_open": True} self._controller.update_state_params(vin=self.vin, params=params) async def flash_lights(self) -> None: @@ -502,9 +520,7 @@ async def lock(self): wake_if_asleep=True, ) if data and data["response"]["result"] is True: - params = { - "locked": True - } + params = {"locked": True} self._controller.update_state_params(vin=self.vin, params=params) async def remote_seat_heater_request(self, level: int, seat_id: int) -> None: @@ -533,10 +549,8 @@ async def remote_seat_heater_request(self, level: int, seat_id: int) -> None: level=level, wake_if_asleep=True, ) - if data and data["response"]["result"]: - params = { - f"seat_{seat_id}_heater": level - } + if data and data["response"]["result"] is True: + params = {f"seat_heater_{SEAT_NAME_MAP[seat_id]}": level} self._controller.update_climate_params(vin=self.vin, params=params) def get_seat_heater_status(self, seat_id) -> int: @@ -563,9 +577,7 @@ async def set_charging_amps(self, value: float) -> None: ) if data and data["response"]["result"] is True: - params = { - "charge_amps": int(value) - } + params = {"charge_amps": int(value)} self._controller.update_charging_params(vin=self.vin, params=params) async def set_cabin_overheat_protection(self, option: str) -> None: @@ -595,9 +607,7 @@ async def set_cabin_overheat_protection(self, option: str) -> None: wake_if_asleep=True, ) if data and data["response"]["result"]: - params = { - "cabin_overheat_protection": option - } + params = {"cabin_overheat_protection": option} self._controller.update_climate_params(vin=self.vin, params=params) async def set_climate_keeper_mode(self, keeper_id) -> None: @@ -623,10 +633,8 @@ async def set_heated_steering_wheel(self, value: bool) -> None: wake_if_asleep=True, ) - if data and data["response"]["result"]: - params = { - "steering_wheel_heater": value - } + if data and data["response"]["result"] is True: + params = {"steering_wheel_heater": value} self._controller.update_climate_params(vin=self.vin, params=params) async def set_hvac_mode(self, on_off: str) -> None: @@ -667,10 +675,8 @@ async def set_sentry_mode(self, value: bool) -> None: wake_if_asleep=True, ) - if data and data["response"]["result"]: - params = { - "sentry_mode": value - } + if data and data["response"]["result"] is True: + params = {"sentry_mode": value} self._controller.update_state_params(vin=self.vin, params=params) async def set_temperature(self, temp) -> dict: @@ -682,10 +688,8 @@ async def set_temperature(self, temp) -> dict: passenger_temp=temp, wake_if_asleep=True, ) - if data and data["response"]["result"]: - params = { - "driver_temp_setting": temp - } + if data and data["response"]["result"] is True: + params = {"driver_temp_setting": temp} self._controller.update_climate_params(vin=self.vin, params=params) @@ -698,11 +702,9 @@ async def start_charge(self): ) if data and data["response"]["result"] is True: - params = { - "charging_state": "Charging" - } + params = {"charging_state": "Charging"} self._controller.update_charging_params(vin=self.vin, params=params) - + async def stop_charge(self): """Send command to start charge.""" data = await self._send_command( @@ -712,9 +714,7 @@ async def stop_charge(self): ) if data and data["response"]["result"] is True: - params = { - "charging_state": None - } + params = {"charging_state": None} self._controller.update_charging_params(vin=self.vin, params=params) async def wake_up(self) -> None: @@ -733,16 +733,12 @@ async def toggle_trunk(self): which_trunk="rear", wake_if_asleep=True, ) - if data and data["response"]["result"]: + if data and data["response"]["result"] is True: if self.is_trunk_locked: - params = { - "rt": 0 - } + params = {"rt": 0} self._controller.update_state_params(vin=self.vin, params=params) if not self.is_trunk_locked: - params = { - "rt": 255 - } + params = {"rt": 255} self._controller.update_state_params(vin=self.vin, params=params) async def toggle_frunk(self): @@ -753,16 +749,12 @@ async def toggle_frunk(self): which_trunk="front", wake_if_asleep=True, ) - if data and data["response"]["result"]: + if data and data["response"]["result"] is True: if self.is_frunk_locked: - params = { - "ft": 0 - } + params = {"ft": 0} self._controller.update_state_params(vin=self.vin, params=params) if not self.is_frunk_locked: - params = { - "ft": 255 - } + params = {"ft": 255} self._controller.update_state_params(vin=self.vin, params=params) async def trigger_homelink(self): @@ -797,8 +789,6 @@ async def unlock(self): path_vars={"vehicle_id": self.id}, wake_if_asleep=True, ) - if data and data["response"]["result"]: - params = { - "locked": False - } - self._controller.update_state_params(vin=self.vin, params=params) \ No newline at end of file + if data and data["response"]["result"] is True: + params = {"locked": False} + self._controller.update_state_params(vin=self.vin, params=params) diff --git a/teslajsonpy/const.py b/teslajsonpy/const.py index caa3a00c..13a49d6d 100644 --- a/teslajsonpy/const.py +++ b/teslajsonpy/const.py @@ -22,7 +22,7 @@ PRODUCT_TYPE_ENERGY_SITES = "energy_sites" PRODUCT_TYPE_POWERWALLS = "powerwalls" -DEFAULT_ENERGY_SITE_NAME = "My Home" +DEFAULT_ENERGYSITE_NAME = "My Home" RESOURCE_TYPE = "resource_type" RESOURCE_TYPE_SOLAR = "solar" RESOURCE_TYPE_BATTERY = "battery" diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 7c0c1b32..62e80153 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -380,13 +380,13 @@ def __init__( self.__last_parked_timestamp = {} self.__update_state = {} self.enable_websocket = enable_websocket + self.endpoints = {} self.polling_policy = polling_policy + self.__energysite_list = [] + self.__power_data = {} + self.__vehicle_list = [] self.cars = {} - self.cars_raw = {} - self.endpoints = {} self.energysites = {} - self.__energysites = {} - self.__power_data = {} async def connect( self, @@ -394,7 +394,6 @@ async def connect( wake_if_asleep: bool = False, filtered_vins: Optional[List[Text]] = None, mfa_code: Text = "", - skip_add: bool = False, ) -> Dict[Text, Text]: """Connect controller to Tesla. @@ -412,12 +411,14 @@ async def connect( if mfa_code: self.__connection.mfa_code = mfa_code - self.cars_raw = await self.get_vehicles() + product_list = await self.get_product_list() self._last_attempted_update_time = round(time.time()) self.__update_lock = asyncio.Lock() - for car in self.cars_raw: + self.__vehicle_list = [cars for cars in product_list if "vehicle_id" in cars] + + for car in self.__vehicle_list: vin = car["vin"] if filtered_vins and vin not in filtered_vins: _LOGGER.debug("Skipping car with VIN: %s", vin) @@ -444,9 +445,14 @@ async def connect( self._generate_car_objects() - self.__energysites = await self.get_energysites() + self.__energysite_list = [ + p + for p in product_list + if p.get(RESOURCE_TYPE) == RESOURCE_TYPE_SOLAR + or p.get(RESOURCE_TYPE) == RESOURCE_TYPE_BATTERY + ] - for energysite in self.__energysites: + for energysite in self.__energysite_list: energysite_id = energysite["energy_site_id"] if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: @@ -548,22 +554,18 @@ def register_websocket_callback(self, callback) -> int: return len(self.__websocket_listeners) - 1 @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) - async def get_vehicles(self): - """Get vehicles json from TeslaAPI.""" - return (await self.api("VEHICLE_LIST"))["response"] + async def get_product_list(self) -> list: + """Get product list from Tesla.""" + return (await self.api("PRODUCT_LIST"))["response"] @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) - async def get_energysites(self): - """Get energy sites json from TeslaAPI and filter to solar or battery sites.""" - return [ - p - for p in (await self.api("PRODUCT_LIST"))["response"] - if p.get(RESOURCE_TYPE) == RESOURCE_TYPE_SOLAR or p.get(RESOURCE_TYPE) == RESOURCE_TYPE_BATTERY - ] + async def get_vehicles(self) -> list: + """Get vehicles json from TeslaAPI.""" + return (await self.api("VEHICLE_LIST"))["response"] @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) - async def get_site_config(self, energysite_id): - """Get site config json from TeslaAPI.""" + async def get_site_config(self, energysite_id: int) -> dict: + """Get site config json from TeslaAPI for a given energysite_id.""" return (await self.api("SITE_CONFIG", path_vars={"site_id": energysite_id}))[ "response" ] @@ -730,13 +732,13 @@ async def command( def _generate_car_objects(self) -> None: """Generate car objects.""" - for car in self.cars_raw: + for car in self.__vehicle_list: vin = car["vin"] self.cars[vin] = TeslaCar(car, self) def _generate_energysite_objects(self) -> None: """Generate energy site objects.""" - for energysite in self.__energysites: + for energysite in self.__energysite_list: energysite_id = energysite["energy_site_id"] # Solar only systems (no Powerwalls) are listed as "solar" if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: @@ -1052,12 +1054,14 @@ async def _get_and_process_battery_data( except TeslaException: data = None if data and data["response"]: - response = data["response"]["power_reading"][0] - # Add grid_status to the response - # Already in data for non-Powerwall sites - response.update(data["response"]["grid_status"]) + response = data["response"] + + params = response["power_reading"][0] + params["grid_status"] = response.get("grid_status") + params["default_real_mode"] = response.get("default_real_mode") + params["operation"] = response.get("operation") # Use energysite_id since that's how it's retrieved - self.__power_data[energysite_id] = response + self.__power_data[energysite_id].update(params) async def _get_and_process_battery_summary( energysite_id: Text, battery_id: Text @@ -1075,7 +1079,7 @@ async def _get_and_process_battery_summary( except TeslaException: data = None if data and data["response"]: - self.__power_data[energysite_id] = data["response"] + self.__power_data[energysite_id].update(data["response"]) async with self.__update_lock: cur_time = round(time.time()) @@ -1149,7 +1153,7 @@ async def _get_and_process_battery_summary( cur_time - self.get_last_park_time(vin=vin), cur_time - self.get_last_wake_up_time(vin=vin), ) - if self.__energysites and not car_id: + if self.__energysite_list and not car_id: # do not update energy sites if car_id was a parameter. for energysite in self.energysites.values(): energysite_id = energysite.energysite_id diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index c001854a..eb0fc2ab 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -2,7 +2,7 @@ from teslajsonpy.const import ( RESOURCE_TYPE, - DEFAULT_ENERGY_SITE_NAME, + DEFAULT_ENERGYSITE_NAME, ) @@ -12,34 +12,34 @@ class EnergySite: def __init__(self, api, energysite, power_data) -> None: """Initialize EnergySite.""" self._api = api - self._energy_site = energysite + self._energysite = energysite self._power_data = power_data @property def energysite_id(self) -> int: """Return energy site id (aka site_id).""" - return self._energy_site["energy_site_id"] + return self._energysite["energy_site_id"] @property - def has_load_meter(self) -> int: + def has_load_meter(self) -> bool: """Return True if energy site has a load meter.""" - return self._energy_site["components"]["load_meter"] + return self._energysite["components"]["load_meter"] @property def id(self) -> int: """Return id (aka battery_id).""" - return self._energy_site["id"] + return self._energysite["id"] @property - def resource_type(self) -> int: + def resource_type(self) -> str: """Return energy site type.""" - return self._energy_site[RESOURCE_TYPE] + return self._energysite[RESOURCE_TYPE] @property - def site_name(self) -> int: + def site_name(self) -> str: """Return energy site name.""" # "site_name" not a valid key if name never set in Tesla app - return self._energy_site.get("site_name", DEFAULT_ENERGY_SITE_NAME) + return self._energysite.get("site_name", DEFAULT_ENERGYSITE_NAME) class SolarSite(EnergySite): @@ -54,83 +54,82 @@ def __init__(self, api, energysite, power_data) -> None: super().__init__(api, energysite, power_data) @property - def grid_power(self) -> int: + def grid_power(self) -> float: """Return grid power in Watts.""" # Add check to see if site has power metering? return self._power_data["grid_power"] @property - def load_power(self) -> int: + def load_power(self) -> float: """Return load power in Watts.""" # Add check to see if site has power metering? return self._power_data["load_power"] @property - def solar_power(self) -> int: + def solar_power(self) -> float: """Return solar power in Watts.""" return self._power_data["solar_power"] @property - def solar_type(self) -> int: + def solar_type(self) -> str: """Return type of solar (e.g. pv_panels or roof).""" - return self._energy_site["components"]["solar_type"] + return self._energysite["components"]["solar_type"] class PowerwallSite(EnergySite): """Represents a Tesla Energy Powerwall site. This class shouldn't be instantiated directly; it will be instantiated - by :meth:`teslajsonpy.controller.generate_energy_site_objects`. + by :meth:`teslajsonpy.controller.generate_energysite_objects`. """ def __init__(self, api, energysite, power_data) -> None: """Initialize PowerwallSite.""" super().__init__(api, energysite, power_data) - # self.__default_real_mode = None - # self.__backup_reserve_percent = None @property - def battery_percent(self) -> int: + def battery_percent(self) -> float: """Return battery charge level percentage.""" - # Add check to see if site has power metering? return self._power_data["battery_percentage"] @property - def battery_power(self) -> int: + def battery_power(self) -> float: """Return battery power in Watts.""" return self._power_data["battery_power"] @property - def grid_power(self) -> int: + def grid_power(self) -> float: # Grid and load power are the same in SolarSite because of how we store - # the data. It comes from two different endpoints but we stored in self.__power_data + # the data. It comes from two different endpoints but we stored in self._power_data return self._power_data["grid_power"] @property - def load_power(self) -> int: + def load_power(self) -> float: """Return load power in Watts.""" return self._power_data["load_power"] - # async def set_operation_mode(self, real_mode, backup_reserve_percent) -> None: - # """Set operation mode of Powerwall.""" - # # real_mode - self_consumption, backup, autonomous - # # backup_reserve_percent - 1-100 + # async def set_operation_mode(self, real_mode, value) -> None: + # """Set operation mode of Powerwall. + + # Mode: "self_consumption", "backup", "autonomous" + # Value: 0-100 + # """ # data = await self._api( # "BATTERY_OPERATION_MODE", # path_vars={"battery_id": self.id}, # default_real_mode=real_mode, - # backup_reserve_percent=int(backup_reserve_percent), + # backup_reserve_percent=int(value), # ) # if data and data["response"]["result"]: # self.__default_real_mode = real_mode - # self.__backup_reserve_percent = backup_reserve_percent + # self.__backup_reserve_percent = value class SolarPowerwallSite(PowerwallSite, SolarSite): """Represents a Tesla Energy Solar site with Powerwall(s). This class shouldn't be instantiated directly; it will be instantiated - by :meth:`teslajsonpy.controller.generate_energy_site_objects`. + by :meth:`teslajsonpy.controller.generate_energysite_objects`. """ def __init__(self, api, energysite, power_data) -> None: diff --git a/teslajsonpy/homeassistant/power.py b/teslajsonpy/homeassistant/power.py index 13038be4..83bf4646 100644 --- a/teslajsonpy/homeassistant/power.py +++ b/teslajsonpy/homeassistant/power.py @@ -7,7 +7,7 @@ import logging from typing import Dict, Text -from teslajsonpy.const import DEFAULT_ENERGY_SITE_NAME +from teslajsonpy.const import DEFAULT_ENERGYSITE_NAME _LOGGER = logging.getLogger(__name__) @@ -36,7 +36,7 @@ def __init__(self, data, controller): """ self._id: int = data["id"] self._energysite_id: int = data["energy_site_id"] - self._site_name: Text = data.get("site_name", DEFAULT_ENERGY_SITE_NAME) + self._site_name: Text = data.get("site_name", DEFAULT_ENERGYSITE_NAME) self._controller = controller self.should_poll: bool = True self.type: Text = "device" From 8edbc7832a3290ef713f13d9732f43d0db05d6c7 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 21 Aug 2022 16:47:10 -0700 Subject: [PATCH 26/84] Add property and move generate_objects --- teslajsonpy/car.py | 20 +++++++++++++++++++- teslajsonpy/controller.py | 8 ++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index a79128c0..0776038f 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -391,6 +391,17 @@ def outside_temp(self) -> float: """Return outside temperature.""" return self._controller.get_climate_params(vin=self.vin).get("outside_temp") + @property + def rear_heated_seats(self) -> bool: + """Return if car has rear (second row) heated seats.""" + # Assuming if rear left doesn't have it, there's no rear seat heating + if self._controller.get_climate_params(vin=self.vin).get( + "seat_heater_rear_left" + ): + return True + else: + return False + @property def sentry_mode(self) -> bool: """Return sentry mode.""" @@ -420,6 +431,13 @@ def software_update(self) -> dict: "software_update", {} ) + @property + def steering_wheel_heater(self) -> bool: + """Return steering wheel heater option.""" + return self._controller.get_climate_params(vin=self.vin).get( + "steering_wheel_heater" + ) + @property def third_row_seats(self) -> str: """Return third row seats option.""" @@ -555,7 +573,7 @@ async def remote_seat_heater_request(self, level: int, seat_id: int) -> None: def get_seat_heater_status(self, seat_id) -> int: """Return status of seat heater for a given seat.""" - seat_id = f"seat_{seat_id}_heater" + seat_id = f"seat_heater_{SEAT_NAME_MAP[seat_id]}" return self._controller.get_climate_params(vin=self.vin).get(seat_id) async def schedule_software_update(self, offset_sec=0) -> None: diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 62e80153..d0fe8601 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -443,8 +443,6 @@ async def connect( self.__driving[vin] = {} self.__gui[vin] = {} - self._generate_car_objects() - self.__energysite_list = [ p for p in product_list @@ -469,13 +467,15 @@ async def connect( self.__lock[energysite_id] = asyncio.Lock() - self._generate_energysite_objects() - if not test_login: try: await self.update(wake_if_asleep=wake_if_asleep) except (TeslaException, RetryLimitError): pass + + self._generate_car_objects() + self._generate_energysite_objects() + return { "refresh_token": self.__connection.refresh_token, "access_token": self.__connection.access_token, From 8ee1d99621a54f817b57201a95ce7ede2239883d Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 21 Aug 2022 19:29:04 -0700 Subject: [PATCH 27/84] Typing and doc string clean up --- teslajsonpy/car.py | 78 +++++++++++++++++---------------------- teslajsonpy/controller.py | 14 +++---- teslajsonpy/energy.py | 2 +- 3 files changed, 42 insertions(+), 52 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 0776038f..e7c8a7fe 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -1,5 +1,6 @@ """Tesla car.""" import logging +from typing import Optional from teslajsonpy.exceptions import HomelinkError @@ -186,8 +187,10 @@ def charger_voltage(self) -> int: def climate_keeper_mode(self) -> str: """Return climate keeper mode mode. - Returns string "dog", "camp" or "on", "off" - API call not supported on all Tesla models. + Returns + str: dog, camp, on, off + + Not supported on all Tesla models. """ return self._controller.get_climate_params(vin=self.vin).get( "climate_keeper_mode", "" @@ -204,8 +207,8 @@ def conn_charge_cable(self) -> str: def defrost_mode(self) -> int: """Return defrost mode. - On: 2 - Off: 0 + Returns + int: 2 (on), 0 (off) """ return self._controller.get_climate_params(vin=self.vin).get("defrost_mode", 0) @@ -295,10 +298,10 @@ def is_climate_on(self) -> bool: @property def is_frunk_locked(self) -> int: - """Return car frunk is locked. + """Return car frunk is locked (closed). - Locked: 0 - Unlocked: 255 + Returns + int: 0 (locked), 255 (unlocked) """ response = self._controller.get_state_params(vin=self.vin).get("ft") @@ -321,10 +324,10 @@ def is_steering_wheel_heater_on(self) -> bool: @property def is_trunk_locked(self) -> int: - """Return car trunk is locked. + """Return car trunk is locked (closed). - Locked: 0 - Unlocked: 255 + Returns + int: 0 (locked), 255 (unlocked) """ response = self._controller.get_state_params(vin=self.vin).get("rt") @@ -544,20 +547,10 @@ async def lock(self): async def remote_seat_heater_request(self, level: int, seat_id: int) -> None: """Send command to change seat heat. - Levels: - -Off: 0 - -Low: 1 - -Medium: 2 - -High: 3 - - Seat ID: - -Left: 0 - -Right": 1 - -Rear_left": 2 - -Rear_center": 4 - -Rear_right": 5 - -Third_row_left": 6 - -Third_row_right": 7 + Args + levels: 0 (off), 1 (low), 2 (medium), 3 (high) + seat_id: 0 (front left), 1 (front right), 2 (rear left), 4 (rear center) + 5 (rear right), 6 (third row left), 7 (third row right) """ data = await self._send_command( @@ -571,12 +564,12 @@ async def remote_seat_heater_request(self, level: int, seat_id: int) -> None: params = {f"seat_heater_{SEAT_NAME_MAP[seat_id]}": level} self._controller.update_climate_params(vin=self.vin, params=params) - def get_seat_heater_status(self, seat_id) -> int: + def get_seat_heater_status(self, seat_id: int) -> int: """Return status of seat heater for a given seat.""" seat_id = f"seat_heater_{SEAT_NAME_MAP[seat_id]}" return self._controller.get_climate_params(vin=self.vin).get(seat_id) - async def schedule_software_update(self, offset_sec=0) -> None: + async def schedule_software_update(self, offset_sec: Optional[int] = 0) -> None: """Send command to install software update.""" await self._coordinator.controller.api( "SCHEDULE_SOFTWARE_UPDATE", @@ -601,10 +594,8 @@ async def set_charging_amps(self, value: float) -> None: async def set_cabin_overheat_protection(self, option: str) -> None: """Send command to set cabin overheat protection. - Options: - -"Off" - -"No A/C" - -"On" + Args + option: "Off", "No A/C", "On" """ if option == "Off": @@ -628,12 +619,11 @@ async def set_cabin_overheat_protection(self, option: str) -> None: params = {"cabin_overheat_protection": option} self._controller.update_climate_params(vin=self.vin, params=params) - async def set_climate_keeper_mode(self, keeper_id) -> None: + async def set_climate_keeper_mode(self, keeper_id: int) -> None: """Send command to set climate keeper mode. - Keep On: 1 - Dog Mode: 2 - Camp Mode: 3 + Args + keeper_id: 1 (keep on), 2 (dog mode), 3 (camp mode) """ await self._send_command( "SET_CLIMATE_KEEPER_MODE", @@ -671,11 +661,11 @@ async def set_hvac_mode(self, on_off: str) -> None: wake_if_asleep=True, ) - async def set_max_defrost(self, state: bool) -> None: + async def set_max_defrost(self, state: int) -> None: """Send command to set max defrost. - On: 2 - Off: 0 + Args + state: 2 = on, 0 = off """ await self._send_command( "MAX_DEFROST", @@ -697,7 +687,7 @@ async def set_sentry_mode(self, value: bool) -> None: params = {"sentry_mode": value} self._controller.update_state_params(vin=self.vin, params=params) - async def set_temperature(self, temp) -> dict: + async def set_temperature(self, temp: float) -> None: """Send command to set temperature.""" data = await self._send_command( "CHANGE_CLIMATE_TEMPERATURE_SETTING", @@ -711,7 +701,7 @@ async def set_temperature(self, temp) -> dict: self._controller.update_climate_params(vin=self.vin, params=params) - async def start_charge(self): + async def start_charge(self) -> None: """Send command to start charge.""" data = await self._send_command( "START_CHARGE", @@ -723,7 +713,7 @@ async def start_charge(self): params = {"charging_state": "Charging"} self._controller.update_charging_params(vin=self.vin, params=params) - async def stop_charge(self): + async def stop_charge(self) -> None: """Send command to start charge.""" data = await self._send_command( "STOP_CHARGE", @@ -743,7 +733,7 @@ async def wake_up(self) -> None: wake_if_asleep=True, ) - async def toggle_trunk(self): + async def toggle_trunk(self) -> None: """Actuate rear trunk lock.""" data = await self._send_command( "ACTUATE_TRUNK", @@ -759,7 +749,7 @@ async def toggle_trunk(self): params = {"rt": 255} self._controller.update_state_params(vin=self.vin, params=params) - async def toggle_frunk(self): + async def toggle_frunk(self) -> None: """Actuate front trunk lock.""" data = await self._send_command( "ACTUATE_TRUNK", @@ -775,7 +765,7 @@ async def toggle_frunk(self): params = {"ft": 255} self._controller.update_state_params(vin=self.vin, params=params) - async def trigger_homelink(self): + async def trigger_homelink(self) -> None: """Send command to trigger homelink.""" if self.homelink_device_count is None: raise HomelinkError(f"No homelink devices added to {self.display_name}.") @@ -800,7 +790,7 @@ async def trigger_homelink(self): if result is False: raise HomelinkError(f"Error calling trigger_homelink: {reason}") - async def unlock(self): + async def unlock(self) -> None: """Send unlock command.""" data = await self._send_command( "UNLOCK", diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index d0fe8601..0d952a3f 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -13,7 +13,7 @@ import json import pkgutil import time -from typing import Callable, Dict, List, Optional, Text +from typing import Any, Callable, Dict, List, Optional, Text import backoff import httpx @@ -76,7 +76,7 @@ ) from teslajsonpy.car import TeslaCar -from teslajsonpy.energy import SolarSite, PowerwallSite, SolarPowerwallSite +from teslajsonpy.energy import EnergySite, SolarSite, PowerwallSite, SolarPowerwallSite _LOGGER = logging.getLogger(__name__) @@ -382,11 +382,11 @@ def __init__( self.enable_websocket = enable_websocket self.endpoints = {} self.polling_policy = polling_policy - self.__energysite_list = [] - self.__power_data = {} - self.__vehicle_list = [] - self.cars = {} - self.energysites = {} + self.__energysite_list: List[dict] = [] + self.__power_data: Dict[str, Any] = {} + self.__vehicle_list: List[dict] = [] + self.cars: Dict[str, TeslaCar] = {} + self.energysites: Dict[int, EnergySite] = {} async def connect( self, diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index eb0fc2ab..9408458e 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -108,7 +108,7 @@ def load_power(self) -> float: """Return load power in Watts.""" return self._power_data["load_power"] - # async def set_operation_mode(self, real_mode, value) -> None: + # async def set_operation_mode(self, real_mode: str, value: int) -> None: # """Set operation mode of Powerwall. # Mode: "self_consumption", "backup", "autonomous" From a6f836b20a3e398f1b4c9fe20b2031bc4c1f8844 Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 25 Aug 2022 18:51:35 -0700 Subject: [PATCH 28/84] Solution for grid_status issue --- teslajsonpy/controller.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 0d952a3f..c312e035 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -383,7 +383,8 @@ def __init__( self.endpoints = {} self.polling_policy = polling_policy self.__energysite_list: List[dict] = [] - self.__power_data: Dict[str, Any] = {} + self.__grid_status: Dict[int, dict] = {} + self.__power_data: Dict[int, dict] = {} self.__vehicle_list: List[dict] = [] self.cars: Dict[str, TeslaCar] = {} self.energysites: Dict[int, EnergySite] = {} @@ -464,6 +465,8 @@ async def connect( "grid_power": 0, "battery_power": 0, } + # Default to True but check in first update + self.__grid_status[energysite_id] = {"grid_always_unk": True} self.__lock[energysite_id] = asyncio.Lock() @@ -1030,7 +1033,10 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: # At the same time, newer systems maye report spurious reads of 0 Watts # and grid status unknown. In this case, remove values but update # self.__power_data with remaining data (grid and load power). - if ( + if response["grid_status"] == "Active": + self.__grid_status[energysite_id]["grid_always_unk"] = False + + if not self.__grid_status[energysite_id]["grid_always_unk"] and ( response["grid_status"] == "Unknown" and response["solar_power"] == 0 ): @@ -1741,9 +1747,10 @@ def update_interval(self) -> int: @update_interval.setter def update_interval(self, value: int) -> None: """Set update_interval.""" - if value < 0: + # Sometimes receive a value of None + if value and value < 0: value = UPDATE_INTERVAL - if value: + if value and value: _LOGGER.debug("Update interval set to %s.", value) self._update_interval = int(value) From a96c2ab81538fc285e8a4e8ad2f87a1f6a6f667e Mon Sep 17 00:00:00 2001 From: shred86 Date: Sat, 27 Aug 2022 16:21:59 -0700 Subject: [PATCH 29/84] Remove HA code and initial test update --- teslajsonpy/__init__.py | 38 -- teslajsonpy/controller.py | 113 +---- teslajsonpy/homeassistant/__init__.py | 6 - teslajsonpy/homeassistant/alerts.py | 99 ---- teslajsonpy/homeassistant/battery_sensor.py | 150 ------ teslajsonpy/homeassistant/binary_sensor.py | 272 ----------- teslajsonpy/homeassistant/charger.py | 440 ------------------ teslajsonpy/homeassistant/climate.py | 259 ----------- teslajsonpy/homeassistant/gps.py | 161 ------- teslajsonpy/homeassistant/heated_seats.py | 101 ---- .../homeassistant/heated_steering_wheel.py | 87 ---- teslajsonpy/homeassistant/homelink.py | 98 ---- teslajsonpy/homeassistant/lock.py | 181 ------- teslajsonpy/homeassistant/power.py | 287 ------------ teslajsonpy/homeassistant/sentry_mode.py | 104 ----- teslajsonpy/homeassistant/trunk.py | 159 ------- teslajsonpy/homeassistant/vehicle.py | 163 ------- teslajsonpy/homeassistant/vehicle_data.py | 317 ------------- tests/tesla_mock.py | 251 +++++----- tests/unit_tests/homeassistant/__init__.py | 7 - tests/unit_tests/homeassistant/test_alerts.py | 46 -- .../homeassistant/test_battery_sensor.py | 146 ------ .../test_calculate_update_interval.py | 352 -------------- .../test_charger_connection_sensor.py | 96 ---- .../homeassistant/test_charger_lock.py | 145 ------ .../homeassistant/test_charger_switch.py | 150 ------ .../homeassistant/test_charging_sensor.py | 219 --------- .../unit_tests/homeassistant/test_climate.py | 284 ----------- .../homeassistant/test_frunk_lock.py | 151 ------ .../homeassistant/test_gps_tracker.py | 155 ------ .../homeassistant/test_heated_seat.py | 111 ----- .../test_heated_steering_wheel.py | 112 ----- .../homeassistant/test_helper_functions.py | 182 -------- .../unit_tests/homeassistant/test_homelink.py | 192 -------- tests/unit_tests/homeassistant/test_lock.py | 145 ------ .../homeassistant/test_odometer_sensor.py | 131 ------ .../homeassistant/test_online_sensor.py | 108 ----- .../homeassistant/test_parking_sensor.py | 95 ---- .../homeassistant/test_power_sensor.py | 148 ------ .../homeassistant/test_range_sensor.py | 192 -------- .../homeassistant/test_range_switch.py | 155 ------ .../homeassistant/test_sentry_mode_switch.py | 320 ------------- .../homeassistant/test_temp_sensor.py | 70 --- .../homeassistant/test_trunk_lock.py | 151 ------ .../homeassistant/test_vehicle_data.py | 284 ----------- .../homeassistant/test_vehicle_device.py | 181 ------- tests/unit_tests/test_energy.py | 204 ++++++++ 47 files changed, 346 insertions(+), 7472 deletions(-) delete mode 100644 teslajsonpy/homeassistant/__init__.py delete mode 100644 teslajsonpy/homeassistant/alerts.py delete mode 100644 teslajsonpy/homeassistant/battery_sensor.py delete mode 100644 teslajsonpy/homeassistant/binary_sensor.py delete mode 100644 teslajsonpy/homeassistant/charger.py delete mode 100644 teslajsonpy/homeassistant/climate.py delete mode 100644 teslajsonpy/homeassistant/gps.py delete mode 100644 teslajsonpy/homeassistant/heated_seats.py delete mode 100644 teslajsonpy/homeassistant/heated_steering_wheel.py delete mode 100644 teslajsonpy/homeassistant/homelink.py delete mode 100644 teslajsonpy/homeassistant/lock.py delete mode 100644 teslajsonpy/homeassistant/power.py delete mode 100644 teslajsonpy/homeassistant/sentry_mode.py delete mode 100644 teslajsonpy/homeassistant/trunk.py delete mode 100644 teslajsonpy/homeassistant/vehicle.py delete mode 100644 teslajsonpy/homeassistant/vehicle_data.py delete mode 100644 tests/unit_tests/homeassistant/__init__.py delete mode 100644 tests/unit_tests/homeassistant/test_alerts.py delete mode 100644 tests/unit_tests/homeassistant/test_battery_sensor.py delete mode 100644 tests/unit_tests/homeassistant/test_calculate_update_interval.py delete mode 100644 tests/unit_tests/homeassistant/test_charger_connection_sensor.py delete mode 100644 tests/unit_tests/homeassistant/test_charger_lock.py delete mode 100644 tests/unit_tests/homeassistant/test_charger_switch.py delete mode 100644 tests/unit_tests/homeassistant/test_charging_sensor.py delete mode 100644 tests/unit_tests/homeassistant/test_climate.py delete mode 100644 tests/unit_tests/homeassistant/test_frunk_lock.py delete mode 100644 tests/unit_tests/homeassistant/test_gps_tracker.py delete mode 100644 tests/unit_tests/homeassistant/test_heated_seat.py delete mode 100644 tests/unit_tests/homeassistant/test_heated_steering_wheel.py delete mode 100644 tests/unit_tests/homeassistant/test_helper_functions.py delete mode 100644 tests/unit_tests/homeassistant/test_homelink.py delete mode 100644 tests/unit_tests/homeassistant/test_lock.py delete mode 100644 tests/unit_tests/homeassistant/test_odometer_sensor.py delete mode 100644 tests/unit_tests/homeassistant/test_online_sensor.py delete mode 100644 tests/unit_tests/homeassistant/test_parking_sensor.py delete mode 100644 tests/unit_tests/homeassistant/test_power_sensor.py delete mode 100644 tests/unit_tests/homeassistant/test_range_sensor.py delete mode 100644 tests/unit_tests/homeassistant/test_range_switch.py delete mode 100644 tests/unit_tests/homeassistant/test_sentry_mode_switch.py delete mode 100644 tests/unit_tests/homeassistant/test_temp_sensor.py delete mode 100644 tests/unit_tests/homeassistant/test_trunk_lock.py delete mode 100644 tests/unit_tests/homeassistant/test_vehicle_data.py delete mode 100644 tests/unit_tests/homeassistant/test_vehicle_device.py create mode 100644 tests/unit_tests/test_energy.py diff --git a/teslajsonpy/__init__.py b/teslajsonpy/__init__.py index c7c9c58e..ff240e1b 100644 --- a/teslajsonpy/__init__.py +++ b/teslajsonpy/__init__.py @@ -12,23 +12,7 @@ IncompleteCredentials, TeslaException, UnknownPresetMode, - HomelinkError, ) -from teslajsonpy.homeassistant.battery_sensor import Battery, Range -from teslajsonpy.homeassistant.binary_sensor import ( - ChargerConnectionSensor, - OnlineSensor, - ParkingSensor, - UpdateSensor, -) -from teslajsonpy.homeassistant.charger import ChargerSwitch, ChargingSensor, RangeSwitch -from teslajsonpy.homeassistant.climate import Climate, TempSensor -from teslajsonpy.homeassistant.gps import GPS, Odometer -from teslajsonpy.homeassistant.lock import Lock -from teslajsonpy.homeassistant.sentry_mode import SentryModeSwitch -from teslajsonpy.homeassistant.trunk import FrunkLock, TrunkLock -from teslajsonpy.homeassistant.alerts import Horn, FlashLights -from teslajsonpy.homeassistant.homelink import TriggerHomelink from teslajsonpy.teslaproxy import TeslaProxy from .__version__ import __version__ @@ -36,30 +20,8 @@ "Connection", "Controller", "TeslaProxy", - "Battery", - "Range", - "ChargerConnectionSensor", - "ChargingSensor", - "OnlineSensor", - "ParkingSensor", - "UpdateSensor", - "ChargerSwitch", - "RangeSwitch", - "Climate", - "TempSensor", - "Controller", "TeslaException", "UnknownPresetMode", - "HomelinkError", - "GPS", - "Odometer", - "Lock", - "SentryModeSwitch", - "Horn", - "FlashLights", - "TriggerHomelink", - "TrunkLock", - "FrunkLock", "__version__", "RetryLimitError", "IncompleteCredentials", diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index c312e035..1524db8e 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -20,6 +20,7 @@ import wrapt from yarl import URL +from teslajsonpy.car import TeslaCar from teslajsonpy.connection import Connection from teslajsonpy.const import ( AUTH_DOMAIN, @@ -34,49 +35,8 @@ RESOURCE_TYPE_SOLAR, RESOURCE_TYPE_BATTERY, ) -from teslajsonpy.exceptions import should_giveup, RetryLimitError, TeslaException -from teslajsonpy.homeassistant.battery_sensor import Battery, Range -from teslajsonpy.homeassistant.binary_sensor import ( - ChargerConnectionSensor, - OnlineSensor, - ParkingSensor, - UpdateSensor, -) -from teslajsonpy.homeassistant.charger import ( - ChargerSwitch, - ChargingEnergySensor, - ChargingSensor, - RangeSwitch, -) -from teslajsonpy.homeassistant.climate import Climate, TempSensor -from teslajsonpy.homeassistant.gps import GPS, Odometer -from teslajsonpy.homeassistant.heated_seats import HeatedSeatSelect -from teslajsonpy.homeassistant.lock import ChargerLock, Lock -from teslajsonpy.homeassistant.sentry_mode import SentryModeSwitch -from teslajsonpy.homeassistant.trunk import FrunkLock, TrunkLock -from teslajsonpy.homeassistant.heated_steering_wheel import HeatedSteeringWheelSwitch -from teslajsonpy.homeassistant.power import ( - SolarPowerSensor, - GridPowerSensor, - LoadPowerSensor, - BatteryPowerSensor, -) -from teslajsonpy.homeassistant.alerts import Horn, FlashLights -from teslajsonpy.homeassistant.homelink import TriggerHomelink -from teslajsonpy.homeassistant.vehicle_data import ( - ChargeStateDataSensor, - ClimateStateDataSensor, - DriveStateDataSensor, - GuiSettingsDataSensor, - SoftwareDataSensor, - SpeedLimitDataSensor, - VehicleConfigDataSensor, - VehicleDataSensor, - VehicleStateDataSensor, -) - -from teslajsonpy.car import TeslaCar from teslajsonpy.energy import EnergySite, SolarSite, PowerwallSite, SolarPowerwallSite +from teslajsonpy.exceptions import should_giveup, RetryLimitError, TeslaException _LOGGER = logging.getLogger(__name__) @@ -353,7 +313,6 @@ def __init__( expiration=expiration, auth_domain=auth_domain, ) - self.__components = [] self._update_interval: int = update_interval self._update_interval_vin = {} self.__update = {} @@ -765,68 +724,6 @@ def _generate_energysite_objects(self) -> None: self.api, energysite, self.__power_data[energysite_id] ) - def get_homeassistant_components(self): - """Return list of Tesla components for Home Assistant setup. - - Use get_vehicles() for general API use. - """ - return self.__components - - def _add_energysite_components(self, energysite): - self.__components.append(SolarPowerSensor(energysite, self)) - self.__components.append(LoadPowerSensor(energysite, self)) - self.__components.append(GridPowerSensor(energysite, self)) - if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_BATTERY: - self.__components.append(BatteryPowerSensor(energysite, self)) - - def _add_car_components(self, car): - self.__components.append(Climate(car, self)) - self.__components.append(Battery(car, self)) - self.__components.append(Range(car, self)) - self.__components.append(TempSensor(car, self)) - self.__components.append(Lock(car, self)) - self.__components.append(ChargerLock(car, self)) - self.__components.append(ChargerConnectionSensor(car, self)) - self.__components.append(ChargingSensor(car, self)) - self.__components.append(ChargingEnergySensor(car, self)) - self.__components.append(ChargerSwitch(car, self)) - self.__components.append(RangeSwitch(car, self)) - self.__components.append(ParkingSensor(car, self)) - self.__components.append(GPS(car, self)) - self.__components.append(Odometer(car, self)) - self.__components.append(OnlineSensor(car, self)) - self.__components.append(SentryModeSwitch(car, self)) - self.__components.append(TrunkLock(car, self)) - self.__components.append(FrunkLock(car, self)) - self.__components.append(UpdateSensor(car, self)) - self.__components.append(HeatedSteeringWheelSwitch(car, self)) - self.__components.append(Horn(car, self)) - self.__components.append(FlashLights(car, self)) - self.__components.append(TriggerHomelink(car, self)) - self.__components.append(ChargeStateDataSensor(car, self)) - self.__components.append(ClimateStateDataSensor(car, self)) - self.__components.append(DriveStateDataSensor(car, self)) - self.__components.append(GuiSettingsDataSensor(car, self)) - self.__components.append(SoftwareDataSensor(car, self)) - self.__components.append(SpeedLimitDataSensor(car, self)) - self.__components.append(VehicleConfigDataSensor(car, self)) - self.__components.append(VehicleDataSensor(car, self)) - self.__components.append(VehicleStateDataSensor(car, self)) - - for seat in [ - "left", - "right", - "rear_left", - "rear_center", - "rear_right", - "third_row_left", - "third_row_right", - ]: - try: - self.__components.append(HeatedSeatSelect(car, self, seat)) - except KeyError: - _LOGGER.debug("Seat warmer %s not detected", seat) - async def _wake_up(self, car_id): car_vin = self._id_to_vin(car_id) car_id = self._update_id(car_id) @@ -1033,12 +930,12 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: # At the same time, newer systems maye report spurious reads of 0 Watts # and grid status unknown. In this case, remove values but update # self.__power_data with remaining data (grid and load power). - if response["grid_status"] == "Active": + if response.get("grid_status") == "Active": self.__grid_status[energysite_id]["grid_always_unk"] = False if not self.__grid_status[energysite_id]["grid_always_unk"] and ( - response["grid_status"] == "Unknown" - and response["solar_power"] == 0 + response.get("grid_status") == "Unknown" + and response.get("solar_power") == 0 ): _LOGGER.debug("Possible spurious energy site power read") del response["grid_status"] diff --git a/teslajsonpy/homeassistant/__init__.py b/teslajsonpy/homeassistant/__init__.py deleted file mode 100644 index ce17bb83..00000000 --- a/teslajsonpy/homeassistant/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -""" -Python Package for controlling Tesla API. - -For more details about this api, please refer to the documentation at -https://github.com/zabuldon/teslajsonpy -""" diff --git a/teslajsonpy/homeassistant/alerts.py b/teslajsonpy/homeassistant/alerts.py deleted file mode 100644 index afe8b254..00000000 --- a/teslajsonpy/homeassistant/alerts.py +++ /dev/null @@ -1,99 +0,0 @@ -# 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.homeassistant.vehicle import VehicleDevice - - -class Horn(VehicleDevice): - """Home-Assistant class for horn of Tesla vehicles.""" - - def __init__(self, data, controller): - """Initialize the horn for the vehicle. - - Parameters - ---------- - data : dict - The horn for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/commands/alerts - controller : teslajsonpy.Controller - The controller that controls updates to the Tesla API. - - Returns - ------- - None - - """ - super().__init__(data, controller) - self.type = "horn" - self.hass_type = "button" - self.name = self._name() - self.uniq_name = self._uniq_name() - - async def async_update(self, wake_if_asleep=False, force=False): - """Update the horn of the vehicle.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - @staticmethod - def has_battery() -> bool: - """Return whether the device has a battery.""" - return False - - async def honk_horn(self) -> None: - """Horn.""" - await self._controller.api( - "HONK_HORN", - path_vars={"vehicle_id": self._id}, - on=True, - wake_if_asleep=True, - ) - - -class FlashLights(VehicleDevice): - """Home-Assistant class for flash lights of Tesla vehicles.""" - - def __init__(self, data, controller): - """Initialize the flash lights for the vehicle. - - Parameters - ---------- - data : dict - The flash lights for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/commands/alerts - controller : teslajsonpy.Controller - The controller that controls updates to the Tesla API. - - Returns - ------- - None - - """ - super().__init__(data, controller) - self.type = "flash lights" - self.hass_type = "button" - self.name = self._name() - self.uniq_name = self._uniq_name() - - async def async_update(self, wake_if_asleep=False, force=False): - """Update the flash lights of the vehicle.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - @staticmethod - def has_battery() -> bool: - """Return whether the device has a battery.""" - return False - - async def flash_lights(self) -> None: - """Flash Lights.""" - await self._controller.api( - "FLASH_LIGHTS", - path_vars={"vehicle_id": self._id}, - on=True, - wake_if_asleep=True, - ) diff --git a/teslajsonpy/homeassistant/battery_sensor.py b/teslajsonpy/homeassistant/battery_sensor.py deleted file mode 100644 index 0b6be00a..00000000 --- a/teslajsonpy/homeassistant/battery_sensor.py +++ /dev/null @@ -1,150 +0,0 @@ -# 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 typing import Dict, Optional, Text - -from teslajsonpy.homeassistant.vehicle import VehicleDevice - - -class Battery(VehicleDevice): - """Home-Assistant battery class for a Tesla VehicleDevice.""" - - def __init__(self, data: Dict, controller) -> None: - """Initialize the Battery sensor. - - Args: - data (Dict): The charging parameters for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/chargestate - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - self.__battery_level: int = None - self.__charging_state: bool = None - self.__charge_port_door_open: bool = None - self.type: Text = "battery sensor" - self.measurement: Text = "%" - self.hass_type: Text = "sensor" - self._device_class: Text = "battery" - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - self.bin_type: hex = 0x5 - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the battery state.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - data = self._controller.get_charging_params(self._id) - if data: - self.__battery_level = data["battery_level"] - self.__charging_state = data["charging_state"] == "Charging" - - @staticmethod - def has_battery() -> bool: - """Return whether the device has a battery.""" - return True - - def get_value(self) -> int: - """Return the battery level.""" - return self.__battery_level - - def battery_level(self) -> int: - """Return the battery level.""" - return self.get_value() - - def battery_charging(self) -> bool: - """Return the battery level.""" - return self.__charging_state - - @property - def device_class(self) -> Text: - """Return the HA device class.""" - return self._device_class - - -class Range(VehicleDevice): - """Home-Assistant class of the battery range for a Tesla VehicleDevice.""" - - def __init__(self, data: Dict, controller) -> None: - """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 = None - self.__est_battery_range = None - self.__ideal_battery_range = None - self.type = "range sensor" - self.__rated = True - self.measurement = "LENGTH_MILES" - self.hass_type = "sensor" - self._device_class: Optional[Text] = None - self.name = self._name() - self.uniq_name = self._uniq_name() - self.bin_type = 0xA - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the battery range state.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - data = self._controller.get_charging_params(self._id) - if data: - self.__battery_range = data["battery_range"] - self.__est_battery_range = data["est_battery_range"] - self.__ideal_battery_range = data["ideal_battery_range"] - data = self._controller.get_gui_params(self._id) - if data: - if data["gui_distance_units"] == "mi/hr": - self.measurement = "LENGTH_MILES" - else: - self.measurement = "LENGTH_KILOMETERS" - self.__rated = data["gui_range_display"] == "Rated" - - @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 - return self.__ideal_battery_range - - @property - def device_class(self) -> Text: - """Return the HA device class.""" - return self._device_class diff --git a/teslajsonpy/homeassistant/binary_sensor.py b/teslajsonpy/homeassistant/binary_sensor.py deleted file mode 100644 index 2a74d6a6..00000000 --- a/teslajsonpy/homeassistant/binary_sensor.py +++ /dev/null @@ -1,272 +0,0 @@ -# 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 typing import Dict, Optional, Text - -from teslajsonpy.const import RELEASE_NOTES_URL -from teslajsonpy.homeassistant.vehicle import VehicleDevice - - -class BinarySensor(VehicleDevice): - """Home-assistant binary sensor class for Tesla vehicles. - - This is intended to be partially inherited by a Home-Assitant entity. - """ - - def __init__(self, data: Dict, 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: Optional[bool] = None - - self.type: Text = "binary sensor" - self.hass_type: Text = "binary_sensor" - # this will be returned to HA as a device_class - # https://developers.home-assistant.io/docs/core/entity/binary-sensor - self._sensor_type: Optional[Text] = None - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the binary sensor.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - - def get_value(self) -> Optional[bool]: - """Return whether binary sensor is true.""" - return self.__state - - @property - def sensor_type(self) -> Optional[Text]: - """Return the sensor_type for use by HA as a device_class.""" - return self._sensor_type - - @staticmethod - def has_battery() -> bool: - """Return whether the device has a battery.""" - return False - - -class ParkingSensor(BinarySensor): - """Home-assistant parking brake class for Tesla vehicles. - - This is intended to be partially inherited by a Home-Assitant entity. - """ - - def __init__(self, data: Dict, 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: Optional[bool] = None - self.type: Text = "parking brake sensor" - self.hass_type: Text = "binary_sensor" - self._sensor_type: Optional[Text] = None - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the parking brake sensor.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - data = self._controller.get_drive_params(self._id) - if data: - self.attrs["shift_state"] = ( - data["shift_state"] if data["shift_state"] else "P" - ) - if not data["shift_state"] or data["shift_state"] == "P": - self.__state = True - else: - self.__state = False - - def get_value(self) -> Optional[bool]: - """Return whether parking brake engaged.""" - return self.__state - - -class ChargerConnectionSensor(BinarySensor): - """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: Optional[bool] = None - self.type: Text = "charger sensor" - self.hass_type: Text = "binary_sensor" - self._sensor_type: Optional[Text] = None - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the charger connection sensor.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - data = self._controller.get_charging_params(self._id) - if data: - self.attrs["charging_state"] = data["charging_state"] - self.attrs["conn_charge_cable"] = data["conn_charge_cable"] - self.attrs["fast_charger_present"] = data["fast_charger_present"] - self.attrs["fast_charger_brand"] = data["fast_charger_brand"] - self.attrs["fast_charger_type"] = data["fast_charger_type"] - if data["charging_state"] in ["Disconnected"]: - self.__state = False - else: - self.__state = True - - def get_value(self) -> Optional[bool]: - """Return whether the charger cable is connected.""" - return self.__state - - -class OnlineSensor(BinarySensor): - """Home-Assistant Online sensor class for a Tesla VehicleDevice.""" - - def __init__(self, data: Dict, controller) -> None: - """Initialize the Online sensor. - - Args: - data (Dict): The base state for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/data - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - self.__online_state: Optional[bool] = None - self.type: Text = "online sensor" - self.hass_type: Text = "binary_sensor" - self._sensor_type: Optional[Text] = "connectivity" - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the battery state.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - self.__online_state = self._controller.car_online[self._vin] - self.attrs["state"] = self._controller.car_state[self._vin].get("state") - self.attrs["vehicle_id"] = self.vehicle_id() - self.attrs["vin"] = self.vin() - self.attrs["id"] = self.id() - self.attrs["update_interval"] = self._controller.get_update_interval_vin( - vin=self._vin - ) - - def get_value(self) -> Optional[bool]: - """Return the car is online.""" - return self.__online_state - - -class UpdateSensor(BinarySensor): - """Home-Assistant update sensor class for a Tesla VehicleDevice.""" - - def __init__(self, data: Dict, controller) -> None: - """Initialize the Update sensor. - - Args: - data (Dict): Thes base state for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/data - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - self.type: Text = "update available sensor" - self._sensor_type: Optional[Text] = None - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the battery state.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - self.attrs = ( - self.device_state_attributes.copy() if self.device_state_attributes else {} - ) - - def get_value(self) -> Optional[bool]: - """Return the car is online.""" - return self.update_available - - @property - def device_state_attributes(self) -> Optional[dict]: - """Return the optional state attributes.""" - if not self.car_version: - return None - data = {} - data["installed_version"] = self.car_version - if self.update_available: - data["release_notes"] = f"{RELEASE_NOTES_URL}{self.update_version}" - data["update_version"] = self.update_version - return data diff --git a/teslajsonpy/homeassistant/charger.py b/teslajsonpy/homeassistant/charger.py deleted file mode 100644 index 79fbadf1..00000000 --- a/teslajsonpy/homeassistant/charger.py +++ /dev/null @@ -1,440 +0,0 @@ -# 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 typing import Dict, Optional, Text -import datetime - -from teslajsonpy.homeassistant.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 = None - self.type = "charger switch" - self.hass_type = "switch" - self.name = self._name() - self.uniq_name = self._uniq_name() - self.bin_type = 0x8 - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the charging state of the Tesla Vehicle.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - last_update = self._controller.get_last_update_time(self._id) - if last_update >= self.__manual_update_time: - data = self._controller.get_charging_params(self._id) - if data and data["charging_state"] != "Charging": - self.__charger_state = False - else: - self.__charger_state = True - - async def start_charge(self): - """Start charging the Tesla Vehicle.""" - if not self.__charger_state: - data = await self._controller.api( - "START_CHARGE", path_vars={"vehicle_id": self._id}, wake_if_asleep=True - ) - if data and data["response"]["result"]: - self.__charger_state = True - self.__manual_update_time = time.time() - - async def stop_charge(self): - """Stop charging the Tesla Vehicle.""" - if self.__charger_state: - data = await self._controller.api( - "STOP_CHARGE", path_vars={"vehicle_id": self._id}, wake_if_asleep=True - ) - if data and data["response"]["result"]: - self.__charger_state = False - 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 = None - self.type = "maxrange switch" - self.hass_type = "switch" - self.name = self._name() - self.uniq_name = self._uniq_name() - self.bin_type = 0x9 - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the status of the range setting.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - last_update = self._controller.get_last_update_time(self._id) - if last_update >= self.__manual_update_time: - data = self._controller.get_charging_params(self._id) - if data: - self.__maxrange_state = data["charge_to_max_range"] - - async def set_max(self): - """Set the charger to max range for trips.""" - if not self.__maxrange_state: - data = await self._controller.api( - "CHANGE_CHARGE_MAX", - path_vars={"vehicle_id": self._id}, - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__maxrange_state = True - self.__manual_update_time = time.time() - - async def set_standard(self): - """Set the charger to standard range for daily commute.""" - if self.__maxrange_state: - data = await self._controller.api( - "CHANGE_CHARGE_STANDARD", - path_vars={"vehicle_id": self._id}, - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__maxrange_state = False - 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 - - -class ChargingSensor(VehicleDevice): - """Home-Assistant charging sensor class for a Tesla VehicleDevice.""" - - def __init__(self, data: Dict, controller) -> None: - """Initialize the Charger sensor. - - Args: - data (Dict): The charging parameters for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/chargestate - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - self.type: Text = "charging rate sensor" - self.__rated: bool = True - self.measurement: Text = "mi/hr" - self.hass_type: Text = "sensor" - self._device_class: Optional[Text] = None - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - self.bin_type: hex = 0xC - self.__added_range = None - self.__charge_energy_added = None - self.__charging_rate = None - self.__time_to_full = None - self.__charge_current_request = None - self.__charge_current_request_max = None - self.__charger_actual_current = None - self.__charger_voltage = None - self.__charge_limit_soc = None - self.__charger_power = None - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the battery state.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - data = self._controller.get_gui_params(self._id) - if data: - self.measurement = data["gui_distance_units"] - self.__rated = data["gui_range_display"] == "Rated" - data = self._controller.get_charging_params(self._id) - if data: - self.attrs["charger_phases"] = data["charger_phases"] - self.__added_range = ( - data["charge_miles_added_rated"] - if self.__rated - else data["charge_miles_added_ideal"] - ) - self.__charge_energy_added = data["charge_energy_added"] - self.__charging_rate = data["charge_rate"] - self.__time_to_full = data["time_to_full_charge"] - self.__charge_current_request = data["charge_current_request"] - self.__charge_current_request_max = data["charge_current_request_max"] - self.__charger_actual_current = data["charger_actual_current"] - self.__charger_voltage = data["charger_voltage"] - self.__charge_limit_soc = data["charge_limit_soc"] - self.__charger_power = data["charger_power"] - self.attrs["charge_limit_soc"] = self.charge_limit_soc - if self.measurement != "mi/hr": - self.__added_range = round(self.__added_range / 0.621371, 2) - self.__charging_rate = round(self.__charging_rate / 0.621371, 2) - - @staticmethod - def has_battery() -> bool: - """Return whether the device has a battery.""" - return False - - @property - def charging_rate(self) -> float: - """Return the charging rate.""" - return self.__charging_rate - - @property - def time_left(self) -> float: - """Return the time left to full in hours.""" - return self.__time_to_full - - @property - def added_range(self) -> float: - """Return the added range.""" - return self.__added_range - - @property - def charge_current_request(self) -> float: - """Return the requested current.""" - return self.__charge_current_request - - @property - def charge_current_request_max(self) -> float: - """Return the requested current max.""" - return self.__charge_current_request_max - - @property - def charger_actual_current(self) -> float: - """Return the actual current.""" - return self.__charger_actual_current - - @property - def charger_voltage(self) -> float: - """Return the voltage.""" - return self.__charger_voltage - - @property - def charge_energy_added(self) -> float: - """Return the energy added.""" - return self.__charge_energy_added - - @property - def charge_limit_soc(self) -> int: - """Return the state of charge limit.""" - return self.__charge_limit_soc - - @property - def charger_power(self) -> float: - """Return the state of charger power.""" - return self.__charger_power - - @property - def device_class(self) -> Text: - """Return the HA device class.""" - return self._device_class - - @property - def state_class(self) -> Text: - """Return the state class.""" - return "measurement" - - -class ChargingEnergySensor(VehicleDevice): - """Home-Assistant energy sensor class for a Tesla VehicleDevice.""" - - def __init__(self, data: Dict, controller) -> None: - """Initialize the Charger sensor. - - Args: - data (Dict): The charging parameters for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/chargestate - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - self.type: Text = "energy added sensor" - self.__rated: bool = True - self.__miles: bool = True - self.measurement: Text = "kWh" - self.hass_type: Text = "sensor" - self._device_class: Optional[Text] = "energy" - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - self.__added_range = None - self.__charge_energy_added = None - self.__charging_rate = None - self.__time_to_full = None - self.__charge_current_request = None - self.__charge_current_request_max = None - self.__charger_actual_current = None - self.__charger_voltage = None - self.__charge_limit_soc = None - self.__charger_power = None - self.__last_reset: Optional[datetime.datetime] = None - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the battery state.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - data = self._controller.get_gui_params(self._id) - if data: - self.__miles = data["gui_distance_units"] == "mi/hr" - self.__rated = data["gui_range_display"] == "Rated" - data = self._controller.get_charging_params(self._id) - if data: - self.attrs["charger_phases"] = data["charger_phases"] - self.__added_range = ( - data["charge_miles_added_rated"] - if self.__rated - else data["charge_miles_added_ideal"] - ) - if ( - self.__charge_energy_added - and self.__charge_energy_added > data["charge_energy_added"] - ): - self.__last_reset = datetime.datetime.utcnow() - self.__charge_energy_added = data["charge_energy_added"] - self.__charging_rate = data["charge_rate"] - self.__time_to_full = data["time_to_full_charge"] - self.__charge_current_request = data["charge_current_request"] - self.__charge_current_request_max = data["charge_current_request_max"] - self.__charger_actual_current = data["charger_actual_current"] - self.__charger_voltage = data["charger_voltage"] - self.__charge_limit_soc = data["charge_limit_soc"] - self.__charger_power = data["charger_power"] - self.attrs["charge_limit_soc"] = self.charge_limit_soc - self.attrs["last_reset"] = self.last_reset - if self.__miles: - self.__added_range = round(self.__added_range / 0.621371, 2) - self.__charging_rate = round(self.__charging_rate / 0.621371, 2) - - @staticmethod - def has_battery() -> bool: - """Return whether the device has a battery.""" - return False - - @property - def charging_rate(self) -> float: - """Return the charging rate.""" - return self.__charging_rate - - @property - def time_left(self) -> float: - """Return the time left to full in hours.""" - return self.__time_to_full - - @property - def added_range(self) -> float: - """Return the added range.""" - return self.__added_range - - @property - def charge_current_request(self) -> float: - """Return the requested current.""" - return self.__charge_current_request - - @property - def charge_current_request_max(self) -> float: - """Return the requested current max.""" - return self.__charge_current_request_max - - @property - def charger_actual_current(self) -> float: - """Return the actual current.""" - return self.__charger_actual_current - - @property - def charger_voltage(self) -> float: - """Return the voltage.""" - return self.__charger_voltage - - @property - def charge_energy_added(self) -> float: - """Return the energy added.""" - return self.__charge_energy_added - - @property - def charge_limit_soc(self) -> int: - """Return the state of charge limit.""" - return self.__charge_limit_soc - - @property - def charger_power(self) -> float: - """Return the state of charger power.""" - return self.__charger_power - - @property - def device_class(self) -> Text: - """Return the HA device class.""" - return self._device_class - - @property - def last_reset(self) -> Optional[datetime.datetime]: - """Return the last reset time.""" - return self.__last_reset - - @property - def state_class(self) -> Text: - """Return the state class.""" - return "total_increasing" - - def get_value(self) -> float: - """Return charge energy added.""" - return self.charge_energy_added diff --git a/teslajsonpy/homeassistant/climate.py b/teslajsonpy/homeassistant/climate.py deleted file mode 100644 index b4e75452..00000000 --- a/teslajsonpy/homeassistant/climate.py +++ /dev/null @@ -1,259 +0,0 @@ -# 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 typing import List, Optional, Text - -from teslajsonpy.exceptions import UnknownPresetMode -from teslajsonpy.homeassistant.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 = None - self.__inside_temp = None - self.__outside_temp = None - self.__driver_temp_setting = None - self.__passenger_temp_setting = None - self.__is_climate_on = None - self.__fan_status = None - self.__preset_mode: Text = None - self.__manual_update_time = 0 - - self.type = "HVAC (climate) system" - self.hass_type = "climate" - self.measurement = "C" - - self.name = self._name() - - self.uniq_name = self._uniq_name() - self.bin_type = 0x3 - - 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 - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the HVAC state.""" - await super().async_update(wake_if_asleep=wake_if_asleep, force=force) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - data = self._controller.get_climate_params(self._id) - if data: - last_update = self._controller.get_last_update_time(self._id) - if last_update >= self.__manual_update_time: - 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.__fan_status = data["fan_status"] - if data.get("defrost_mode") is not None: - self.__preset_mode = ( - "defrost" if data.get("defrost_mode") == 2 else "normal" - ) - - async 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 = await self._controller.api( - "CHANGE_CLIMATE_TEMPERATURE_SETTING", - path_vars={"vehicle_id": self._id}, - driver_temp=temp, - passenger_temp=temp, - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__driver_temp_setting = temp - self.__passenger_temp_setting = temp - - async def set_status(self, enabled): - """Enable or disable the HVAC.""" - self.__manual_update_time = time.time() - if enabled: - data = await self._controller.api( - "CLIMATE_ON", path_vars={"vehicle_id": self._id}, wake_if_asleep=True - ) - if data and data["response"]["result"]: - self.__is_auto_conditioning_on = True - self.__is_climate_on = True - else: - data = await self._controller.api( - "CLIMATE_OFF", path_vars={"vehicle_id": self._id}, wake_if_asleep=True - ) - if data and data["response"]["result"]: - self.__is_auto_conditioning_on = False - self.__is_climate_on = False - await self.async_update() - - async def set_preset_mode(self, preset_mode: str) -> None: - """Set new preset mode.""" - if preset_mode not in self.preset_modes: - raise UnknownPresetMode( - f"Preset mode '{preset_mode}' is not valid. Use {self.preset_modes}" - ) - self.__manual_update_time = time.time() - data = await self._controller.api( - "MAX_DEFROST", - path_vars={"vehicle_id": self._id}, - on=preset_mode == "defrost", - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - await self.async_update(force=True) - self.__preset_mode = preset_mode - - @property - def preset_mode(self) -> Optional[str]: - """Return the current preset mode, e.g., home, away, temp. - - Requires SUPPORT_PRESET_MODE. - """ - return self.__preset_mode - - @property - def preset_modes(self) -> Optional[List[str]]: - """Return a list of available preset modes. - - Requires SUPPORT_PRESET_MODE. - """ - return ["normal", "defrost"] - - @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 = None - self.__outside_temp = None - - self.type = "temperature sensor" - self.measurement = "C" - self.hass_type = "sensor" - self._device_class: Text = "temperature" - self.name = self._name() - self.uniq_name = self._uniq_name() - self.bin_type = 0x4 - - def get_inside_temp(self): - """Get inside temperature.""" - return self.__inside_temp - - def get_outside_temp(self): - """Get outside temperature.""" - return self.__outside_temp - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the temperature.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - 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 - ) - - @staticmethod - def has_battery(): - """Return whether the device has a battery.""" - return False - - @property - def device_class(self) -> Text: - """Return the HA device class.""" - return self._device_class diff --git a/teslajsonpy/homeassistant/gps.py b/teslajsonpy/homeassistant/gps.py deleted file mode 100644 index e69b71f1..00000000 --- a/teslajsonpy/homeassistant/gps.py +++ /dev/null @@ -1,161 +0,0 @@ -# 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 typing import Optional, Text - -from teslajsonpy.homeassistant.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 = None - self.__latitude = None - self.__heading = None - self.__speed = None - self.__location = {} - - self.last_seen = 0 - self.last_updated = 0 - self.type = "location tracker" - self.hass_type = "devices_tracker" - self.bin_type = 0x6 - - self.name = self._name() - - self.uniq_name = self._uniq_name() - - def get_location(self): - """Return the current location.""" - if ( - self.__longitude is not None - and self.__latitude is not None - and self.__heading is not None - ): - self.__location = { - "longitude": self.__longitude, - "latitude": self.__latitude, - "heading": self.__heading, - "speed": self.__speed, - } - return self.__location - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the current GPS location.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - data = self._controller.get_drive_params(self._id) - if data: - if data["native_location_supported"]: - self.__longitude = data["native_longitude"] - self.__latitude = data["native_latitude"] - self.__heading = ( - data["native_heading"] - if data.get("native_heading") - else data["heading"] - ) - else: - self.__longitude = data["longitude"] - self.__latitude = data["latitude"] - self.__heading = data["heading"] - self.__speed = data["speed"] if data["speed"] else 0 - - @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 = None - self.type = "mileage sensor" - self.measurement = "LENGTH_MILES" - self.hass_type = "sensor" - self._device_class: Optional[Text] = None - self.name = self._name() - self.uniq_name = self._uniq_name() - self.bin_type = 0xB - self.__rated = True - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the odometer and the unit of measurement based on GUI.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - data = self._controller.get_state_params(self._id) - if data: - self.__odometer = data["odometer"] - data = self._controller.get_gui_params(self._id) - if data: - if data["gui_distance_units"] == "mi/hr": - self.measurement = "LENGTH_MILES" - else: - self.measurement = "LENGTH_KILOMETERS" - self.__rated = data["gui_range_display"] == "Rated" - - @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) if self.__odometer else None - - @property - def device_class(self) -> Text: - """Return the HA device class.""" - return self._device_class diff --git a/teslajsonpy/homeassistant/heated_seats.py b/teslajsonpy/homeassistant/heated_seats.py deleted file mode 100644 index 4f60dbb2..00000000 --- a/teslajsonpy/homeassistant/heated_seats.py +++ /dev/null @@ -1,101 +0,0 @@ -# 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.homeassistant.vehicle import VehicleDevice - -seat_id_map = { - "left": 0, - "right": 1, - "rear_left": 2, - "rear_center": 4, - "rear_right": 5, - "third_row_left": 6, - "third_row_right": 7, -} - - -class HeatedSeatSelect(VehicleDevice): - """Home-assistant heated seat class for Tesla vehicles. - - This is intended to be partially inherited by a Home-Assitant entity. - """ - - def __init__(self, data, controller, seat_name): - """Initialize a heated seat 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. - seat_name : string - The name of the seat to control. - One of "left", "right", "rear_left", "rear_center", "rear_right", "third_row_left", "third_row_right" - Returns - ------- - None - - """ - super().__init__(data, controller) - self.__manual_update_time = 0 - self.__seat_heat_level = None - self.__seat_name = seat_name - - self.type = f"heated seat {seat_name}" - self.hass_type = "select" - - self.name = self._name() - - self.uniq_name = self._uniq_name() - # Disable by default, integration will enable those supported by vehicle - self.enabled_by_default = False - - self.bin_type = 0x7 - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the seat state.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - last_update = self._controller.get_last_update_time(self._id) - if last_update >= self.__manual_update_time: - data = self._controller.get_climate_params(self._id) - self.__seat_heat_level = ( - data.get(f"seat_heater_{self.__seat_name}") if data else None - ) - - async def set_seat_heat_level(self, level): - """Set heated seat level.""" - data = await self._controller.api( - "REMOTE_SEAT_HEATER_REQUEST", - path_vars={"vehicle_id": self._id}, - heater=seat_id_map[self.__seat_name], - level=level, - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__seat_heat_level = level - self.__manual_update_time = time.time() - - def get_seat_heat_level(self): - """Return current heated seat level.""" - return self.__seat_heat_level - - @staticmethod - def has_battery(): - """Return whether the device has a battery.""" - return False diff --git a/teslajsonpy/homeassistant/heated_steering_wheel.py b/teslajsonpy/homeassistant/heated_steering_wheel.py deleted file mode 100644 index d536ae9e..00000000 --- a/teslajsonpy/homeassistant/heated_steering_wheel.py +++ /dev/null @@ -1,87 +0,0 @@ -# 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.homeassistant.vehicle import VehicleDevice - - -class HeatedSteeringWheelSwitch(VehicleDevice): - """Home-assistant heated steering wheel class for Tesla vehicles. - - This is intended to be partially inherited by a Home-Assitant entity. - """ - - def __init__(self, data, controller): - """Initialize a heated steering wheel 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.__steering_wheel_heated = None - - self.type = "heated steering switch" - self.hass_type = "switch" - - self.name = self._name() - - self.uniq_name = self._uniq_name() - # Disable by default, integration will enable if supported by vehicle - self.enabled_by_default = False - - self.bin_type = 0x7 - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the steering wheel state.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - last_update = self._controller.get_last_update_time(self._id) - if last_update >= self.__manual_update_time: - data = self._controller.get_climate_params(self._id) - self.__steering_wheel_heated = ( - data.get("steering_wheel_heater") if data else None - ) - - async def set_steering_wheel_heat(self, value: bool): - """Set heated steering wheel.""" - data = await self._controller.api( - "REMOTE_STEERING_WHEEL_HEATER_REQUEST", - path_vars={"vehicle_id": self._id}, - on=value, - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__steering_wheel_heated = value - self.__manual_update_time = time.time() - - def get_steering_wheel_heat(self): - """Return current heated setting.""" - return self.__steering_wheel_heated - - @staticmethod - def has_battery(): - """Return whether the device has a battery.""" - return False diff --git a/teslajsonpy/homeassistant/homelink.py b/teslajsonpy/homeassistant/homelink.py deleted file mode 100644 index 3f213812..00000000 --- a/teslajsonpy/homeassistant/homelink.py +++ /dev/null @@ -1,98 +0,0 @@ -# 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.exceptions import HomelinkError -from teslajsonpy.homeassistant.vehicle import VehicleDevice - - -class TriggerHomelink(VehicleDevice): - """Home-Assistant class for trigger homelink of Tesla vehicles.""" - - def __init__(self, data, controller): - """Initialize the trigger homelink for the vehicle. - - Parameters - ---------- - data : dict - The trigger homelink for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/commands/homelink - controller : teslajsonpy.Controller - The controller that controls updates to the Tesla API. - - Returns - ------- - None - - """ - super().__init__(data, controller) - self.type = "trigger homelink" - self.hass_type = "button" - self.name = self._name() - self.uniq_name = self._uniq_name() - self.enabled_by_default = False - - self._longitude = None - self._latitude = None - self._homelink_device_count = None - self._homelink_nearby = None - self._homelink_available = False - - @staticmethod - def has_battery() -> bool: - """Return whether the device has a battery.""" - return False - - def available(self) -> bool: - """Return whether homelink is available.""" - return self._homelink_available - - async def async_update(self, wake_if_asleep=False, force=False): - """Update the trigger homelink of the vehicle.""" - await super().async_update(wake_if_asleep=wake_if_asleep, force=force) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - data = self._controller.get_drive_params(self._id) - if data: - if data.get("native_location_supported"): - self._longitude = data.get("native_longitude") - self._latitude = data.get("native_latitude") - else: - self._longitude = data.get("longitude") - self._latitude = data.get("latitude") - data = self._controller.get_state_params(self._id) - if data: - self._homelink_device_count = data.get("homelink_device_count") - self._homelink_nearby = data.get("homelink_nearby") - self._homelink_available = bool(self._homelink_device_count) - - async def trigger_homelink(self) -> None: - """Trigger Homelink.""" - await self.async_update(wake_if_asleep=True, force=True) - if self._latitude is not None and self._longitude is not None: - if not self._homelink_device_count: - raise HomelinkError(f"No homelink devices added to {self.car_name()}.") - if not self._homelink_nearby: - raise HomelinkError(f"No homelink devices near {self.car_name()}.") - data = await self._controller.api( - "TRIGGER_HOMELINK", - path_vars={"vehicle_id": self._id}, - lat=self._latitude, - lon=self._longitude, - wake_if_asleep=True, - ) - if data and data.get("response"): - result = data["response"].get("result") - reason = data["response"].get("reason") - if result is False: - raise HomelinkError(f"Error calling trigger_homelink: {reason}") diff --git a/teslajsonpy/homeassistant/lock.py b/teslajsonpy/homeassistant/lock.py deleted file mode 100644 index 721b27bb..00000000 --- a/teslajsonpy/homeassistant/lock.py +++ /dev/null @@ -1,181 +0,0 @@ -# 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.homeassistant.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 = None - - self.type = "door lock" - self.hass_type = "lock" - - self.name = self._name() - - self.uniq_name = self._uniq_name() - self.bin_type = 0x7 - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the lock state.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - last_update = self._controller.get_last_update_time(self._id) - if last_update >= self.__manual_update_time: - data = self._controller.get_state_params(self._id) - self.__lock_state = data["locked"] if data else None - - async def lock(self): - """Lock the doors.""" - data = await self._controller.api( - "LOCK", - path_vars={"vehicle_id": self._id}, - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__lock_state = True - self.__manual_update_time = time.time() - - async def unlock(self): - """Unlock the doors and extend handles where applicable.""" - data = await self._controller.api( - "UNLOCK", - path_vars={"vehicle_id": self._id}, - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__lock_state = False - 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 = None - - self.type = "charger door lock" - self.hass_type = "lock" - - self.name = self._name() - - self.uniq_name = self._uniq_name() - self.bin_type = 0x7 - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update state of the charger lock.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - last_update = self._controller.get_last_update_time(self._id) - if last_update >= self.__manual_update_time: - data = self._controller.get_charging_params(self._id) - self.__lock_state = ( - not ( - (data["charge_port_door_open"]) - and (data["charge_port_latch"] != "Engaged") - ) - if data - else None - ) - - async def lock(self): - """Close the charger door.""" - data = await self._controller.api( - "CHARGE_PORT_DOOR_CLOSE", - path_vars={"vehicle_id": self._id}, - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__lock_state = True - self.__manual_update_time = time.time() - - async def unlock(self): - """Open the charger door.""" - data = await self._controller.api( - "CHARGE_PORT_DOOR_OPEN", - path_vars={"vehicle_id": self._id}, - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__lock_state = False - 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/homeassistant/power.py b/teslajsonpy/homeassistant/power.py deleted file mode 100644 index 83bf4646..00000000 --- a/teslajsonpy/homeassistant/power.py +++ /dev/null @@ -1,287 +0,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 -from typing import Dict, Text - -from teslajsonpy.const import DEFAULT_ENERGYSITE_NAME - -_LOGGER = logging.getLogger(__name__) - - -class EnergySiteDevice: - """Home-assistant class of Tesla Energy Sites. - - This is intended to be partially inherited by a Home-Assitant entity. - """ - - def __init__(self, data, controller): - """Initialize the Energy Site. - - Parameters - ---------- - data : dict - The base state for a Tesla Energy Site. - https://www.teslaapi.io/energy-sites/state-and-settings - controller : teslajsonpy.Controller - The controller that controls updates to the Tesla API. - - Returns - ------- - None - - """ - self._id: int = data["id"] - self._energysite_id: int = data["energy_site_id"] - self._site_name: Text = data.get("site_name", DEFAULT_ENERGYSITE_NAME) - self._controller = controller - self.should_poll: bool = True - self.type: Text = "device" - self.attrs: Dict[Text, Text] = {} - self.enabled_by_default: bool = True - - def _name(self) -> Text: - return f"{self._site_name} {self.type}" - - def _uniq_name(self) -> Text: - return f"{self._energysite_id} {self.type}" - - def id(self) -> int: - # pylint: disable=invalid-name - """Return the id.""" - return self._id - - def energysite_id(self) -> int: - """Return the energysite_id.""" - return self._energysite_id - - def site_name(self) -> Text: - """Return the site name.""" - return self._site_name - - async def async_update( - self, wake_if_asleep: bool = False, force: bool = False - ) -> None: - """Update the energy site data. - - This function will call a controller update. - """ - await self._controller.update( - self.id(), wake_if_asleep=wake_if_asleep, force=force - ) - self.refresh() - - # pylint: disable=no-self-use - def refresh(self) -> None: - """Refresh the energy site data. - - This assumes the controller has already been updated. This should be - called by inherited classes so the overall vehicle information is updated. - """ - return - - @property - def power_data(self): - """Return the coordinator controller power data.""" - return self._controller.get_power_params(self._energysite_id) - - -class PowerSensor(EnergySiteDevice): - """Home-assistant class of power sensors for Tesla Energy Sites (Solar Panels). - - This is intended to be partially inherited by a Home-Assitant entity. - """ - - def __init__(self, data, controller): - """Initialize the power sensor and track in Watt for an Energy Site. - - 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.measurement = "W" - self.hass_type = "sensor" - self._device_class: Text = "power" - self._state_class: Text = "measurement" - self.bin_type = 0x4 - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the site info.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - @property - def device_class(self) -> Text: - """Return the HA device class.""" - return self._device_class - - @property - def state_class(self) -> Text: - """Return the HA state class.""" - return self._state_class - - -class SolarPowerSensor(PowerSensor): - """Home-assistant class of power sensors for Tesla Energy Sites (Solar Panels). - - This is intended to be partially inherited by a Home-Assitant entity. - """ - - def __init__(self, data, controller): - """Initialize the solar panel sensor.""" - super().__init__(data, controller) - self._solar_type: Text = data["components"]["solar_type"] - self.__solar_power: float = self.power_data["solar_power"] - self.__generating_status: bool = None - self.type = "solar panel" - self.name = self._name() - self.uniq_name = self._uniq_name() - - def get_value(self) -> float: - """Return solar power.""" - return self.__solar_power - - def get_power(self): - """Get solar power.""" - return self.__solar_power - - def get_generating_status(self): - """Get generating status.""" - return self.__generating_status - - @property - def solar_type(self) -> Text: - """Return the solar type.""" - return self._solar_type - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - data = self._controller.get_power_params(self.energysite_id) - - if data: - # Note: Some systems that pre-date Tesla aquisition of SolarCity will have `grid_status: Unknown`, - # but will have solar power values. At the same time, newer systems will report spurious reads of 0 Watts - # and grid status unknown. If solar power is 0 return null. - if ( - "grid_status" in data - and data["grid_status"] == "Unknown" - and data["solar_power"] == 0 - ): - _LOGGER.debug("Spurious energy site power read") - return - - self.__solar_power = data["solar_power"] - if data["solar_power"] is not None: - self.__generating_status = ( - "Generating" if data["solar_power"] > 0 else "Idle" - ) - - -class LoadPowerSensor(PowerSensor): - """Home-assistant class for load power sensors for Tesla Energy Sites (Solar Panels). - - This is intended to be partially inherited by a Home-Assitant entity. - """ - - def __init__(self, data, controller): - """Initialize the load power sensor.""" - super().__init__(data, controller) - self.__load_power: float = self.power_data["load_power"] - self.type = "load power" - self.name = self._name() - self.uniq_name = self._uniq_name() - - def get_value(self) -> float: - """Return load power.""" - return self.__load_power - - def get_power(self): - """Get load power (home consumption).""" - return self.__load_power - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - self.__load_power = self.power_data["load_power"] - - -class GridPowerSensor(PowerSensor): - """Home-assistant class for grid power sensors for Tesla Energy Sites (Solar Panels). - - This is intended to be partially inherited by a Home-Assitant entity. - """ - - def __init__(self, data, controller): - """Initialize the grid power sensor.""" - super().__init__(data, controller) - self.__grid_power: float = self.power_data["grid_power"] - self.type = "grid power" - self.name = self._name() - self.uniq_name = self._uniq_name() - - def get_value(self) -> float: - """Return grid power.""" - return self.__grid_power - - def get_power(self): - """Get grid power (grid import/export).""" - return self.__grid_power - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - self.__grid_power = self.power_data["grid_power"] - - -class BatteryPowerSensor(PowerSensor): - """Home-assistant class for battery power sensors for Tesla Energy Sites (Solar Panels). - - This is intended to be partially inherited by a Home-Assitant entity. - """ - - def __init__(self, data, controller): - """Initialize the battery power sensor.""" - super().__init__(data, controller) - self.__battery_power: float = self.power_data["battery_power"] - self.type = "battery power" - self.name = self._name() - self.uniq_name = self._uniq_name() - - def get_value(self) -> float: - """Return battery power.""" - return self.__battery_power - - def get_power(self): - """Get battery power (battery charge/discharge).""" - return self.__battery_power - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - self.__battery_power = self.power_data["battery_power"] diff --git a/teslajsonpy/homeassistant/sentry_mode.py b/teslajsonpy/homeassistant/sentry_mode.py deleted file mode 100644 index e92e254f..00000000 --- a/teslajsonpy/homeassistant/sentry_mode.py +++ /dev/null @@ -1,104 +0,0 @@ -# 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 typing import Optional - -from teslajsonpy.homeassistant.vehicle import VehicleDevice - - -class SentryModeSwitch(VehicleDevice): - """Home-Assistant class for sentry mode of Tesla vehicles.""" - - def __init__(self, data, controller): - """Initialize the sentry mode for the vehicle. - - Parameters - ---------- - data : dict - The sentry mode for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/commands/sentrymode - controller : teslajsonpy.Controller - The controller that controls updates to the Tesla API. - - Returns - ------- - None - - """ - super().__init__(data, controller) - self.__manual_update_time = 0 - self.type = "sentry mode switch" - self.hass_type = "switch" - self.name = self._name() - self.uniq_name = self._uniq_name() - self.__sentry_mode = ( - self.sentry_mode_available - and "vehicle_state" in data - and "sentry_mode" in data["vehicle_state"] - and data["vehicle_state"]["sentry_mode"] - ) - - async def async_update(self, wake_if_asleep=False, force=False): - """Update the sentry mode of the vehicle.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - last_update = self._controller.get_last_update_time(self._id) - if last_update >= self.__manual_update_time: - data = self._controller.get_state_params(self._id) - if self.sentry_mode_available and "sentry_mode" in data: - self.__sentry_mode = data["sentry_mode"] - else: - self.__sentry_mode = False - - def available(self) -> bool: - """Return whether the sentry mode is available.""" - return self.sentry_mode_available - - def is_on(self) -> Optional[bool]: - """Return whether the sentry mode is enabled, or None if sentry mode is not available.""" - if not self.sentry_mode_available: - return None - return self.sentry_mode_available and self.__sentry_mode - - @staticmethod - def has_battery() -> bool: - """Return whether the device has a battery.""" - return False - - async def enable_sentry_mode(self) -> None: - """Enable the sentry mode.""" - if self.sentry_mode_available and not self.__sentry_mode: - data = await self._controller.api( - "SET_SENTRY_MODE", - path_vars={"vehicle_id": self._id}, - on=True, - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__sentry_mode = True - self.__manual_update_time = time.time() - - async def disable_sentry_mode(self) -> None: - """Disable the sentry mode.""" - if self.sentry_mode_available and self.__sentry_mode: - data = await self._controller.api( - "SET_SENTRY_MODE", - path_vars={"vehicle_id": self._id}, - on=False, - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__sentry_mode = False - self.__manual_update_time = time.time() diff --git a/teslajsonpy/homeassistant/trunk.py b/teslajsonpy/homeassistant/trunk.py deleted file mode 100644 index 1794ff13..00000000 --- a/teslajsonpy/homeassistant/trunk.py +++ /dev/null @@ -1,159 +0,0 @@ -# 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 typing import Text - -from teslajsonpy.homeassistant.vehicle import VehicleDevice - - -class TrunkLock(VehicleDevice): - """Home-Assistant rear trunk lock for a Tesla VehicleDevice.""" - - def __init__(self, data, controller): - """Initialize the rear trunk lock. - - Args: - data (Dict): The vehicle state for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/vehiclestate - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - self.__lock_state: int = None - self.type: Text = "trunk lock" - self.hass_type: Text = "lock" - self.sensor_type: Text = "door" - self.bin_type = 0x7 - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - self.__manual_update_time = 0 - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the rear trunk state.""" - await super().async_update(wake_if_asleep=wake_if_asleep, force=force) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - last_update = self._controller.get_last_update_time(self._id) - if last_update >= self.__manual_update_time: - data = self._controller.get_state_params(self._id) - self.__lock_state = data["rt"] if (data and "rt" in data) else None - - def is_locked(self): - """Return whether the rear trunk is closed.""" - return self.__lock_state == 0 - - async def unlock(self): - """Open the rear trunk.""" - if self.is_locked(): - data = await self._controller.api( - "ACTUATE_TRUNK", - path_vars={"vehicle_id": self._id}, - which_trunk="rear", - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__lock_state = 255 - self.__manual_update_time = time.time() - - async def lock(self): - """Close the rear trunk.""" - if not self.is_locked(): - data = await self._controller.api( - "ACTUATE_TRUNK", - path_vars={"vehicle_id": self._id}, - which_trunk="rear", - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__lock_state = 0 - self.__manual_update_time = time.time() - - @staticmethod - def has_battery(): - """Return whether the device has a battery.""" - return False - - -class FrunkLock(VehicleDevice): - """Home-Assistant front trunk (frunk) lock for a Tesla VehicleDevice.""" - - def __init__(self, data, controller): - """Initialize the front trunk (frunk) lock. - - Args: - data (Dict): The vehicle state for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/vehiclestate - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - self.__lock_state: int = None - self.type: Text = "frunk lock" - self.hass_type: Text = "lock" - self.sensor_type: Text = "door" - self.bin_type = 0x7 - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - self.__manual_update_time = 0 - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the front trunk (frunk) state.""" - await super().async_update(wake_if_asleep=wake_if_asleep, force=force) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - last_update = self._controller.get_last_update_time(self._id) - if last_update >= self.__manual_update_time: - data = self._controller.get_state_params(self._id) - self.__lock_state = data["ft"] if (data and "ft" in data) else None - - def is_locked(self): - """Return whether the front trunk (frunk) is closed.""" - return self.__lock_state == 0 - - async def unlock(self): - """Open the front trunk (frunk).""" - if self.is_locked(): - data = await self._controller.api( - "ACTUATE_TRUNK", - path_vars={"vehicle_id": self._id}, - which_trunk="front", - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__lock_state = 255 - self.__manual_update_time = time.time() - - async def lock(self): - """Close the front trunk (frunk).""" - if not self.is_locked(): - data = await self._controller.api( - "ACTUATE_TRUNK", - path_vars={"vehicle_id": self._id}, - which_trunk="front", - wake_if_asleep=True, - ) - if data and data["response"]["result"]: - self.__lock_state = 0 - self.__manual_update_time = time.time() - - @staticmethod - def has_battery(): - """Return whether the device has a battery.""" - return False diff --git a/teslajsonpy/homeassistant/vehicle.py b/teslajsonpy/homeassistant/vehicle.py deleted file mode 100644 index 5a3d32f2..00000000 --- a/teslajsonpy/homeassistant/vehicle.py +++ /dev/null @@ -1,163 +0,0 @@ -# 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 -from typing import Dict, Optional, Text - -_LOGGER = logging.getLogger(__name__) - - -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: int = data["id"] - self._vehicle_id: int = data["vehicle_id"] - self._display_name: Text = data["display_name"] - self._vin: Text = data["vin"] - self._state = data["state"] - self._car_type: Text = f"Model {str(self._vin[3]).upper()}" - self._car_version: Text = "" - self._sentry_mode_available: bool = ( - "vehicle_state" in data - and "sentry_mode_available" in data["vehicle_state"] - and data["vehicle_state"]["sentry_mode_available"] - ) - self._controller = controller - self.should_poll: bool = True - self.type: Text = "device" - self.attrs: Dict[Text, Text] = {} - self._update_available: bool = ( - data.get("software_update", {}).get("status") == "available" - ) - self._update_version: Optional[Text] = data.get("software_update", {}).get( - "version" - ) - self.enabled_by_default: bool = True - - def _name(self) -> Text: - return ( - f"{self._display_name} {self.type}" - if self._display_name is not None and self._display_name != self._vin[-6:] - else f"Tesla Model {str(self._vin[3]).upper()} {self.type}" - ) - - def _uniq_name(self) -> Text: - return f"Tesla Model {str(self._vin[3]).upper()} {self._vin[-6:]} {self.type}" - - def id(self) -> int: - # pylint: disable=invalid-name - """Return the id of this Vehicle.""" - return self._id - - def vin(self) -> str: - # pylint: disable=invalid-name - """Return the vin of this Vehicle.""" - return self._vin - - def vehicle_id(self) -> int: - """Return the vehicle_id of this Vehicle.""" - return self._vehicle_id - - def car_name(self) -> Text: - """Return the car name of this Vehicle.""" - return ( - self._display_name - if self._display_name is not None and self._display_name != self._vin[-6:] - else f"Tesla Model {str(self._vin[3]).upper()}" - ) - - @property - def car_version(self) -> Text: - """Return the software version of this Vehicle.""" - return self._car_version - - @property - def update_available(self) -> bool: - """Return whether an update is available for this Vehicle.""" - return self._update_available - - @property - def update_version(self) -> Optional[Text]: - """Return the update version of this Vehicle.""" - return self._update_version - - @property - def car_type(self) -> Text: - """Return the type of this Vehicle.""" - return self._car_type - - @property - def sentry_mode_available(self) -> bool: - """Return True if sentry mode is available on this Vehicle.""" - return self._sentry_mode_available - - def assumed_state(self) -> bool: - # 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 - ) - - async def async_update( - self, wake_if_asleep: bool = False, force: bool = False - ) -> None: - """Update the vehicle data. - - This function will call a controller update. - """ - await self._controller.update( - self.id(), wake_if_asleep=wake_if_asleep, force=force - ) - self.refresh() - - def refresh(self) -> None: - """Refresh the vehicle data. - - This assumes the controller has already been updated. This should be - called by inherited classes so the overall vehicle information is updated. - """ - state = self._controller.get_state_params(self.id()) - if state and "car_version" in state: - self._car_version = state["car_version"] - if state and "sentry_mode_available" in state: - self._sentry_mode_available = state["sentry_mode_available"] - self._update_available = state.get("software_update", {}).get("status") in { - "available", - "scheduled", - } - self._update_version = state.get("software_update", {}).get("version") - - @staticmethod - def is_armable() -> bool: - """Return whether the data is from an online vehicle.""" - return False - - @staticmethod - def is_armed() -> bool: - """Return whether the vehicle is armed.""" - return False diff --git a/teslajsonpy/homeassistant/vehicle_data.py b/teslajsonpy/homeassistant/vehicle_data.py deleted file mode 100644 index 8c818ef1..00000000 --- a/teslajsonpy/homeassistant/vehicle_data.py +++ /dev/null @@ -1,317 +0,0 @@ -# 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 - -from typing import Optional, Text, List -from teslajsonpy.homeassistant.vehicle import VehicleDevice - -_LOGGER = logging.getLogger(__name__) - - -class VehicleDataSensor(VehicleDevice): - """Home-Assistant vehicle data class for a Tesla VehicleDevice.""" - - def __init__(self, data: dict, controller) -> None: - """Initialize the vehicle data 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: Optional[str] = None - - self.type: Text = "sensor" - self.hass_type: Text = "sensor" - # this will be returned to HA as a device_class - # https://developers.home-assistant.io/docs/core/entity/binary-sensor - self._sensor_type: Optional[Text] = None - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - self._device_class: Optional[Text] = None - self.measurement: Optional[Text] = None - self.enabled_by_default = False - - async def async_update(self, wake_if_asleep=False, force=False) -> None: - """Update the vehicle data.""" - await super().async_update(wake_if_asleep=wake_if_asleep) - self.refresh() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - self.__state = self.vin() - - def get_value(self) -> Optional[str]: - """Return the state.""" - return self.__state - - @property - def device_class(self) -> Optional[Text]: - """Return the HA device class.""" - return self._device_class - - @classmethod - def _dict_to_attr( - cls, - data: dict, - exclude_dicts: Optional[List[str]] = None, - prepend: Optional[str] = None, - ) -> dict: - """Convert Tesla returned dict into dict for attributes.""" - prepend = prepend or "" - exclude_dicts = exclude_dicts or [] - - attr: dict = {} - for key, value in data.items(): - prepend_key = f"{key}" if prepend == "" else f"{prepend}_{key}" - if isinstance(value, dict): - if key in exclude_dicts or "*" in exclude_dicts: - continue - attr.update(cls._dict_to_attr(value, exclude_dicts, prepend_key)) - else: - - attr[prepend_key] = value - - return attr - - -class ClimateStateDataSensor(VehicleDataSensor): - """Home-Assistant Climate State sensor class for a Tesla VehicleDevice.""" - - def __init__(self, data: dict, controller) -> None: - """Initialize the ClimateStateData sensor. - - Args: - data (Dict): The base state for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/data - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - - self.type: Text = "climate state data sensor" - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - - self.attrs = self._dict_to_attr(self._controller.get_climate_params(self._id)) - - -class ChargeStateDataSensor(VehicleDataSensor): - """Home-Assistant Charge State sensor class for a Tesla VehicleDevice.""" - - def __init__(self, data: dict, controller) -> None: - """Initialize the ChargeStateData sensor. - - Args: - data (Dict): The base state for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/data - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - - self.type: Text = "charging state data sensor" - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - self.attrs = self._dict_to_attr(self._controller.get_charging_params(self._id)) - - -class VehicleStateDataSensor(VehicleDataSensor): - """Home-Assistant Vehicle State sensor class for a Tesla VehicleDevice.""" - - def __init__(self, data: dict, controller) -> None: - """Initialize the VehicleStateData sensor. - - Args: - data (Dict): The base state for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/data - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - - self.type: Text = "vehicle state data sensor" - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - self.attrs = self._dict_to_attr( - self._controller.get_state_params(self._id), - ["software_update", "speed_limit_mode"], - ) - - -class SoftwareDataSensor(VehicleDataSensor): - """Home-Assistant Software sensor class for a Tesla VehicleDevice.""" - - def __init__(self, data: dict, controller) -> None: - """Initialize the SoftwareData sensor. - - Args: - data (Dict): The base state for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/data - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - - self.type: Text = "software data sensor" - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - self.attrs = self._dict_to_attr( - self._controller.get_state_params(self._id).get("software_update", {}) - ) - - -class SpeedLimitDataSensor(VehicleDataSensor): - """Home-Assistant Speed Limit sensor class for a Tesla VehicleDevice.""" - - def __init__(self, data: dict, controller) -> None: - """Initialize the SpeedLimitData sensor. - - Args: - data (Dict): The base state for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/data - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - - self.type: Text = "speed limit data sensor" - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - self.attrs = self._dict_to_attr( - self._controller.get_state_params(self._id).get("speed_limit_mode", {}) - ) - - -class VehicleConfigDataSensor(VehicleDataSensor): - """Home-Assistant Vehicle Config sensor class for a Tesla VehicleDevice.""" - - def __init__(self, data: dict, controller) -> None: - """Initialize the VehicleConfigData sensor. - - Args: - data (Dict): The base state for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/data - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - - self.type: Text = "vehicle config data sensor" - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - self.attrs = self._dict_to_attr(self._controller.get_config_params(self._id)) - - -class DriveStateDataSensor(VehicleDataSensor): - """Home-Assistant Drive State sensor class for a Tesla VehicleDevice.""" - - def __init__(self, data: dict, controller) -> None: - """Initialize the DriveStateData sensor. - - Args: - data (Dict): The base state for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/data - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - - self.type: Text = "drive state data sensor" - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - self.attrs = self._dict_to_attr(self._controller.get_drive_params(self._id)) - - -class GuiSettingsDataSensor(VehicleDataSensor): - """Home-Assistant GUI settings sensor class for a Tesla VehicleDevice.""" - - def __init__(self, data: dict, controller) -> None: - """Initialize the GuiSettingsData sensor. - - Args: - data (Dict): The base state for a Tesla vehicle. - https://tesla-api.timdorr.com/vehicle/state/data - controller (Controller): The controller that controls updates to the Tesla API. - - """ - super().__init__(data, controller) - - self.type: Text = "gui settings data sensor" - self.name: Text = self._name() - self.uniq_name: Text = self._uniq_name() - - def refresh(self) -> None: - """Refresh data. - - This assumes the controller has already been updated - """ - super().refresh() - self.attrs = self._dict_to_attr(self._controller.get_gui_params(self._id)) diff --git a/tests/tesla_mock.py b/tests/tesla_mock.py index c266490c..102042ae 100644 --- a/tests/tesla_mock.py +++ b/tests/tesla_mock.py @@ -36,33 +36,47 @@ def __init__(self, monkeypatch) -> None: # self._monkeypatch.setattr( # Controller, "get_state_params", self.mock_get_state_params # ) - self._monkeypatch.setattr(Controller, "get_vehicles", self.mock_get_vehicles) self._monkeypatch.setattr( - Controller, "get_energysites", self.mock_get_energysites + Controller, "get_product_list", self.mock_get_product_list ) self._monkeypatch.setattr( Controller, "get_site_config", self.mock_get_site_config ) # self._monkeypatch.setattr( + # Controller, + # "_get_and_process_car_data", + # self.mock_get_and_process_car_data, + # ) + # self._monkeypatch.setattr( + # Controller, + # "_get_and_process_site_data", + # self.mock_get_and_process_site_data, + # ) + # self._monkeypatch.setattr( + # Controller, + # "_get_and_process_battery_data", + # self.mock_get_and_process_battery_data, + # ) + # self._monkeypatch.setattr( # Controller, "get_last_update_time", self.mock_get_last_update_time # ) self._monkeypatch.setattr(Controller, "update", self.mock_update) self._monkeypatch.setattr( Controller, "get_power_params", self.mock_get_power_params ) - self._vehicle_product_list = copy.deepcopy(VEHICLE_PRODUCT_LIST) + self._product_list = copy.deepcopy(PRODUCT_LIST) + self._vehicle_data = copy.deepcopy(VEHICLE_DATA) + self._site_data = copy.deepcopy(SITE_DATA) + self._battery_data = copy.deepcopy(BATTERY_DATA) self._drive_state = copy.deepcopy(DRIVE_STATE) self._climate_state = copy.deepcopy(CLIMATE_STATE) self._charge_state = copy.deepcopy(CHARGE_STATE) self._gui_settings = copy.deepcopy(GUI_SETTINGS) self._vehicle_state = copy.deepcopy(VEHICLE_STATE) self._vehicle_config = copy.deepcopy(VEHICLE_CONFIG) - self._product_list = copy.deepcopy(ENERGYSITE_PRODUCT_LIST) self._solar_combined_data = copy.deepcopy(SOLAR_COMBINED_DATA) self._solar_combined_data_no_name = copy.deepcopy(SOLAR_COMBINED_DATA_NO_NAME) - self._battery_combined_data = copy.deepcopy(BATTERY_COMBINED_DATA) self._site_config = copy.deepcopy(SITE_CONFIG) - self._site_state = copy.deepcopy(SITE_STATE) self._site_state_unknown_grid = copy.deepcopy(SITE_STATE_UNKNOWN_GRID) self._vehicle = copy.deepcopy(VEHICLE) self._vehicle["drive_state"] = self._drive_state @@ -122,21 +136,31 @@ def mock_get_state_params(self, *args, **kwargs): """Mock controller's get_state_params method.""" return self.controller_get_state_params() - def mock_get_vehicles(self, *args, **kwargs): - # pylint: disable=unused-argument - """Mock controller's get_vehicles method.""" - return self.controller_get_vehicles() - - def mock_get_energysites(self, *args, **kwargs): + def mock_get_product_list(self, *args, **kwargs): # pylint: disable=unused-argument - """Mock controller's get_energysites method.""" - return self.controller_get_energysites() + """Mock controller's get_product_list method.""" + return self.controller_get_product_list() def mock_get_site_config(self, *args, **kwargs): # pylint: disable=unused-argument """Mock controller's get_site_config method.""" return self.controller_get_site_config() + def mock_get_and_process_car_data(self, *args, **kwargs): + # pylint: disable=unused-argument + """Mock controller's _get_and_process_car_data method.""" + return self.controller_get_and_process_car_data() + + def mock_get_and_process_site_data(self, *args, **kwargs): + # pylint: disable=unused-argument + """Mock controller's _get_and_process_site_data method.""" + return self.controller_get_and_process_site_data() + + def mock_get_and_process_battery_data(self, *args, **kwargs): + # pylint: disable=unused-argument + """Mock controller's _get_and_process_battery_data method.""" + return self.controller_get_and_process_battery_data() + def mock_get_last_update_time(self, *args, **kwargs): # pylint: disable=unused-argument """Mock controller's get_last_update_time method.""" @@ -190,18 +214,26 @@ def controller_get_state_params(self): """Monkeypatch for controller.get_state_params().""" return self._vehicle_state - async def controller_get_vehicles(self): - """Monkeypatch for controller.get_vehicles().""" - return self._vehicle_product_list - - async def controller_get_energysites(self): - """Monkeypatch for controller.get_energysites().""" + async def controller_get_product_list(self): + """Monkeypatch for controller.get_product_list().""" return self._product_list async def controller_get_site_config(self): """Monkeypatch for controller.get_site_config().""" return self._site_config + async def controller_get_and_process_car_data(self): + """Monkeypatch for controller.update._get_and_process_car_data().""" + return self._vehicle_data + + async def controller_get_and_process_site_data(self): + """Monkeypatch for controller.update._get_and_process_site_data().""" + return self._site_data + + async def controller_get_and_process_battery_data(self): + """Monkeypatch for controller.update._get_and_process_battery_data().""" + return self._battery_data + @staticmethod async def controller_update(): """Monkeypatch for controller.update().""" @@ -236,14 +268,6 @@ def data_request_solar_combined_data_no_name(self): """Similate the result of combined product list & site config without name.""" return self._solar_combined_data_no_name - def data_request_battery_combined_data(self): - """Similate the result of a battery site from product_list.""" - return self._battery_combined_data - - def data_request_site_state(self): - """Similate the result of site state request.""" - return self._site_state - def data_request_site_state_unknown_grid(self): """Similate the result of site state with unknown grid data request.""" return self._site_state_unknown_grid @@ -267,7 +291,7 @@ def command_ok(): VIN = "5YJSA11111111111" CAR_ID = 12345678901234567 -VEHICLE_PRODUCT_LIST = [ +PRODUCT_LIST = [ { "id": 12345678901234567, "vehicle_id": 1234567890, @@ -284,9 +308,59 @@ def command_ok(): "api_version": 36, "backseat_token": None, "backseat_token_updated_at": None, - } + }, + { + "energy_site_id": 12345, + "resource_type": "solar", + "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", + "asset_site_id": "12345", + "solar_power": 2260, + "solar_type": "pv_panel", + "storm_mode_enabled": None, + "powerwall_onboarding_settings_set": None, + "sync_grid_alert_enabled": False, + "breaker_alert_enabled": False, + "components": { + "battery": False, + "solar": True, + "solar_type": "pv_panel", + "grid": True, + "load_meter": True, + "market_type": "residential", + }, + }, + { + "energy_site_id": 67890, + "resource_type": "battery", + "site_name": "My Battery Home", + "id": "212dbc27-333c-45b1-81bb-31e2zd2fs2cm", + "gateway_id": "67890", + "asset_site_id": "67890", + "energy_left": 2864.7368421052633, + "total_pack_energy": 14070, + "percentage_charged": 20.360603000037408, + "battery_type": "ac_powerwall", + "backup_capable": True, + "battery_power": 3080, + "storm_mode_enabled": True, + "powerwall_onboarding_settings_set": True, + "sync_grid_alert_enabled": True, + "breaker_alert_enabled": True, + "components": { + "battery": True, + "battery_type": "ac_powerwall", + "solar": True, + "solar_type": "pv_panel", + "grid": True, + "load_meter": True, + "market_type": "residential", + }, + }, ] +# Temporary +VEHICLE_DATA = 123 + DRIVE_STATE = { "gps_as_of": 1538363883, "heading": 5, @@ -499,57 +573,6 @@ def command_ok(): "vehicle_config": None, } -# Example of battery_data response with two energy sites for future tests -ENERGYSITE_PRODUCT_LIST = [ - { - "energy_site_id": 12345, - "resource_type": "solar", - "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", - "asset_site_id": "12345", - "solar_power": 2260, - "solar_type": "pv_panel", - "storm_mode_enabled": None, - "powerwall_onboarding_settings_set": None, - "sync_grid_alert_enabled": False, - "breaker_alert_enabled": False, - "components": { - "battery": False, - "solar": True, - "solar_type": "pv_panel", - "grid": True, - "load_meter": True, - "market_type": "residential", - }, - }, - { - "energy_site_id": 67890, - "resource_type": "battery", - "site_name": "My Battery Home", - "id": "212dbc27-333c-45b1-81bb-31e2zd2fs2cm", - "gateway_id": "67890", - "asset_site_id": "67890", - "energy_left": 2864.7368421052633, - "total_pack_energy": 14070, - "percentage_charged": 20.360603000037408, - "battery_type": "ac_powerwall", - "backup_capable": True, - "battery_power": 3080, - "storm_mode_enabled": True, - "powerwall_onboarding_settings_set": True, - "sync_grid_alert_enabled": True, - "breaker_alert_enabled": True, - "components": { - "battery": True, - "battery_type": "ac_powerwall", - "solar": True, - "solar_type": "pv_panel", - "grid": True, - "load_meter": True, - "market_type": "residential", - }, - }, -] - SITE_CONFIG = { "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", "site_name": "My Solar Home", @@ -739,38 +762,38 @@ def command_ok(): }, } # Data added from Controller.connect() initialization (solar_power, load_power, etc.) -BATTERY_COMBINED_DATA = { - "energy_site_id": 67890, - "resource_type": "battery", - "site_name": "My Battery Home", - "id": "212dbc27-333c-45b1-81bb-31e2zd2fs2cm", - "gateway_id": "67890", - "asset_site_id": "67890", - "energy_left": 2864.7368421052633, - "total_pack_energy": 14070, - "percentage_charged": 20.360603000037408, - "battery_type": "ac_powerwall", - "backup_capable": True, - "battery_power": 0, - "storm_mode_enabled": True, - "powerwall_onboarding_settings_set": True, - "sync_grid_alert_enabled": True, - "breaker_alert_enabled": True, - "components": { - "battery": True, - "battery_type": "ac_powerwall", - "solar": True, - "solar_type": "pv_panel", - "grid": True, - "load_meter": True, - "market_type": "residential", - }, - "solar_power": 0, - "load_power": 0, - "grid_power": 0, -} - -SITE_STATE = { +# BATTERY_COMBINED_DATA = { +# "energy_site_id": 67890, +# "resource_type": "battery", +# "site_name": "My Battery Home", +# "id": "212dbc27-333c-45b1-81bb-31e2zd2fs2cm", +# "gateway_id": "67890", +# "asset_site_id": "67890", +# "energy_left": 2864.7368421052633, +# "total_pack_energy": 14070, +# "percentage_charged": 20.360603000037408, +# "battery_type": "ac_powerwall", +# "backup_capable": True, +# "battery_power": 0, +# "storm_mode_enabled": True, +# "powerwall_onboarding_settings_set": True, +# "sync_grid_alert_enabled": True, +# "breaker_alert_enabled": True, +# "components": { +# "battery": True, +# "battery_type": "ac_powerwall", +# "solar": True, +# "solar_type": "pv_panel", +# "grid": True, +# "load_meter": True, +# "market_type": "residential", +# }, +# "solar_power": 0, +# "load_power": 0, +# "grid_power": 0, +# } + +SITE_DATA = { "solar_power": 7720, "energy_left": 0, "total_pack_energy": 1, @@ -797,7 +820,7 @@ def command_ok(): } # Example of battery_data response for future tests BATTERY_DATA = { - "energy_site_id": 123456789, + "energy_site_id": 67890, "resource_type": "battery", "site_name": "My Battery Home", "id": "XXX", diff --git a/tests/unit_tests/homeassistant/__init__.py b/tests/unit_tests/homeassistant/__init__.py deleted file mode 100644 index 2a978c16..00000000 --- a/tests/unit_tests/homeassistant/__init__.py +++ /dev/null @@ -1,7 +0,0 @@ -# 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 -""" diff --git a/tests/unit_tests/homeassistant/test_alerts.py b/tests/unit_tests/homeassistant/test_alerts.py deleted file mode 100644 index c3bb11cd..00000000 --- a/tests/unit_tests/homeassistant/test_alerts.py +++ /dev/null @@ -1,46 +0,0 @@ -"""Test sentry mode switch.""" - -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.alerts import Horn, FlashLights - -from tests.tesla_mock import TeslaMock - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _button = Horn(_data, _controller) - - assert not _button.has_battery() - - -@pytest.mark.asyncio -async def test_honk_horn(monkeypatch): - """Test test_honk_horn().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _button = Horn(_data, _controller) - - await _button.honk_horn() - - -@pytest.mark.asyncio -async def test_flash_light(monkeypatch): - """Test test_flash_light().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _button = FlashLights(_data, _controller) - - await _button.flash_lights() diff --git a/tests/unit_tests/homeassistant/test_battery_sensor.py b/tests/unit_tests/homeassistant/test_battery_sensor.py deleted file mode 100644 index 403cec6f..00000000 --- a/tests/unit_tests/homeassistant/test_battery_sensor.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Test battery sensor.""" - -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.battery_sensor import Battery - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = Battery(_data, _controller) - - assert _sensor.has_battery() - - -def test_device_class(monkeypatch): - """Test device_class().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = Battery(_data, _controller) - - assert _sensor.device_class == "battery" - - -def test_get_value_on_init(monkeypatch): - """Test get_value() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = Battery(_data, _controller) - - assert _sensor is not None - assert _sensor.get_value() is None - - -@pytest.mark.asyncio -async def test_get_value_after_update(monkeypatch): - """Test get_value() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = Battery(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _sensor.async_update() - - assert _sensor is not None - assert not _sensor.get_value() is None - assert _sensor.get_value() == 64 - - -@pytest.mark.asyncio -async def test_battery_level(monkeypatch): - """Test battery_level().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = Battery(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _sensor.async_update() - - assert _sensor is not None - assert not _sensor.get_value() is None - assert _sensor.battery_level() == 64 - - -@pytest.mark.asyncio -async def test_battery_charging_off(monkeypatch): - """Test battery_charging() when not charging.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charging_state"] = "Disconnected" - _sensor = Battery(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _sensor.async_update() - - assert _sensor is not None - assert not _sensor.battery_charging() - - -@pytest.mark.asyncio -async def test_battery_charging_on(monkeypatch): - """Test battery_charging() when charging.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charging_state"] = "Charging" - _sensor = Battery(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.battery_charging() - - -@pytest.mark.asyncio -async def test_async_update(monkeypatch): - """Test async_update().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["battery_level"] = 12.3 - _sensor = Battery(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _sensor.async_update() - - assert _sensor is not None - assert not _sensor.get_value() is None - assert _sensor.get_value() == 12.3 diff --git a/tests/unit_tests/homeassistant/test_calculate_update_interval.py b/tests/unit_tests/homeassistant/test_calculate_update_interval.py deleted file mode 100644 index aea17aac..00000000 --- a/tests/unit_tests/homeassistant/test_calculate_update_interval.py +++ /dev/null @@ -1,352 +0,0 @@ -"""Test update interval calculations.""" -# pylint: disable=protected-access - -import time - -from teslajsonpy.controller import Controller -from teslajsonpy.const import SLEEP_INTERVAL, DRIVING_INTERVAL - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - -CAR_PARKED = 1577833200 # Timestamp a long time ago -NOW = time.time() - - -def test_interval_driving(monkeypatch): - """Test interval returned while driving.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - - _data["drive_state"]["shift_state"] = "D" - - _controller.set_last_update_time(car_id=CAR_ID, timestamp=NOW) - _controller.set_last_park_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_last_wake_up_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_state_params(car_id=CAR_ID, params=_data["vehicle_state"]) - _controller.set_climate_params(car_id=CAR_ID, params=_data["climate_state"]) - _controller.set_drive_params(car_id=CAR_ID, params=_data["drive_state"]) - - assert _controller._calculate_next_interval(VIN) == DRIVING_INTERVAL - - -def test_interval_policy_default_charging(monkeypatch): - """Test interval returned with default policy while charging.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - _controller.set_id_vin(CAR_ID, VIN) - - _data["drive_state"]["shift_state"] = None - _data["charge_state"]["charging_state"] = "Charging" - - _controller.set_last_update_time(car_id=CAR_ID, timestamp=NOW) - _controller.set_last_wake_up_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_state_params(car_id=CAR_ID, params=_data["vehicle_state"]) - _controller.set_climate_params(car_id=CAR_ID, params=_data["climate_state"]) - _controller.set_drive_params(car_id=CAR_ID, params=_data["drive_state"]) - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_last_park_time(car_id=CAR_ID, timestamp=NOW) - - assert _controller._calculate_next_interval(VIN) == _controller.update_interval - - -def test_interval_policy_default_charging_idle(monkeypatch): - """Test interval returned while charging after car parked for a long time.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - _controller.set_id_vin(CAR_ID, VIN) - - _data["drive_state"]["shift_state"] = None - _data["charge_state"]["charging_state"] = "Charging" - - _controller.set_last_update_time(car_id=CAR_ID, timestamp=NOW) - _controller.set_last_wake_up_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_state_params(car_id=CAR_ID, params=_data["vehicle_state"]) - _controller.set_climate_params(car_id=CAR_ID, params=_data["climate_state"]) - _controller.set_drive_params(car_id=CAR_ID, params=_data["drive_state"]) - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_last_park_time(car_id=CAR_ID, timestamp=CAR_PARKED) - - assert _controller._calculate_next_interval(VIN) == _controller.update_interval - - -def test_interval_policy_default_completed(monkeypatch): - """Test interval returned after completed charging.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - _controller.set_id_vin(CAR_ID, VIN) - - _data["vehicle_state"]["sentry_mode"] = False - _data["drive_state"]["shift_state"] = None - _data["charge_state"]["charging_state"] = "Complete" - - _controller.set_last_update_time(car_id=CAR_ID, timestamp=NOW) - _controller.set_last_wake_up_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_state_params(car_id=CAR_ID, params=_data["vehicle_state"]) - _controller.set_climate_params(car_id=CAR_ID, params=_data["climate_state"]) - _controller.set_drive_params(car_id=CAR_ID, params=_data["drive_state"]) - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_last_park_time(car_id=CAR_ID, timestamp=NOW) - - assert _controller._calculate_next_interval(VIN) == _controller.update_interval - - -def test_interval_policy_default_completed_idle(monkeypatch): - """Test interval returned after completed charging and car parked for a long time.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - _controller.set_id_vin(CAR_ID, VIN) - - _data["vehicle_state"]["sentry_mode"] = False - _data["drive_state"]["shift_state"] = None - _data["charge_state"]["charging_state"] = "Complete" - - _controller.set_last_update_time(car_id=CAR_ID, timestamp=NOW) - _controller.set_last_wake_up_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_state_params(car_id=CAR_ID, params=_data["vehicle_state"]) - _controller.set_climate_params(car_id=CAR_ID, params=_data["climate_state"]) - _controller.set_drive_params(car_id=CAR_ID, params=_data["drive_state"]) - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_last_park_time(car_id=CAR_ID, timestamp=CAR_PARKED) - - assert _controller._calculate_next_interval(VIN) == SLEEP_INTERVAL - - -def test_interval_policy_default_disconnected_idle(monkeypatch): - """Test interval returned after disconnected charger and car parked for a long time.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - _controller.set_id_vin(CAR_ID, VIN) - - _data["vehicle_state"]["sentry_mode"] = False - _data["drive_state"]["shift_state"] = None - _data["charge_state"]["charging_state"] = "Disconnected" - - _controller.set_last_update_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_last_wake_up_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_state_params(car_id=CAR_ID, params=_data["vehicle_state"]) - _controller.set_climate_params(car_id=CAR_ID, params=_data["climate_state"]) - _controller.set_drive_params(car_id=CAR_ID, params=_data["drive_state"]) - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_last_park_time(car_id=CAR_ID, timestamp=CAR_PARKED) - - assert _controller._calculate_next_interval(VIN) == SLEEP_INTERVAL - - -def test_interval_policy_always(monkeypatch): - """Test interval returned with policy set to always.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - monkeypatch.setattr(_controller, "polling_policy", "always") - _controller.set_id_vin(CAR_ID, VIN) - - _data["vehicle_state"]["sentry_mode"] = False - _data["drive_state"]["shift_state"] = None - _data["charge_state"]["charging_state"] = "Disconnected" - - _controller.set_last_update_time(car_id=CAR_ID, timestamp=NOW) - _controller.set_last_wake_up_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_state_params(car_id=CAR_ID, params=_data["vehicle_state"]) - _controller.set_climate_params(car_id=CAR_ID, params=_data["climate_state"]) - _controller.set_drive_params(car_id=CAR_ID, params=_data["drive_state"]) - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_last_park_time(car_id=CAR_ID, timestamp=NOW) - - assert _controller._calculate_next_interval(VIN) == _controller.update_interval - - -def test_interval_policy_always_disconnected_idle(monkeypatch): - """Test interval returned with policy set to always and car parked for a long time.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - monkeypatch.setattr(_controller, "polling_policy", "always") - _controller.set_id_vin(CAR_ID, VIN) - - _data["vehicle_state"]["sentry_mode"] = False - _data["drive_state"]["shift_state"] = None - _data["charge_state"]["charging_state"] = "Disconnected" - - _controller.set_last_update_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_last_wake_up_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_state_params(car_id=CAR_ID, params=_data["vehicle_state"]) - _controller.set_climate_params(car_id=CAR_ID, params=_data["climate_state"]) - _controller.set_drive_params(car_id=CAR_ID, params=_data["drive_state"]) - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_last_park_time(car_id=CAR_ID, timestamp=CAR_PARKED) - - assert _controller._calculate_next_interval(VIN) == _controller.update_interval - - -def test_interval_policy_connected_charging(monkeypatch): - """Test interval returned with policy set to connected and car charging.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - monkeypatch.setattr(_controller, "polling_policy", "connected") - _controller.set_id_vin(CAR_ID, VIN) - - _data["vehicle_state"]["sentry_mode"] = False - _data["drive_state"]["shift_state"] = None - _data["charge_state"]["charging_state"] = "Disconnected" - - _controller.set_last_update_time(car_id=CAR_ID, timestamp=NOW) - _controller.set_last_wake_up_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_state_params(car_id=CAR_ID, params=_data["vehicle_state"]) - _controller.set_climate_params(car_id=CAR_ID, params=_data["climate_state"]) - _controller.set_drive_params(car_id=CAR_ID, params=_data["drive_state"]) - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_last_park_time(car_id=CAR_ID, timestamp=NOW) - - assert _controller._calculate_next_interval(VIN) == _controller.update_interval - - -def test_interval_policy_connected_completed(monkeypatch): - """Test interval returned with policy set to connected and charging completed.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - monkeypatch.setattr(_controller, "polling_policy", "connected") - _controller.set_id_vin(CAR_ID, VIN) - - _data["vehicle_state"]["sentry_mode"] = False - _data["drive_state"]["shift_state"] = None - _data["charge_state"]["charging_state"] = "Completed" - - _controller.set_last_update_time(car_id=CAR_ID, timestamp=NOW) - _controller.set_last_wake_up_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_state_params(car_id=CAR_ID, params=_data["vehicle_state"]) - _controller.set_climate_params(car_id=CAR_ID, params=_data["climate_state"]) - _controller.set_drive_params(car_id=CAR_ID, params=_data["drive_state"]) - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_last_park_time(car_id=CAR_ID, timestamp=NOW) - - assert _controller._calculate_next_interval(VIN) == _controller.update_interval - - -def test_interval_policy_connected_completed_idle(monkeypatch): - """Test interval returned with policy set to connected and charging completed even when idle.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - - _data["vehicle_state"]["sentry_mode"] = False - _data["drive_state"]["shift_state"] = None - _data["charge_state"]["charging_state"] = "Completed" - - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - monkeypatch.setattr(_controller, "polling_policy", "connected") - _controller.set_id_vin(CAR_ID, VIN) - - _controller.set_last_update_time(car_id=CAR_ID, timestamp=NOW) - _controller.set_last_wake_up_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_state_params(car_id=CAR_ID, params=_data["vehicle_state"]) - _controller.set_climate_params(car_id=CAR_ID, params=_data["climate_state"]) - _controller.set_drive_params(car_id=CAR_ID, params=_data["drive_state"]) - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_last_park_time(car_id=CAR_ID, timestamp=CAR_PARKED) - - assert _controller._calculate_next_interval(VIN) == _controller.update_interval - - -def test_interval_policy_connected_disconnected(monkeypatch): - """Test interval returned while driving().""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - monkeypatch.setattr(_controller, "polling_policy", "connected") - _controller.set_id_vin(CAR_ID, VIN) - - _data["vehicle_state"]["sentry_mode"] = False - _data["drive_state"]["shift_state"] = None - _data["charge_state"]["charging_state"] = "Disconnected" - - _controller.set_last_update_time(car_id=CAR_ID, timestamp=NOW) - _controller.set_last_wake_up_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_state_params(car_id=CAR_ID, params=_data["vehicle_state"]) - _controller.set_climate_params(car_id=CAR_ID, params=_data["climate_state"]) - _controller.set_drive_params(car_id=CAR_ID, params=_data["drive_state"]) - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_last_park_time(car_id=CAR_ID, timestamp=NOW) - - assert _controller._calculate_next_interval(VIN) == _controller.update_interval - - -def test_interval_policy_connected_disconnected_idle(monkeypatch): - """Test interval returned while driving().""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - monkeypatch.setattr(_controller, "polling_policy", "connected") - _controller.set_id_vin(CAR_ID, VIN) - - _data["vehicle_state"]["sentry_mode"] = False - _data["drive_state"]["shift_state"] = None - _data["charge_state"]["charging_state"] = "Disconnected" - - _controller.set_last_update_time(car_id=CAR_ID, timestamp=NOW) - _controller.set_last_wake_up_time(car_id=CAR_ID, timestamp=CAR_PARKED) - _controller.set_state_params(car_id=CAR_ID, params=_data["vehicle_state"]) - _controller.set_climate_params(car_id=CAR_ID, params=_data["climate_state"]) - _controller.set_drive_params(car_id=CAR_ID, params=_data["drive_state"]) - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_last_park_time(car_id=CAR_ID, timestamp=CAR_PARKED) - - assert _controller._calculate_next_interval(VIN) == SLEEP_INTERVAL diff --git a/tests/unit_tests/homeassistant/test_charger_connection_sensor.py b/tests/unit_tests/homeassistant/test_charger_connection_sensor.py deleted file mode 100644 index bb717fec..00000000 --- a/tests/unit_tests/homeassistant/test_charger_connection_sensor.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Test charger connection sensor.""" - -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.binary_sensor import ChargerConnectionSensor - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = ChargerConnectionSensor(_data, _controller) - - assert not _sensor.has_battery() - - -def test_get_value_on_init(monkeypatch): - """Test get_value() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = ChargerConnectionSensor(_data, _controller) - - assert _sensor is not None - assert _sensor.get_value() is None - - -@pytest.mark.asyncio -async def test_get_value_after_update(monkeypatch): - """Test get_value() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charging_state"] = "Charging" - _sensor = ChargerConnectionSensor(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.get_value() is not None - assert _sensor.get_value() - - -@pytest.mark.asyncio -async def test_get_value_on(monkeypatch): - """Test get_value() for charging state ON.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charging_state"] = "Charging" - _sensor = ChargerConnectionSensor(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.get_value() is not None - assert _sensor.get_value() - - -@pytest.mark.asyncio -async def test_get_value_off(monkeypatch): - """Test get_value() for charging state OFF.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charging_state"] = "Disconnected" - _sensor = ChargerConnectionSensor(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.get_value() is not None - assert not _sensor.get_value() diff --git a/tests/unit_tests/homeassistant/test_charger_lock.py b/tests/unit_tests/homeassistant/test_charger_lock.py deleted file mode 100644 index 4eae6bc6..00000000 --- a/tests/unit_tests/homeassistant/test_charger_lock.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Test charger lock.""" - -import pytest -import time - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.lock import ChargerLock - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - -LAST_UPDATE_TIME = time.time() - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _lock = ChargerLock(_data, _controller) - - assert not _lock.has_battery() - - -def test_is_locked_on_init(monkeypatch): - """Test is_locked() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _lock = ChargerLock(_data, _controller) - - assert _lock is not None - assert not _lock.is_locked() - - -@pytest.mark.asyncio -async def test_is_locked_after_update(monkeypatch): - """Test is_locked() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charge_port_door_open"] = True - _lock = ChargerLock(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _lock.async_update() - - assert _lock is not None - assert _lock.is_locked() - - -@pytest.mark.asyncio -async def test_lock(monkeypatch): - """Test lock().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charge_port_door_open"] = False - _lock = ChargerLock(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _lock.async_update() - await _lock.lock() - - assert _lock is not None - assert _lock.is_locked() - - -@pytest.mark.asyncio -async def test_lock_already_locked(monkeypatch): - """Test lock() when already locked.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charge_port_door_open"] = True - _lock = ChargerLock(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _lock.async_update() - await _lock.lock() - - assert _lock is not None - assert _lock.is_locked() - - -@pytest.mark.asyncio -async def test_unlock(monkeypatch): - """Test unlock().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charge_port_door_open"] = True - _lock = ChargerLock(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _lock.async_update() - await _lock.unlock() - - assert _lock is not None - assert not _lock.is_locked() - - -@pytest.mark.asyncio -async def test_unlock_already_unlocked(monkeypatch): - """Test unlock() when already unlocked.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charge_port_door_open"] = False - _lock = ChargerLock(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _lock.async_update() - await _lock.unlock() - - assert _lock is not None - assert not _lock.is_locked() diff --git a/tests/unit_tests/homeassistant/test_charger_switch.py b/tests/unit_tests/homeassistant/test_charger_switch.py deleted file mode 100644 index 004ed9fd..00000000 --- a/tests/unit_tests/homeassistant/test_charger_switch.py +++ /dev/null @@ -1,150 +0,0 @@ -"""Test charger switch.""" - -import pytest -import time - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.charger import ChargerSwitch - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - -LAST_UPDATE_TIME = time.time() - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _switch = ChargerSwitch(_data, _controller) - - assert not _switch.has_battery() - - -def test_is_charging_on_init(monkeypatch): - """Test is_charging() when not charging.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _switch = ChargerSwitch(_data, _controller) - - assert not _switch.is_charging() - - -@pytest.mark.asyncio -async def test_is_charging_on(monkeypatch): - """Test is_charging() with charging state charging.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charging_state"] = "Charging" - _switch = ChargerSwitch(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _switch.async_update() - assert _switch.is_charging() - - -@pytest.mark.asyncio -async def test_is_charging_off(monkeypatch): - """Test is_charging() with charging state disconnected.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charging_state"] = "Disconnected" - _switch = ChargerSwitch(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _switch.async_update() - assert not _switch.is_charging() - - -@pytest.mark.asyncio -async def test_start_charge(monkeypatch): - """Test start_charge().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charging_state"] = "Disconnected" - _switch = ChargerSwitch(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - await _switch.async_update() - - await _switch.start_charge() - assert _switch.is_charging() - - -@pytest.mark.asyncio -async def test_stop_charge(monkeypatch): - """Test stop_charge().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charging_state"] = "Charging" - _switch = ChargerSwitch(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - await _switch.async_update() - - await _switch.stop_charge() - assert not _switch.is_charging() - - -@pytest.mark.asyncio -async def test_async_update(monkeypatch): - """Test async_update().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charging_state"] = "Charging" - _switch = ChargerSwitch(_data, _controller) - - await _switch.async_update() - assert _switch.is_charging() - - -@pytest.mark.asyncio -async def test_async_update_with_change(monkeypatch): - """Test async_update().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charging_state"] = "Charging" - _switch = ChargerSwitch(_data, _controller) - - _data["charge_state"]["charging_state"] = "Disconnected" - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _switch.async_update() - assert not _switch.is_charging() diff --git a/tests/unit_tests/homeassistant/test_charging_sensor.py b/tests/unit_tests/homeassistant/test_charging_sensor.py deleted file mode 100644 index 3df12490..00000000 --- a/tests/unit_tests/homeassistant/test_charging_sensor.py +++ /dev/null @@ -1,219 +0,0 @@ -"""Test charging sensor.""" - -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.charger import ChargingSensor, ChargingEnergySensor - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = ChargingSensor(_data, _controller) - - assert not _sensor.has_battery() - - -def test_device_class(monkeypatch): - """Test device_class().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = ChargingSensor(_data, _controller) - - assert _sensor.device_class is None - - -def test_state_class(monkeypatch): - """Test device_class().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = ChargingSensor(_data, _controller) - - assert _sensor.device_class is None - - _sensor2 = ChargingEnergySensor(_data, _controller) - assert _sensor2.device_class == "energy" - - -def test_get_value_on_init(monkeypatch): - """Test get_value() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - sensors = [ - ChargingSensor(_data, _controller), - ChargingEnergySensor(_data, _controller), - ] - - for _sensor in sensors: - - assert _sensor is not None - assert _sensor.charging_rate is None - assert _sensor.time_left is None - assert _sensor.added_range is None - assert _sensor.charge_current_request is None - assert _sensor.charge_current_request_max is None - assert _sensor.charger_actual_current is None - assert _sensor.charger_voltage is None - assert _sensor.charger_power is None - assert _sensor.charge_energy_added is None - assert _sensor.charge_limit_soc is None - - -@pytest.mark.asyncio -async def test_get_value_after_update(monkeypatch): - """Test get_value() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = ChargingSensor(_data, _controller) - _sensor2 = ChargingEnergySensor(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.charging_rate == 0 - assert _sensor.time_left == 0 - assert _sensor.added_range == 40 - assert _sensor.charge_current_request == 48 - assert _sensor.charge_current_request_max == 48 - assert _sensor.charger_actual_current == 0 - assert _sensor.charger_voltage == 0 - assert _sensor.charger_power == 0 - assert _sensor.charge_energy_added == 12.41 - assert _sensor.charge_limit_soc == 90 - assert _sensor.device_class is None - - _sensor2 = ChargingEnergySensor(_data, _controller) - await _sensor2.async_update() - - assert _sensor2 is not None - assert _sensor2.charging_rate == 0 - assert _sensor2.time_left == 0 - assert _sensor2.charge_current_request == 48 - assert _sensor2.charge_current_request_max == 48 - assert _sensor2.charger_actual_current == 0 - assert _sensor2.charger_voltage == 0 - assert _sensor2.charger_power == 0 - assert _sensor2.charge_energy_added == 12.41 - assert _sensor2.charge_limit_soc == 90 - assert _sensor2.last_reset != 0 - assert _sensor2.state_class == "total_increasing" - assert _sensor2.device_class == "energy" - - -@pytest.mark.asyncio -async def test_async_update(monkeypatch): - """Test async_update().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = ChargingSensor(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.charging_rate == 0 - assert _sensor.time_left == 0 - assert _sensor.added_range == 40 - assert _sensor.charge_current_request == 48 - assert _sensor.charge_current_request_max == 48 - assert _sensor.charger_actual_current == 0 - assert _sensor.charger_voltage == 0 - assert _sensor.charge_energy_added == 12.41 - assert _sensor.charge_limit_soc == 90 - - -@pytest.mark.asyncio -async def test_async_update_in_kmh(monkeypatch): - """Test async_update() for units in km/h.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["gui_settings"]["gui_distance_units"] = "km/hr" - _data["gui_settings"]["gui_range_display"] = "Rated" - _data["charge_state"]["charge_rate"] = 22 - _data["charge_state"]["charge_miles_added_rated"] = 44 - _sensor = ChargingSensor(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - _controller.set_gui_params(vin=VIN, params=_data["gui_settings"]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.charging_rate == 35.41 - assert _sensor.added_range == 70.81 - - -@pytest.mark.asyncio -async def test_async_update_in_mph(monkeypatch): - """Test async_update() for units in mph.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["gui_settings"]["gui_distance_units"] = "mi/hr" - _data["gui_settings"]["gui_range_display"] = "Rated" - _data["charge_state"]["charge_rate"] = 22 - _data["charge_state"]["charge_miles_added_rated"] = 44 - _data["charge_state"]["charger_power"] = 100.2 - _sensor = ChargingSensor(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - _controller.set_gui_params(vin=VIN, params=_data["gui_settings"]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.charging_rate == 22 - assert _sensor.added_range == 44 - assert _sensor.charger_power == 100.2 - - -@pytest.mark.asyncio -async def test_async_update_charger_power(monkeypatch): - """Test async_update() for charger_power.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charger_power"] = 100.2 - _sensor = ChargingSensor(_data, _controller) - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - await _sensor.async_update() - - assert _sensor.charger_power == 100.2 diff --git a/tests/unit_tests/homeassistant/test_climate.py b/tests/unit_tests/homeassistant/test_climate.py deleted file mode 100644 index 4f029d3b..00000000 --- a/tests/unit_tests/homeassistant/test_climate.py +++ /dev/null @@ -1,284 +0,0 @@ -"""Test climate.""" - -import pytest -import time - -from teslajsonpy.controller import Controller -from teslajsonpy.exceptions import UnknownPresetMode -from teslajsonpy.homeassistant.climate import Climate - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - -LAST_UPDATE_TIME = time.time() - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _climate = Climate(_data, _controller) - - assert not _climate.has_battery() - - -def test_get_values_on_init(monkeypatch): - """Test values after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _climate = Climate(_data, _controller) - - assert _climate is not None - assert _climate.get_current_temp() is None - assert _climate.get_fan_status() is None - assert _climate.get_goal_temp() is None - assert _climate.is_hvac_enabled() is None - assert _climate.preset_mode is None - - -@pytest.mark.asyncio -async def test_get_values_after_update(monkeypatch): - """Test values after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _climate = Climate(_data, _controller) - - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - await _climate.async_update() - - assert _climate is not None - - assert _climate.get_current_temp() is None - assert not _climate.get_fan_status() is None - assert _climate.get_fan_status() == 0 - assert not _climate.get_goal_temp() is None - assert _climate.get_goal_temp() == 21.6 - assert not _climate.is_hvac_enabled() is None - assert not _climate.is_hvac_enabled() - assert _climate.preset_mode is not None - assert _climate.preset_mode == "normal" - - -@pytest.mark.asyncio -async def test_get_current_temp(monkeypatch): - """Test get_current_temp().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["climate_state"]["inside_temp"] = 18.8 - _climate = Climate(_data, _controller) - - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _climate.async_update() - - assert not _climate.get_current_temp() is None - assert _climate.get_current_temp() == 18.8 - - -@pytest.mark.asyncio -async def test_get_fan_status(monkeypatch): - """Test get_fan_status().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["climate_state"]["fan_status"] = 1 - _climate = Climate(_data, _controller) - - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _climate.async_update() - - assert not _climate.get_fan_status() is None - assert _climate.get_fan_status() == 1 - - -@pytest.mark.asyncio -async def test_get_goal_temp(monkeypatch): - """Test get_goal_temp().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["climate_state"]["driver_temp_setting"] = 23.4 - _climate = Climate(_data, _controller) - - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _climate.async_update() - - assert not _climate.get_goal_temp() is None - assert _climate.get_goal_temp() == 23.4 - - -@pytest.mark.asyncio -async def test_is_hvac_enabled_on(monkeypatch): - """Test is_hvac_enabled() when is_climate_on is True.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["climate_state"]["is_climate_on"] = True - _climate = Climate(_data, _controller) - - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _climate.async_update() - - assert not _climate.is_hvac_enabled() is None - assert _climate.is_hvac_enabled() - - -@pytest.mark.asyncio -async def test_is_hvac_enabled_off(monkeypatch): - """Test is_hvac_enabled() when is_climate_on is False.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["climate_state"]["is_climate_on"] = False - _climate = Climate(_data, _controller) - - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _climate.async_update() - - assert not _climate.is_hvac_enabled() is None - assert not _climate.is_hvac_enabled() - - -@pytest.mark.asyncio -async def test_set_status_on(monkeypatch): - """Test set_status() to enabled.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _climate = Climate(_data, _controller) - - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _climate.async_update() - await _climate.set_status(True) - - assert not _climate.is_hvac_enabled() is None - assert _climate.is_hvac_enabled() - - -@pytest.mark.asyncio -async def test_set_status_off(monkeypatch): - """Test set_status() to disabled.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _climate = Climate(_data, _controller) - - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _climate.async_update() - await _climate.set_status(False) - - assert not _climate.is_hvac_enabled() is None - assert not _climate.is_hvac_enabled() - - -@pytest.mark.asyncio -async def test_set_temperature(monkeypatch): - """Test set_temperature().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _climate = Climate(_data, _controller) - - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _climate.async_update() - - await _climate.set_temperature(12.3) - - assert not _climate.get_goal_temp() is None - assert _climate.get_goal_temp() == 12.3 - - -@pytest.mark.asyncio -async def test_set_preset_mode_success(monkeypatch): - """Test set_preset_mode().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _climate = Climate(_data, _controller) - - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _climate.async_update() - - preset_modes = _climate.preset_modes - for mode in preset_modes: - await _climate.set_preset_mode(mode) - assert _climate.preset_mode is not None - assert _climate.preset_mode == mode - - -@pytest.mark.asyncio -async def test_set_preset_mode_invalid_modes(monkeypatch): - """Test set_preset_mode() with invalid modes.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _climate = Climate(_data, _controller) - - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _climate.async_update() - - bad_modes = ["UKNOWN_MODE", "home", "auto", "away", "hot"] - for mode in bad_modes: - assert mode not in _climate.preset_modes - with pytest.raises(UnknownPresetMode): - await _climate.set_preset_mode(mode) diff --git a/tests/unit_tests/homeassistant/test_frunk_lock.py b/tests/unit_tests/homeassistant/test_frunk_lock.py deleted file mode 100644 index 2aef22d4..00000000 --- a/tests/unit_tests/homeassistant/test_frunk_lock.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Test frunk lock.""" - -import pytest -import time - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.trunk import FrunkLock - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - -LAST_UPDATE_TIME = time.time() - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _lock = FrunkLock(_data, _controller) - - assert not _lock.has_battery() - - -def test_is_locked_on_init(monkeypatch): - """Test is_locked() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _lock = FrunkLock(_data, _controller) - - assert _lock is not None - assert not _lock.is_locked() - - -@pytest.mark.asyncio -async def test_is_locked_after_update(monkeypatch): - """Test is_locked() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["ft"] = 0 - _lock = FrunkLock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - - assert _lock is not None - assert _lock.is_locked() - - -@pytest.mark.asyncio -async def test_unlock(monkeypatch): - """Test unlock().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["ft"] = 0 - _lock = FrunkLock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - await _lock.unlock() - - assert _lock is not None - assert not _lock.is_locked() - - -@pytest.mark.asyncio -async def test_unlock_already_unlocked(monkeypatch): - """Test unlock() when already unlocked.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["ft"] = 123 - _lock = FrunkLock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - await _lock.unlock() - - assert _lock is not None - assert not _lock.is_locked() - - # Reset to default for next tests - _data["vehicle_state"]["ft"] = 0 - - -@pytest.mark.asyncio -async def test_lock(monkeypatch): - """Test lock().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["ft"] = 123 - _lock = FrunkLock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - await _lock.lock() - - assert _lock is not None - assert _lock.is_locked() - - # Reset to default for next tests - _data["vehicle_state"]["ft"] = 0 - - -@pytest.mark.asyncio -async def test_lock_already_locked(monkeypatch): - """Test lock() when already locked.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["ft"] = 0 - _lock = FrunkLock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - await _lock.lock() - - assert _lock is not None - assert _lock.is_locked() diff --git a/tests/unit_tests/homeassistant/test_gps_tracker.py b/tests/unit_tests/homeassistant/test_gps_tracker.py deleted file mode 100644 index b1b6e9b0..00000000 --- a/tests/unit_tests/homeassistant/test_gps_tracker.py +++ /dev/null @@ -1,155 +0,0 @@ -"""Test GPS.""" - -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.gps import GPS - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _gps = GPS(_data, _controller) - - assert not _gps.has_battery() - - -def test_get_location_on_init(monkeypatch): - """Test get_location() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _gps = GPS(_data, _controller) - - _location = _gps.get_location() - assert _location is not None - assert "longitude" not in _location - assert "latitude" not in _location - assert "heading" not in _location - assert "speed" not in _location - - -@pytest.mark.asyncio -async def test_get_location_after_update(monkeypatch): - """Test get_location() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _gps = GPS(_data, _controller) - - _controller.set_drive_params(vin=VIN, params=_data["drive_state"]) - - await _gps.async_update() - _location = _gps.get_location() - - assert _location is not None - assert _location["longitude"] == -88.111111 - assert _location["latitude"] == 33.111111 - assert _location["heading"] == 5 - assert _location["speed"] == 0 - - -@pytest.mark.asyncio -async def test_get_location_native_location(monkeypatch): - """Test get_location() with native location support.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["drive_state"]["native_location_supported"] = True - _data["drive_state"]["longitude"] = 12.345 - _data["drive_state"]["native_longitude"] = 23.456 - _data["drive_state"]["latitude"] = 34.567 - _data["drive_state"]["native_latitude"] = 45.678 - _data["drive_state"]["heading"] = 12 - _data["drive_state"]["native_heading"] = 23 - _data["drive_state"]["speed"] = 23.4 - - _gps = GPS(_data, _controller) - _controller.set_drive_params(vin=VIN, params=_data["drive_state"]) - - await _gps.async_update() - _location = _gps.get_location() - - assert _location is not None - assert _location["longitude"] == 23.456 - assert _location["latitude"] == 45.678 - assert _location["heading"] == 23 - assert _location["speed"] == 23.4 - - -@pytest.mark.asyncio -async def test_get_location_no_native_location(monkeypatch): - """Test get_location() without native location support.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["drive_state"]["native_location_supported"] = False - _data["drive_state"]["longitude"] = 12.345 - _data["drive_state"]["native_longitude"] = 23.456 - _data["drive_state"]["latitude"] = 34.567 - _data["drive_state"]["native_latitude"] = 45.678 - _data["drive_state"]["heading"] = 12 - _data["drive_state"]["native_heading"] = 21 - _data["drive_state"]["speed"] = 23.4 - - _gps = GPS(_data, _controller) - - _controller.set_drive_params(vin=VIN, params=_data["drive_state"]) - - await _gps.async_update() - _location = _gps.get_location() - - assert _location is not None - assert _location["longitude"] == 12.345 - assert _location["latitude"] == 34.567 - assert _location["heading"] == 12 - assert _location["speed"] == 23.4 - - -@pytest.mark.asyncio -async def test_async_update(monkeypatch): - """Test async_update().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["drive_state"]["longitude"] = 12.345 - _data["drive_state"]["native_longitude"] = 12.345 - _data["drive_state"]["latitude"] = 34.567 - _data["drive_state"]["native_latitude"] = 34.567 - _data["drive_state"]["heading"] = 12 - _data["drive_state"]["native_heading"] = 12 - _data["drive_state"]["speed"] = 23.4 - _gps = GPS(_data, _controller) - - _controller.set_drive_params(vin=VIN, params=_data["drive_state"]) - - await _gps.async_update() - _location = _gps.get_location() - - print(_location) - - assert _location is not None - assert _location["longitude"] == 12.345 - assert _location["latitude"] == 34.567 - assert _location["heading"] == 12 - assert _location["speed"] == 23.4 diff --git a/tests/unit_tests/homeassistant/test_heated_seat.py b/tests/unit_tests/homeassistant/test_heated_seat.py deleted file mode 100644 index 55d891f9..00000000 --- a/tests/unit_tests/homeassistant/test_heated_seat.py +++ /dev/null @@ -1,111 +0,0 @@ -"""Test door HeatedSeatSwitch.""" - -import time -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.heated_seats import HeatedSeatSelect - -from tests.tesla_mock import TeslaMock, CAR_ID, VIN - -LAST_UPDATE_TIME = time.time() - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _seat = HeatedSeatSelect(_data, _controller, "left") - - assert not _seat.has_battery() - - -def test_get_seat_heat_level_on_init(monkeypatch): - """Test get_seat_heat_level() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _seat = HeatedSeatSelect(_data, _controller, "left") - - assert _seat is not None - assert not _seat.get_seat_heat_level() - - -@pytest.mark.asyncio -async def test_get_seat_heat_level_after_update(monkeypatch): - """Test get_seat_heat_level() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - new_level = 1 - - _data = _mock.data_request_vehicle() - # _data["climate_state"]['seat_heater_left'] = new_level - _seat = HeatedSeatSelect(_data, _controller, "left") - _data["climate_state"]["seat_heater_left"] = new_level - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _seat.async_update() - - assert _seat is not None - assert _seat.get_seat_heat_level() == new_level - - -@pytest.mark.asyncio -async def test_set_get_seat_heat_level(monkeypatch): - """Test HeatedSeatSelect().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - orig_level = 1 - new_level = 2 - - _data = _mock.data_request_vehicle() - # _data["climate_state"]["seat_heater_left"] = orig_level - _seat = HeatedSeatSelect(_data, _controller, "left") - - _data["climate_state"]["seat_heater_left"] = orig_level - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _seat.async_update() - - await _seat.set_seat_heat_level(new_level) - - assert _seat is not None - assert _seat.get_seat_heat_level() == new_level - - -@pytest.mark.asyncio -async def test_seat_same_level(monkeypatch): - """Test set_seat_heat_level to same level.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - orig_level = 1 - - _data = _mock.data_request_vehicle() - # _data["climate_state"]["seat_heater_left"] = orig_level - _seat = HeatedSeatSelect(_data, _controller, "left") - _data["climate_state"]["seat_heater_left"] = orig_level - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _seat.async_update() - - await _seat.set_seat_heat_level(orig_level) - - assert _seat is not None - assert _seat.get_seat_heat_level() == orig_level diff --git a/tests/unit_tests/homeassistant/test_heated_steering_wheel.py b/tests/unit_tests/homeassistant/test_heated_steering_wheel.py deleted file mode 100644 index 78d30dca..00000000 --- a/tests/unit_tests/homeassistant/test_heated_steering_wheel.py +++ /dev/null @@ -1,112 +0,0 @@ -"""Test HeatedSteeringWheelSwitch.""" - -import time -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.heated_steering_wheel import HeatedSteeringWheelSwitch - -from tests.tesla_mock import TeslaMock, CAR_ID, VIN - -LAST_UPDATE_TIME = time.time() - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _seat = HeatedSteeringWheelSwitch(_data, _controller) - - assert not _seat.has_battery() - - -def test_get_steering_wheel_heat_on_init(monkeypatch): - """Test get_steering_wheel_heat() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _wheel = HeatedSteeringWheelSwitch(_data, _controller) - - assert _wheel is not None - assert not _wheel.get_steering_wheel_heat() - - -@pytest.mark.asyncio -async def test_get_steering_wheel_heat_after_update(monkeypatch): - """Test get_steering_wheel_heat() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - new_level = True - - _data = _mock.data_request_vehicle() - # _data["climate_state"]['steering_wheel_heater'] = new_level - _seat = HeatedSteeringWheelSwitch(_data, _controller) - - _data["climate_state"]["steering_wheel_heater"] = new_level - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _seat.async_update() - - assert _seat is not None - assert _seat.get_steering_wheel_heat() == new_level - - -@pytest.mark.asyncio -async def test_set_get_seat_heat_level(monkeypatch): - """Test HeatedSteeringWheelSwitch().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - orig_level = True - new_level = False - - _data = _mock.data_request_vehicle() - # _data["climate_state"]["steering_wheel_heater"] = orig_level - _seat = HeatedSteeringWheelSwitch(_data, _controller) - - _data["climate_state"]["steering_wheel_heater"] = orig_level - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _seat.async_update() - - await _seat.set_steering_wheel_heat(new_level) - - assert _seat is not None - assert _seat.get_steering_wheel_heat() == new_level - - -@pytest.mark.asyncio -async def test_seat_same_level(monkeypatch): - """Test set_steering_wheel_heat to same level.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - orig_level = True - - _data = _mock.data_request_vehicle() - _data["climate_state"]["steering_wheel_heater"] = orig_level - _seat = HeatedSteeringWheelSwitch(_data, _controller) - - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _seat.async_update() - - await _seat.set_steering_wheel_heat(orig_level) - - assert _seat is not None - assert _seat.get_steering_wheel_heat() == orig_level diff --git a/tests/unit_tests/homeassistant/test_helper_functions.py b/tests/unit_tests/homeassistant/test_helper_functions.py deleted file mode 100644 index d2b1a11b..00000000 --- a/tests/unit_tests/homeassistant/test_helper_functions.py +++ /dev/null @@ -1,182 +0,0 @@ -"""Test helper functions.""" - -import time - -from teslajsonpy.controller import Controller - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - -CAR_PARKED = 1577833200 # Timestamp a long time ago -NOW = time.time() - - -def test_climate_params(monkeypatch): - """Test set/get climate params.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - - # monkeypatch.setitem(_controller.car_online, VIN, True) - # monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - _controller.set_id_vin(CAR_ID, VIN) - assert _controller.get_climate_params() == {} - assert _controller.get_climate_params(vin=VIN) == {} - - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - # print(_controller.get_climate_params()) - assert _controller.get_climate_params() == {VIN: _data["climate_state"]} - assert _controller.get_climate_params(vin=VIN) == _data["climate_state"] - assert _controller.is_climate_on(vin=VIN) is False - - _data["climate_state"]["is_climate_on"] = True - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - assert _controller.is_climate_on(vin=VIN) is True - - -def test_charging_params(monkeypatch): - """Test set/get charging params.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - assert _controller.get_charging_params() == {} - assert _controller.get_charging_params(vin=VIN) == {} - - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - assert _controller.get_charging_params() == {VIN: _data["charge_state"]} - assert _controller.get_charging_params(vin=VIN) == _data["charge_state"] - assert _controller.charging_state(vin=VIN) == "Disconnected" - - _data["charge_state"]["charging_state"] = "Charging" - _controller.set_charging_params(vin=VIN, params=_data["charge_state"]) - - assert _controller.charging_state(vin=VIN) == "Charging" - - -def test_state_params(monkeypatch): - """Test set/get state params.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - assert _controller.get_state_params() == {} - assert _controller.get_state_params(vin=VIN) == {} - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - assert _controller.get_state_params() == {VIN: _data["vehicle_state"]} - assert _controller.get_state_params(vin=VIN) == _data["vehicle_state"] - - -def test_drive_params(monkeypatch): - """Test set/get drive params.""" - - _mock = TeslaMock(monkeypatch) - _data = _mock.data_request_vehicle() - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - assert _controller.get_drive_params() == {} - assert _controller.get_drive_params(vin=VIN) == {} - - _controller.set_drive_params(vin=VIN, params=_data["drive_state"]) - - assert _controller.get_drive_params() == {VIN: _data["drive_state"]} - assert _controller.get_drive_params(vin=VIN) == _data["drive_state"] - assert _controller.is_in_gear(vin=VIN) is False - - _data["drive_state"]["shift_state"] = "D" - _controller.set_drive_params(vin=VIN, params=_data["drive_state"]) - - assert _controller.is_in_gear(vin=VIN) is True - assert _controller.shift_state(vin=VIN) == "D" - - -def test_updates_helper(): - """Test set/get updates available.""" - - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - assert _controller.get_updates() == {} - assert _controller.get_updates(vin=VIN) == {} - - _controller.set_updates(vin=VIN, value=True) - assert _controller.get_updates(vin=VIN) is True - - _controller.set_updates(vin=VIN, value=False) - assert _controller.get_updates(vin=VIN) is False - - -def test_last_update_time(): - """Test set/get last_update_time.""" - - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - assert _controller.get_last_update_time() == {} - assert _controller.get_last_update_time(vin=VIN) == {} - - _controller.set_last_update_time(vin=VIN, timestamp=CAR_PARKED) - - assert _controller.get_last_update_time() == {VIN: CAR_PARKED} - assert _controller.get_last_update_time(vin=VIN) == CAR_PARKED - - -def test_last_park_time(): - """Test set/get last_park_time.""" - - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - assert _controller.get_last_park_time() == {} - assert _controller.get_last_park_time(vin=VIN) == {} - - _controller.set_last_park_time(vin=VIN, timestamp=CAR_PARKED) - - assert _controller.get_last_park_time() == {VIN: CAR_PARKED} - assert _controller.get_last_park_time(vin=VIN) == CAR_PARKED - - -def test_last_wake_up_time(): - """Test set/get last_wake_up_time.""" - - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - assert _controller.get_last_wake_up_time() == {} - assert _controller.get_last_wake_up_time(vin=VIN) == {} - - _controller.set_last_wake_up_time(vin=VIN, timestamp=CAR_PARKED) - - assert _controller.get_last_wake_up_time() == {VIN: CAR_PARKED} - assert _controller.get_last_wake_up_time(vin=VIN) == CAR_PARKED - - -def test_set_car_online(): - """Test set/get car_online.""" - - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - assert _controller.get_car_online() == {} - assert _controller.get_car_online(vin=VIN) == {} - assert _controller.is_car_online(vin=VIN) == {} - - _controller.set_car_online(vin=VIN) - - assert _controller.is_car_online(vin=VIN) is True - last_wake_up = _controller.get_last_wake_up_time(vin=VIN) - - assert int(last_wake_up) >= int(NOW) - - _controller.set_car_online(vin=VIN, online_status=False) - assert _controller.is_car_online(vin=VIN) is False - assert _controller.get_last_wake_up_time(vin=VIN) == last_wake_up diff --git a/tests/unit_tests/homeassistant/test_homelink.py b/tests/unit_tests/homeassistant/test_homelink.py deleted file mode 100644 index bcaf2968..00000000 --- a/tests/unit_tests/homeassistant/test_homelink.py +++ /dev/null @@ -1,192 +0,0 @@ -"""Test homelink button.""" - -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.exceptions import HomelinkError -from teslajsonpy.homeassistant.homelink import TriggerHomelink - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _button = TriggerHomelink(_data, _controller) - - assert not _button.has_battery() - - -@pytest.mark.asyncio -async def test_trigger_homelink(monkeypatch): - """Test test_trigger_homelink().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["drive_state"]["longitude"] = 12.345 - _data["drive_state"]["native_longitude"] = 12.345 - _data["drive_state"]["latitude"] = 34.567 - _data["drive_state"]["native_latitude"] = 34.567 - _data["vehicle_state"]["homelink_device_count"] = 1 - _data["vehicle_state"]["homelink_nearby"] = True - _button = TriggerHomelink(_data, _controller) - - assert _button.enabled_by_default is False - - _controller.set_drive_params(vin=VIN, params=_data["drive_state"]) - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - await _button.async_update() - - await _button.trigger_homelink() - - -@pytest.mark.asyncio -async def test_available(monkeypatch): - """Test available().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _button = TriggerHomelink(_data, _controller) - - assert not _button.available() - - _test_set = [None, 0, 1, 2] - - for _count in _test_set: - _data["vehicle_state"]["homelink_device_count"] = _count - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - await _button.async_update() - assert _button.available() == bool(_count) - - -@pytest.mark.asyncio -async def test_available_no_keys(monkeypatch): - """Test available() when there are no homelink keys.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _button = TriggerHomelink(_data, _controller) - - del _data["vehicle_state"]["homelink_device_count"] - del _data["vehicle_state"]["homelink_nearby"] - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _button.async_update() - assert not _button.available() - - -@pytest.mark.asyncio -async def test_homelink_error_device_count(monkeypatch): - """Test HomelinkError for no device count.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["drive_state"]["longitude"] = 12.345 - _data["drive_state"]["native_longitude"] = 12.345 - _data["drive_state"]["latitude"] = 34.567 - _data["drive_state"]["native_latitude"] = 34.567 - _data["vehicle_state"]["homelink_device_count"] = 0 - _data["vehicle_state"]["homelink_nearby"] = False - _button = TriggerHomelink(_data, _controller) - - _controller.set_drive_params(vin=VIN, params=_data["drive_state"]) - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _button.async_update() - - with pytest.raises(HomelinkError) as excinfo: - await _button.trigger_homelink() - assert ( - excinfo.value.message == f"No homelink devices added to {_button.car_name()}." - ) - - -@pytest.mark.asyncio -async def test_homelink_error_device_nearby(monkeypatch): - """Test HomelinkError for no device nearby.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["drive_state"]["longitude"] = 12.345 - _data["drive_state"]["native_longitude"] = 12.345 - _data["drive_state"]["latitude"] = 34.567 - _data["drive_state"]["native_latitude"] = 34.567 - _data["vehicle_state"]["homelink_device_count"] = 1 - _data["vehicle_state"]["homelink_nearby"] = False - _button = TriggerHomelink(_data, _controller) - - _controller.set_drive_params(vin=VIN, params=_data["drive_state"]) - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _button.async_update() - - with pytest.raises(HomelinkError) as excinfo: - await _button.trigger_homelink() - assert excinfo.value.message == f"No homelink devices near {_button.car_name()}." - - -@pytest.mark.asyncio -async def test_native_location(monkeypatch): - """Test native location values are set correctly.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["drive_state"]["native_location_supported"] = 1 - _data["drive_state"]["longitude"] = 12.345 - _data["drive_state"]["native_longitude"] = 23.456 - _data["drive_state"]["latitude"] = 34.567 - _data["drive_state"]["native_latitude"] = 45.678 - _button = TriggerHomelink(_data, _controller) - - _controller.set_drive_params(vin=VIN, params=_data["drive_state"]) - - await _button.async_update() - - assert _button._longitude == 23.456 - assert _button._latitude == 45.678 - - -@pytest.mark.asyncio -async def test_location(monkeypatch): - """Test non-native location values are set correctly.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["drive_state"]["native_location_supported"] = 0 - _data["drive_state"]["longitude"] = 12.345 - _data["drive_state"]["native_longitude"] = 23.456 - _data["drive_state"]["latitude"] = 34.567 - _data["drive_state"]["native_latitude"] = 45.678 - _button = TriggerHomelink(_data, _controller) - - _controller.set_drive_params(vin=VIN, params=_data["drive_state"]) - - await _button.async_update() - - assert _button._longitude == 12.345 - assert _button._latitude == 34.567 diff --git a/tests/unit_tests/homeassistant/test_lock.py b/tests/unit_tests/homeassistant/test_lock.py deleted file mode 100644 index 22860ca2..00000000 --- a/tests/unit_tests/homeassistant/test_lock.py +++ /dev/null @@ -1,145 +0,0 @@ -"""Test door lock.""" - -import pytest -import time - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.lock import Lock - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - -LAST_UPDATE_TIME = time.time() - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _lock = Lock(_data, _controller) - - assert not _lock.has_battery() - - -def test_is_locked_on_init(monkeypatch): - """Test is_locked() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _lock = Lock(_data, _controller) - - assert _lock is not None - assert not _lock.is_locked() - - -@pytest.mark.asyncio -async def test_is_locked_after_update(monkeypatch): - """Test is_locked() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["locked"] = True - _lock = Lock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - - assert _lock is not None - assert _lock.is_locked() - - -@pytest.mark.asyncio -async def test_lock(monkeypatch): - """Test lock().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["locked"] = False - _lock = Lock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - await _lock.lock() - - assert _lock is not None - assert _lock.is_locked() - - -@pytest.mark.asyncio -async def test_lock_already_locked(monkeypatch): - """Test lock() when already locked.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["locked"] = True - _lock = Lock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - await _lock.lock() - - assert _lock is not None - assert _lock.is_locked() - - -@pytest.mark.asyncio -async def test_unlock(monkeypatch): - """Test unlock().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["locked"] = True - _lock = Lock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - await _lock.unlock() - - assert _lock is not None - assert not _lock.is_locked() - - -@pytest.mark.asyncio -async def test_unlock_already_unlocked(monkeypatch): - """Test unlock() when already unlocked.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["locked"] = False - _lock = Lock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - await _lock.unlock() - - assert _lock is not None - assert not _lock.is_locked() diff --git a/tests/unit_tests/homeassistant/test_odometer_sensor.py b/tests/unit_tests/homeassistant/test_odometer_sensor.py deleted file mode 100644 index c08cab13..00000000 --- a/tests/unit_tests/homeassistant/test_odometer_sensor.py +++ /dev/null @@ -1,131 +0,0 @@ -"""Test odometer sensor.""" - -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.gps import Odometer - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _odometer = Odometer(_data, _controller) - - assert not _odometer.has_battery() - - -def test_device_class(monkeypatch): - """Test device_class().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _odometer = Odometer(_data, _controller) - - assert _odometer.device_class is None - - -def test_get_value_on_init(monkeypatch): - """Test get_value() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _odometer = Odometer(_data, _controller) - - assert _odometer is not None - assert _odometer.get_value() is None - - -@pytest.mark.asyncio -async def test_get_value_after_update(monkeypatch): - """Test get_value() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _odometer = Odometer(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _odometer.async_update() - - assert _odometer is not None - assert _odometer.get_value() == 33561.4 - - -@pytest.mark.asyncio -async def test_async_update(monkeypatch): - """Test async_update().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["odometer"] = 12345.6789 - _odometer = Odometer(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _odometer.async_update() - - assert _odometer is not None - assert _odometer.get_value() is not None - assert _odometer.get_value() == 12345.7 - - -@pytest.mark.asyncio -async def test_async_update_in_kmh(monkeypatch): - """Test async_update() for units in km/h.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["gui_settings"]["gui_distance_units"] = "km/hr" - _data["vehicle_state"]["odometer"] = 12345.6789 - _odometer = Odometer(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - _controller.set_gui_params(vin=VIN, params=_data["gui_settings"]) - - await _odometer.async_update() - - assert _odometer is not None - assert _odometer.get_value() is not None - assert _odometer.get_value() == 12345.7 - - -@pytest.mark.asyncio -async def test_async_update_in_mph(monkeypatch): - """Test async_update() for units in mph.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["gui_settings"]["gui_distance_units"] = "mi/hr" - _data["vehicle_state"]["odometer"] = 12345.6789 - _odometer = Odometer(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - _controller.set_gui_params(vin=VIN, params=_data["gui_settings"]) - - await _odometer.async_update() - - assert _odometer is not None - assert _odometer.get_value() is not None - assert _odometer.get_value() == 12345.7 diff --git a/tests/unit_tests/homeassistant/test_online_sensor.py b/tests/unit_tests/homeassistant/test_online_sensor.py deleted file mode 100644 index e104c36c..00000000 --- a/tests/unit_tests/homeassistant/test_online_sensor.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Test online sensor.""" - -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.binary_sensor import OnlineSensor - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - -# VIN = "5YJSA11111111111" - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = OnlineSensor(_data, _controller) - - assert not _sensor.has_battery() - - -def test_get_value_on_init(monkeypatch): - """Test get_value() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = OnlineSensor(_data, _controller) - - assert _sensor is not None - assert _sensor.get_value() is None - - -@pytest.mark.asyncio -async def test_get_value_after_update(monkeypatch): - """Test get_value() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - - _data = _mock.data_request_vehicle() - _sensor = OnlineSensor(_data, _controller) - - await _sensor.async_update() - - assert _sensor is not None - assert not _sensor.get_value() is None - assert _sensor.get_value() - - -@pytest.mark.asyncio -async def test_get_value_on(monkeypatch): - """Test get_value() for online mode.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - - _data = _mock.data_request_vehicle() - _sensor = OnlineSensor(_data, _controller) - _data["state"] = "online" - - await _sensor.async_update() - - assert _sensor is not None - assert not _sensor.get_value() is None - assert _sensor.get_value() - assert _sensor.attrs == { - "state": "online", - "vin": VIN, - "id": 12345678901234567, - "vehicle_id": 1234567890, - "update_interval": 300, - } - - -@pytest.mark.asyncio -async def test_get_value_off(monkeypatch): - """Test get_value() for offline mode.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - monkeypatch.setitem(_controller.car_online, VIN, False) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) - - _data = _mock.data_request_vehicle() - _sensor = OnlineSensor(_data, _controller) - _data["state"] = "asleep" - - await _sensor.async_update() - - assert _sensor is not None - assert not _sensor.get_value() is None - assert not _sensor.get_value() - assert _sensor.attrs == { - "state": "asleep", - "vin": VIN, - "id": 12345678901234567, - "vehicle_id": 1234567890, - "update_interval": 300, - } diff --git a/tests/unit_tests/homeassistant/test_parking_sensor.py b/tests/unit_tests/homeassistant/test_parking_sensor.py deleted file mode 100644 index 25c5a4ae..00000000 --- a/tests/unit_tests/homeassistant/test_parking_sensor.py +++ /dev/null @@ -1,95 +0,0 @@ -"""Test parking sensor.""" - -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.binary_sensor import ParkingSensor - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = ParkingSensor(_data, _controller) - - assert not _sensor.has_battery() - - -def test_get_value_on_init(monkeypatch): - """Test get_value() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = ParkingSensor(_data, _controller) - - assert _sensor is not None - assert _sensor.get_value() is None - - -@pytest.mark.asyncio -async def test_get_value_after_update(monkeypatch): - """Test get_value() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = ParkingSensor(_data, _controller) - - _controller.set_drive_params(vin=VIN, params=_data["drive_state"]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.get_value() is not None - assert _sensor.get_value() - - -@pytest.mark.asyncio -async def test_get_value_on(monkeypatch): - """Test get_value() for parking mode ON.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = ParkingSensor(_data, _controller) - - _data["drive_state"]["shift_state"] = "P" - _controller.set_drive_params(vin=VIN, params=_data["drive_state"]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.get_value() is not None - assert _sensor.get_value() - - -@pytest.mark.asyncio -async def test_get_value_off(monkeypatch): - """Test get_value() for parking mode OFF.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = ParkingSensor(_data, _controller) - - _data["drive_state"]["shift_state"] = "N" - _controller.set_drive_params(vin=VIN, params=_data["drive_state"]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.get_value() is not None - assert not _sensor.get_value() diff --git a/tests/unit_tests/homeassistant/test_power_sensor.py b/tests/unit_tests/homeassistant/test_power_sensor.py deleted file mode 100644 index d7440ca9..00000000 --- a/tests/unit_tests/homeassistant/test_power_sensor.py +++ /dev/null @@ -1,148 +0,0 @@ -"""Test power sensor.""" - -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.power import ( - SolarPowerSensor, - LoadPowerSensor, - GridPowerSensor, - BatteryPowerSensor, -) - -from tests.tesla_mock import TeslaMock - - -@pytest.mark.asyncio -async def test_energysite_setup(monkeypatch): - """Test setup of energysites in Controller.connect().""" - TeslaMock(monkeypatch) - _controller = Controller(None) - await _controller.connect() - - solar_site = _controller.energysites[12345] - powerwall_site = _controller.energysites[67890] - - assert _controller.energysites is not None - assert solar_site.resource_type == "solar" - assert powerwall_site.resource_type == "battery" - - -@pytest.mark.asyncio -async def test_solar_power_sensor(monkeypatch): - """Test SolarPowerSensor class.""" - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - # Test a solar only site (no Powerwall) - _data = _mock.data_request_solar_combined_data() - _sensor = SolarPowerSensor(_data, _controller) - - assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" - assert _sensor.get_power() == 7720 - # Test a battery site (Powerwall) - _data = _mock.data_request_battery_combined_data() - _sensor = SolarPowerSensor(_data, _controller) - - assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" - assert _sensor.get_power() == 7720 - - -@pytest.mark.asyncio -async def test_load_power_sensor(monkeypatch): - """Test LoadPowerSensor class.""" - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - # Test a solar only site (no Powerwall) - _data = _mock.data_request_solar_combined_data() - _sensor = LoadPowerSensor(_data, _controller) - - assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" - assert _sensor.get_power() == 4517.14990234375 - # Test a battery site (Powerwall) - _data = _mock.data_request_battery_combined_data() - _sensor = LoadPowerSensor(_data, _controller) - - assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" - assert _sensor.get_power() == 4517.14990234375 - - -@pytest.mark.asyncio -async def test_grid_power_sensor(monkeypatch): - """Test GridPowerSensor class.""" - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - # Test a solar only site (no Powerwall) - _data = _mock.data_request_solar_combined_data() - _sensor = GridPowerSensor(_data, _controller) - - assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" - assert _sensor.get_power() == -3202.85009765625 - # Test a battery site (Powerwall) - _data = _mock.data_request_battery_combined_data() - _sensor = GridPowerSensor(_data, _controller) - - assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" - assert _sensor.get_power() == -3202.85009765625 - - -@pytest.mark.asyncio -async def test_battery_power_sensor(monkeypatch): - """Test BatteryPowerSensor class.""" - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _data = _mock.data_request_battery_combined_data() - _sensor = BatteryPowerSensor(_data, _controller) - - assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" - assert _sensor.get_power() == 0 - - -def test_site_without_name(monkeypatch): - """Test site with no site_name in json data.""" - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _data = _mock.data_request_solar_combined_data_no_name() - _sensor = LoadPowerSensor(_data, _controller) - - assert _sensor.site_name() == "My Home" - - -@pytest.mark.asyncio -async def test_get_power_after_update(monkeypatch): - """Test get_power() after an update.""" - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _data = _mock.data_request_solar_combined_data() - _data["solar_power"] = 1800 - _data["load_power"] = 1800 - _data["grid_power"] = 1800 - - _sensor = SolarPowerSensor(_data, _controller) - - await _sensor.async_update() - assert _sensor.get_power() == 7720 - - _sensor = LoadPowerSensor(_data, _controller) - - await _sensor.async_update() - assert _sensor.get_power() == 4517.14990234375 - - _sensor = GridPowerSensor(_data, _controller) - - await _sensor.async_update() - assert _sensor.get_power() == -3202.85009765625 - - -@pytest.mark.asyncio -async def test_get_power_after_update_with_unknown_status(monkeypatch): - """Test get_power() after an update with unknown grid status.""" - _mock = TeslaMock(monkeypatch) - monkeypatch.setattr( - Controller, "get_power_params", _mock.mock_get_power_unknown_grid_params - ) - _controller = Controller(None) - _data = _mock.data_request_solar_combined_data() - _sensor = SolarPowerSensor(_data, _controller) - - await _sensor.async_update() - assert _sensor.get_power() == 1750 diff --git a/tests/unit_tests/homeassistant/test_range_sensor.py b/tests/unit_tests/homeassistant/test_range_sensor.py deleted file mode 100644 index cebc80cd..00000000 --- a/tests/unit_tests/homeassistant/test_range_sensor.py +++ /dev/null @@ -1,192 +0,0 @@ -"""Test range sensor.""" - -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.battery_sensor import Range - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _range = Range(_data, _controller) - - assert not _range.has_battery() - - -def test_device_class(monkeypatch): - """Test device_class().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _range = Range(_data, _controller) - - assert _range.device_class is None - - -def test_get_value_on_init(monkeypatch): - """Test get_value() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _range = Range(_data, _controller) - - assert _range is not None - assert _range.get_value() is None - - -@pytest.mark.asyncio -async def test_get_value_after_update(monkeypatch): - """Test get_value() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _range = Range(_data, _controller) - - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - - await _range.async_update() - - assert _range is not None - assert _range.get_value() is not None - assert _range.get_value() == 167.96 - - -@pytest.mark.asyncio -async def test_get_value_rated_on(monkeypatch): - """Test get_value() for range display 'Rated'.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _range = Range(_data, _controller) - _data["gui_settings"]["gui_range_display"] = "Rated" - _data["charge_state"]["battery_range"] = 123.45 - _data["charge_state"]["est_battery_range"] = 234.56 - _data["charge_state"]["ideal_battery_range"] = 345.67 - - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_gui_params(car_id=CAR_ID, params=_data["gui_settings"]) - - await _range.async_update() - - assert _range is not None - assert _range.get_value() is not None - assert _range.get_value() == 123.45 - - -@pytest.mark.asyncio -async def test_get_value_rated_off(monkeypatch): - """Test get_value() for range display not 'Rated'.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _range = Range(_data, _controller) - _data["gui_settings"]["gui_range_display"] = "Other" - _data["charge_state"]["battery_range"] = 123.45 - _data["charge_state"]["est_battery_range"] = 234.56 - _data["charge_state"]["ideal_battery_range"] = 345.67 - - _controller.set_gui_params(car_id=CAR_ID, params=_data["gui_settings"]) - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - - await _range.async_update() - - assert _range is not None - assert _range.get_value() is not None - assert _range.get_value() == 345.67 - - -@pytest.mark.asyncio -async def test_get_value_in_kmh(monkeypatch): - """Test get_value() for units in km/h'.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _range = Range(_data, _controller) - _data["gui_settings"]["gui_distance_units"] = "km/hr" - _data["gui_settings"]["gui_range_display"] = "Rated" - _data["charge_state"]["battery_range"] = 123.45 - _data["charge_state"]["est_battery_range"] = 234.56 - _data["charge_state"]["ideal_battery_range"] = 345.67 - - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_gui_params(car_id=CAR_ID, params=_data["gui_settings"]) - - await _range.async_update() - - assert _range is not None - assert _range.get_value() is not None - assert _range.get_value() == 123.45 - - -@pytest.mark.asyncio -async def test_get_value_in_mph(monkeypatch): - """Test get_value() for units in mph'.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _range = Range(_data, _controller) - _data["gui_settings"]["gui_distance_units"] = "mi/hr" - _data["gui_settings"]["gui_range_display"] = "Rated" - _data["charge_state"]["battery_range"] = 123.45 - _data["charge_state"]["est_battery_range"] = 234.56 - _data["charge_state"]["ideal_battery_range"] = 345.67 - - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_gui_params(car_id=CAR_ID, params=_data["gui_settings"]) - - await _range.async_update() - - assert _range is not None - assert _range.get_value() is not None - assert _range.get_value() == 123.45 - - -@pytest.mark.asyncio -async def test_async_update(monkeypatch): - """Test async_update().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _data["gui_settings"]["gui_range_display"] = "Rated" - _data["charge_state"]["battery_range"] = 123.45 - _data["charge_state"]["est_battery_range"] = 234.56 - _data["charge_state"]["ideal_battery_range"] = 345.67 - _range = Range(_data, _controller) - - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - _controller.set_gui_params(car_id=CAR_ID, params=_data["gui_settings"]) - - await _range.async_update() - - assert _range is not None - assert _range.get_value() is not None - assert _range.get_value() == 123.45 diff --git a/tests/unit_tests/homeassistant/test_range_switch.py b/tests/unit_tests/homeassistant/test_range_switch.py deleted file mode 100644 index cba86d40..00000000 --- a/tests/unit_tests/homeassistant/test_range_switch.py +++ /dev/null @@ -1,155 +0,0 @@ -"""Test range switch.""" - -import pytest -import time - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.charger import RangeSwitch - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - -LAST_UPDATE_TIME = time.time() - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _switch = RangeSwitch(_data, _controller) - - assert not _switch.has_battery() - - -def test_is_maxrange_on_init(monkeypatch): - """Test is_maxrange() when not charging.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _switch = RangeSwitch(_data, _controller) - - assert not _switch.is_maxrange() - - -@pytest.mark.asyncio -async def test_is_maxrange_on(monkeypatch): - """Test is_maxrange() with charging state charging.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charge_to_max_range"] = True - _switch = RangeSwitch(_data, _controller) - - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - - await _switch.async_update() - assert _switch.is_maxrange() - - -@pytest.mark.asyncio -async def test_is_maxrange_off(monkeypatch): - """Test is_maxrange() with charging state disconnected.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charge_to_max_range"] = False - _switch = RangeSwitch(_data, _controller) - - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - - await _switch.async_update() - assert not _switch.is_maxrange() - - -@pytest.mark.asyncio -async def test_set_max(monkeypatch): - """Test set_max().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charge_to_max_range"] = False - _switch = RangeSwitch(_data, _controller) - - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - - await _switch.async_update() - - await _switch.set_max() - assert _switch.is_maxrange() - - -@pytest.mark.asyncio -async def test_set_standard(monkeypatch): - """Test set_standard().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charge_to_max_range"] = True - _switch = RangeSwitch(_data, _controller) - - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - - await _switch.async_update() - - await _switch.set_standard() - assert not _switch.is_maxrange() - - -@pytest.mark.asyncio -async def test_async_update(monkeypatch): - """Test async_update().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charge_to_max_range"] = True - _switch = RangeSwitch(_data, _controller) - - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - - await _switch.async_update() - assert _switch.is_maxrange() - - -@pytest.mark.asyncio -async def test_async_update_with_change(monkeypatch): - """Test async_update() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["charge_state"]["charge_to_max_range"] = True - _switch = RangeSwitch(_data, _controller) - - _data["charge_state"]["charge_to_max_range"] = False - - _controller.set_charging_params(car_id=CAR_ID, params=_data["charge_state"]) - - await _switch.async_update() - assert not _switch.is_maxrange() diff --git a/tests/unit_tests/homeassistant/test_sentry_mode_switch.py b/tests/unit_tests/homeassistant/test_sentry_mode_switch.py deleted file mode 100644 index 80d9b53d..00000000 --- a/tests/unit_tests/homeassistant/test_sentry_mode_switch.py +++ /dev/null @@ -1,320 +0,0 @@ -"""Test sentry mode switch.""" - -import pytest -import time - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.sentry_mode import SentryModeSwitch - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - -LAST_UPDATE_TIME = time.time() - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _switch = SentryModeSwitch(_data, _controller) - - assert not _switch.has_battery() - - -def test_available_true(monkeypatch): - """Test available() when flag sentry_mode_available is false.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = True - _switch = SentryModeSwitch(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - assert _switch.available() - - -def test_available_false(monkeypatch): - """Test available() when flag sentry_mode_available is false.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = False - _switch = SentryModeSwitch(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - assert not _switch.available() - - -def test_is_on_false(monkeypatch): - """Test is_on() when flag sentry_mode is false.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = True - _data["vehicle_state"]["sentry_mode"] = False - _switch = SentryModeSwitch(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - assert not _switch.is_on() - - -def test_is_on_true(monkeypatch): - """Test is_on() when flag sentry_mode is true.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = True - _data["vehicle_state"]["sentry_mode"] = True - _switch = SentryModeSwitch(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - assert _switch.is_on() - - -def test_is_on_unavailable(monkeypatch): - """Test is_on() when flag sentry_mode_available is false.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = False - _data["vehicle_state"]["sentry_mode"] = True - _switch = SentryModeSwitch(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - assert not _switch.is_on() - - -@pytest.mark.asyncio -async def test_enable_sentry_mode(monkeypatch): - """Test enable_sentry_mode().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = True - _data["vehicle_state"]["sentry_mode"] = False - _switch = SentryModeSwitch(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _switch.enable_sentry_mode() - assert _switch.is_on() - - -@pytest.mark.asyncio -async def test_enable_sentry_mode_already_enabled(monkeypatch): - """Test enable_sentry_mode() when already enabled.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = True - _data["vehicle_state"]["sentry_mode"] = True - _switch = SentryModeSwitch(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _switch.enable_sentry_mode() - assert _switch.is_on() - - -@pytest.mark.asyncio -async def test_enable_sentry_mode_not_available(monkeypatch): - """Test enable_sentry_mode() when sentry mode is not available.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = False - _data["vehicle_state"]["sentry_mode"] = False - _switch = SentryModeSwitch(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _switch.enable_sentry_mode() - assert not _switch.is_on() - - -@pytest.mark.asyncio -async def test_disable_sentry_mode(monkeypatch): - """Test disable_sentry_mode().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = True - _data["vehicle_state"]["sentry_mode"] = True - _switch = SentryModeSwitch(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _switch.disable_sentry_mode() - assert not _switch.is_on() - - -@pytest.mark.asyncio -async def test_disable_sentry_mode_already_disabled(monkeypatch): - """Test disable_sentry_mode() when already disabled.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = True - _data["vehicle_state"]["sentry_mode"] = False - _switch = SentryModeSwitch(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _switch.disable_sentry_mode() - assert not _switch.is_on() - - -@pytest.mark.asyncio -async def test_disable_sentry_mode_not_available(monkeypatch): - """Test disable_sentry_mode() when sentry mode is not available.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = False - _data["vehicle_state"]["sentry_mode"] = False - _switch = SentryModeSwitch(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _switch.enable_sentry_mode() - assert not _switch.is_on() - - -@pytest.mark.asyncio -async def test_async_update(monkeypatch): - """Test async_update().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = True - _data["vehicle_state"]["sentry_mode"] = False - _switch = SentryModeSwitch(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _switch.async_update() - assert not _switch.is_on() - - -@pytest.mark.asyncio -async def test_async_update_with_change(monkeypatch): - """Test async_update() with a state change.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = True - _data["vehicle_state"]["sentry_mode"] = False - _switch = SentryModeSwitch(_data, _controller) - - # Change state value - _data["vehicle_state"]["sentry_mode"] = True - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _switch.async_update() - assert _switch.is_on() - - -@pytest.mark.asyncio -async def test_async_update_with_change_but_not_available(monkeypatch): - """Test async_update() with a state change, but sentry mode is not available.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = False - _data["vehicle_state"]["sentry_mode"] = False - _switch = SentryModeSwitch(_data, _controller) - - # Change state value - _data["vehicle_state"]["sentry_mode"] = True - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _switch.async_update() - assert not _switch.is_on() - - -@pytest.mark.asyncio -async def test_async_update_with_change_same_value(monkeypatch): - """Test async_update() with a state change, using same value.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["sentry_mode_available"] = True - _data["vehicle_state"]["sentry_mode"] = True - _switch = SentryModeSwitch(_data, _controller) - - # Change state value - _data["vehicle_state"]["sentry_mode"] = True - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _switch.async_update() - assert _switch.is_on() diff --git a/tests/unit_tests/homeassistant/test_temp_sensor.py b/tests/unit_tests/homeassistant/test_temp_sensor.py deleted file mode 100644 index 01589c60..00000000 --- a/tests/unit_tests/homeassistant/test_temp_sensor.py +++ /dev/null @@ -1,70 +0,0 @@ -"""Test temp sensor.""" - -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.climate import TempSensor - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = TempSensor(_data, _controller) - - assert not _sensor.has_battery() - - -def test_device_class(monkeypatch): - """Test device_class().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = TempSensor(_data, _controller) - - assert _sensor.device_class == "temperature" - - -def test_get_temp_on_init(monkeypatch): - """Test get_inside_temp() and get_outside_temp() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = TempSensor(_data, _controller) - - assert _sensor is not None - assert _sensor.get_inside_temp() is None - assert _sensor.get_outside_temp() is None - - -@pytest.mark.asyncio -async def test_get_temp_after_update(monkeypatch): - """Test get_inside_temp() and get_outside_temp() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = TempSensor(_data, _controller) - - _data["climate_state"]["inside_temp"] = 18.8 - _data["climate_state"]["outside_temp"] = 22.2 - _controller.set_climate_params(vin=VIN, params=_data["climate_state"]) - - await _sensor.async_update() - - assert _sensor is not None - assert not _sensor.get_inside_temp() is None - assert not _sensor.get_outside_temp() is None - assert _sensor.get_inside_temp() == 18.8 - assert _sensor.get_outside_temp() == 22.2 diff --git a/tests/unit_tests/homeassistant/test_trunk_lock.py b/tests/unit_tests/homeassistant/test_trunk_lock.py deleted file mode 100644 index c4ded1bd..00000000 --- a/tests/unit_tests/homeassistant/test_trunk_lock.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Test trunk lock.""" - -import pytest -import time - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.trunk import TrunkLock - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - -LAST_UPDATE_TIME = time.time() - - -def test_has_battery(monkeypatch): - """Test has_battery().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _lock = TrunkLock(_data, _controller) - - assert not _lock.has_battery() - - -def test_is_locked_on_init(monkeypatch): - """Test is_locked() after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _lock = TrunkLock(_data, _controller) - - assert _lock is not None - assert not _lock.is_locked() - - -@pytest.mark.asyncio -async def test_is_locked_after_update(monkeypatch): - """Test is_locked() after an update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["rt"] = 0 - _lock = TrunkLock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - - assert _lock is not None - assert _lock.is_locked() - - -@pytest.mark.asyncio -async def test_unlock(monkeypatch): - """Test unlock().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["rt"] = 0 - _lock = TrunkLock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - await _lock.unlock() - - assert _lock is not None - assert not _lock.is_locked() - - -@pytest.mark.asyncio -async def test_unlock_already_unlocked(monkeypatch): - """Test unlock() when already unlocked.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["rt"] = 123 - _lock = TrunkLock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - await _lock.unlock() - - assert _lock is not None - assert not _lock.is_locked() - - # Reset to default for next tests - _data["vehicle_state"]["rt"] = 0 - - -@pytest.mark.asyncio -async def test_lock(monkeypatch): - """Test lock().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["rt"] = 123 - _lock = TrunkLock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - await _lock.lock() - - assert _lock is not None - assert _lock.is_locked() - - # Reset to default for next tests - _data["vehicle_state"]["rt"] = 0 - - -@pytest.mark.asyncio -async def test_lock_already_locked(monkeypatch): - """Test lock() when already locked.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - _controller.set_last_update_time(vin=VIN, timestamp=LAST_UPDATE_TIME) - - _data = _mock.data_request_vehicle() - _data["vehicle_state"]["rt"] = 0 - _lock = TrunkLock(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _lock.async_update() - await _lock.lock() - - assert _lock is not None - assert _lock.is_locked() diff --git a/tests/unit_tests/homeassistant/test_vehicle_data.py b/tests/unit_tests/homeassistant/test_vehicle_data.py deleted file mode 100644 index c3763b30..00000000 --- a/tests/unit_tests/homeassistant/test_vehicle_data.py +++ /dev/null @@ -1,284 +0,0 @@ -"""Test online sensor.""" - -import pytest -import copy - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.vehicle_data import ( - ChargeStateDataSensor, - ClimateStateDataSensor, - DriveStateDataSensor, - GuiSettingsDataSensor, - SoftwareDataSensor, - SpeedLimitDataSensor, - VehicleConfigDataSensor, - VehicleDataSensor, - VehicleStateDataSensor, -) - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - - -def test_dict_to_attr(monkeypatch): - """Test converting dict to attributes.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = VehicleDataSensor(_data, _controller) - - data = { - "item1": 1, - "item2": 2, - "dict1": { - "item3": 3, - "item4": 4, - "dict2": { - "item5": 5, - "dict3": { - "item6": 6, - }, - "dict4": { - "item7": 7, - }, - }, - "dict5": {"item8": 8}, - }, - "dict6": { - "item9": 9, - }, - "dict7": { - "item10": 10, - }, - } - - attr = { - "item1": 1, - "item2": 2, - "dict1_item3": 3, - "dict1_item4": 4, - "dict1_dict2_item5": 5, - "dict1_dict2_dict3_item6": 6, - "dict1_dict5_item8": 8, - "dict7_item10": 10, - } - result = _sensor._dict_to_attr(data, ["dict4", "dict6"]) - assert result == attr - - -def test_dict_to_attr_no_dicts(monkeypatch): - """Test converting dict to attributes.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _sensor = VehicleDataSensor(_data, _controller) - - data = { - "item1": 1, - "item2": 2, - "dict1": { - "item3": 3, - "item4": 4, - "dict2": { - "item5": 5, - "dict3": { - "item6": 6, - }, - "dict4": { - "item7": 7, - }, - }, - "dict5": {"item8": 8}, - }, - "dict6": { - "item9": 9, - }, - "dict7": { - "item10": 10, - }, - } - - attr = { - "item1": 1, - "item2": 2, - } - result = _sensor._dict_to_attr(data, ["*"]) - assert result == attr - - -@pytest.mark.asyncio -async def test_charge_state_data_sensor(monkeypatch): - """Test charge state data sensor.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = ChargeStateDataSensor(_data, _controller) - _vehicle_data = "charge_state" - _controller.set_charging_params(vin=VIN, params=_data[_vehicle_data]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.get_value() == VIN - assert _sensor.enabled_by_default is False - assert _sensor.attrs == _data[_vehicle_data] - - -@pytest.mark.asyncio -async def test_climate_state_data_sensor(monkeypatch): - """Test climate state data sensor.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = ClimateStateDataSensor(_data, _controller) - _vehicle_data = "climate_state" - _controller.set_climate_params(vin=VIN, params=_data[_vehicle_data]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.get_value() == VIN - assert _sensor.enabled_by_default is False - assert _sensor.attrs == _data[_vehicle_data] - - -@pytest.mark.asyncio -async def test_drive_state_data_sensor(monkeypatch): - """Test drive state data sensor.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = DriveStateDataSensor(_data, _controller) - _vehicle_data = "drive_state" - _controller.set_drive_params(vin=VIN, params=_data[_vehicle_data]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.get_value() == VIN - assert _sensor.enabled_by_default is False - assert _sensor.attrs == _data[_vehicle_data] - - -@pytest.mark.asyncio -async def test_gui_settings_data_sensor(monkeypatch): - """Test gui settings data sensor.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = GuiSettingsDataSensor(_data, _controller) - _vehicle_data = "gui_settings" - _controller.set_gui_params(vin=VIN, params=_data[_vehicle_data]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.get_value() == VIN - assert _sensor.enabled_by_default is False - assert _sensor.attrs == _data[_vehicle_data] - - -@pytest.mark.asyncio -async def test_software_data_sensor(monkeypatch): - """Test software data sensor.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = SoftwareDataSensor(_data, _controller) - _vehicle_data = "vehicle_state" - _controller.set_state_params(vin=VIN, params=_data[_vehicle_data]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.get_value() == VIN - assert _sensor.enabled_by_default is False - assert _sensor.attrs == _data[_vehicle_data]["software_update"] - - -@pytest.mark.asyncio -async def test_speed_limit_data_sensor(monkeypatch): - """Test speed limit data sensor.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = SpeedLimitDataSensor(_data, _controller) - _vehicle_data = "vehicle_state" - _controller.set_state_params(vin=VIN, params=_data[_vehicle_data]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.get_value() == VIN - assert _sensor.enabled_by_default is False - assert _sensor.attrs == _data[_vehicle_data]["speed_limit_mode"] - - -@pytest.mark.asyncio -async def test_vehicle_config_data_sensor(monkeypatch): - """Test vehicle config sensor.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = VehicleConfigDataSensor(_data, _controller) - _vehicle_data = "vehicle_config" - _controller.set_config_params(vin=VIN, params=_data[_vehicle_data]) - - await _sensor.async_update() - - assert _sensor is not None - assert _sensor.get_value() == VIN - assert _sensor.enabled_by_default is False - assert _sensor.attrs == _data[_vehicle_data] - - -@pytest.mark.asyncio -async def test_vehicle_state_data_sensor(monkeypatch): - """Test vehicle state data sensor.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _sensor = VehicleStateDataSensor(_data, _controller) - _vehicle_data = "vehicle_state" - _vehicle_state_data: dict = copy.deepcopy(_data[_vehicle_data]) - - _controller.set_state_params(vin=VIN, params=_data[_vehicle_data]) - - await _sensor.async_update() - - del _vehicle_state_data["speed_limit_mode"] - del _vehicle_state_data["software_update"] - del _vehicle_state_data["media_state"] - _vehicle_state_data.update({"media_state_remote_control_enabled": True}) - - assert _sensor is not None - assert _sensor.get_value() == VIN - assert _sensor.enabled_by_default is False - assert _sensor.attrs == _vehicle_state_data diff --git a/tests/unit_tests/homeassistant/test_vehicle_device.py b/tests/unit_tests/homeassistant/test_vehicle_device.py deleted file mode 100644 index ee162351..00000000 --- a/tests/unit_tests/homeassistant/test_vehicle_device.py +++ /dev/null @@ -1,181 +0,0 @@ -"""Test vehicle device.""" - -import pytest - -from teslajsonpy.controller import Controller -from teslajsonpy.homeassistant.vehicle import VehicleDevice - -from tests.tesla_mock import TeslaMock, VIN, CAR_ID - - -def test_is_armable(monkeypatch): - """Test is_armable().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _device = VehicleDevice(_data, _controller) - - assert not _device.is_armable() - - -def test_is_armed(monkeypatch): - """Test is_armed().""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _device = VehicleDevice(_data, _controller) - - assert not _device.is_armed() - - -def test_values_on_init(monkeypatch): - """Test values after initialization.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - _data = _mock.data_request_vehicle() - _device = VehicleDevice(_data, _controller) - - assert _device is not None - - assert _device.car_name() is not None - assert _device.car_name() == "Nikola 2.0" - - assert _device.car_type is not None - assert _device.car_type == "Model S" - - assert _device.car_version is not None - assert _device.car_version == "" - - assert _device.id() is not None - assert _device.id() == 12345678901234567 - - assert _device.sentry_mode_available is not None - assert _device.sentry_mode_available - - assert _device.vehicle_id is not None - assert _device.vehicle_id() == 1234567890 - - assert not _device.update_available - assert _device.update_version is None - - -@pytest.mark.asyncio -async def test_values_after_update(monkeypatch): - """Test values after update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - _device = VehicleDevice(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _device.async_update() - - assert _device is not None - - assert not _device.car_name() is None - assert _device.car_name() == "Nikola 2.0" - - assert _device.car_type is not None - assert _device.car_type == "Model S" - - assert _device.car_version is not None - assert _device.car_version == "2019.40.2.1 38f55d9f9205" - - assert _device.id() is not None - assert _device.id() == 12345678901234567 - - assert _device.sentry_mode_available is not None - assert _device.sentry_mode_available - - assert _device.vehicle_id is not None - assert _device.vehicle_id() == 1234567890 - - assert _device.update_available - assert _device.update_version == "2019.40.2.1" - - -@pytest.mark.asyncio -async def test_values_after_update_no_vehicle_update(monkeypatch): - """Test values after update.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - _controller.set_id_vin(CAR_ID, VIN) - - _data = _mock.data_request_vehicle() - monkeypatch.setitem( - _data["vehicle_state"], - "software_update", - { - "download_perc": 0, - "expected_duration_sec": 2700, - "install_perc": 1, - "scheduled_time_ms": None, - "status": "", - "version": "", - }, - ) - _device = VehicleDevice(_data, _controller) - - _controller.set_state_params(vin=VIN, params=_data["vehicle_state"]) - - await _device.async_update() - - assert not _device.update_available - assert _device.update_version == "" - - -@pytest.mark.asyncio -async def test_assumed_state_online(monkeypatch): - # pylint: disable=protected-access - """Test assumed_state() with online vehicle.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - # TODO: Should not have protected access here (see vehicle.py) - monkeypatch.setitem(_controller.car_online, 12345678901234567, True) - monkeypatch.setitem(_controller._last_update_time, 12345678901234567, 100) - monkeypatch.setitem(_controller._last_wake_up_time, 12345678901234567, 0) - - _data = _mock.data_request_vehicle() - _device = VehicleDevice(_data, _controller) - - await _device.async_update() - - assert _device is not None - assert not _device.assumed_state() is None - assert not _device.assumed_state() - - -@pytest.mark.asyncio -async def test_assumed_state_offline(monkeypatch): - # pylint: disable=protected-access - """Test assumed_state() with offline vehicle.""" - - _mock = TeslaMock(monkeypatch) - _controller = Controller(None) - - # TODO: Should not have protected access here (see vehicle.py) - monkeypatch.setitem(_controller.car_online, 12345678901234567, False) - monkeypatch.setitem(_controller._last_update_time, 12345678901234567, 1000) - monkeypatch.setitem(_controller._last_wake_up_time, 12345678901234567, 0) - - _data = _mock.data_request_vehicle() - _device = VehicleDevice(_data, _controller) - - await _device.async_update() - - assert _device is not None - assert not _device.assumed_state() is None - assert _device.assumed_state() diff --git a/tests/unit_tests/test_energy.py b/tests/unit_tests/test_energy.py new file mode 100644 index 00000000..f781ac8c --- /dev/null +++ b/tests/unit_tests/test_energy.py @@ -0,0 +1,204 @@ +"""Test power sensor.""" + +import pytest + +from teslajsonpy.controller import Controller + +from tests.tesla_mock import TeslaMock + + +@pytest.mark.asyncio +async def test_energysite_setup(monkeypatch): + """Test setup of energysites in Controller.connect().""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + + solar_site = _controller.energysites[12345] + powerwall_site = _controller.energysites[67890] + + assert _controller.energysites is not None + assert solar_site.resource_type == "solar" + assert powerwall_site.resource_type == "battery" + + +@pytest.mark.asyncio +async def test_solar_site(monkeypatch): + """Test SolarSite class.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + + _solar_site = _controller.energysites[12345] + + assert _solar_site._api is not None + assert _solar_site._energysite is not None + assert _solar_site._power_data == { + "solar_power": 0, + "load_power": 0, + "grid_power": 0, + "battery_power": 0, + } + + assert _solar_site.energysite_id == 12345 + assert _solar_site.has_load_meter + assert _solar_site.id == "313dbc37-555c-45b1-83aa-62a4ef9ff7ac" + assert _solar_site.resource_type == "solar" + assert _solar_site.site_name == "My Solar Home" + + assert _solar_site.grid_power == 0 + assert _solar_site.load_power == 0 + assert _solar_site.solar_power == 0 + assert _solar_site.solar_type == "pv_panel" + + +@pytest.mark.asyncio +async def test_powerwall_site(monkeypatch): + """Test PowerwallSite class.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + + _solar_powerwall_site = _controller.energysites[67890] + + assert _solar_powerwall_site._api is not None + assert _solar_powerwall_site._energysite is not None + assert _solar_powerwall_site._power_data == { + "solar_power": 0, + "load_power": 0, + "grid_power": 0, + "battery_power": 0, + } + + assert _solar_powerwall_site.energysite_id == 67890 + assert _solar_powerwall_site.has_load_meter + assert _solar_powerwall_site.id == "212dbc27-333c-45b1-81bb-31e2zd2fs2cm" + assert _solar_powerwall_site.resource_type == "battery" + assert _solar_powerwall_site.site_name == "My Battery Home" + + # assert _solar_powerwall_site.battery_percent == 0 + # assert _solar_powerwall_site.battery_power == 0 + assert _solar_powerwall_site.grid_power == 0 + assert _solar_powerwall_site.load_power == 0 + assert _solar_powerwall_site.solar_power == 0 + assert _solar_powerwall_site.solar_type == "pv_panel" + + +# @pytest.mark.asyncio +# async def test_solar_power_sensor(monkeypatch): +# """Test SolarPowerSensor class.""" +# _mock = TeslaMock(monkeypatch) +# _controller = Controller(None) +# # Test a solar only site (no Powerwall) +# _data = _mock.data_request_solar_combined_data() +# _sensor = SolarPowerSensor(_data, _controller) + +# assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" +# assert _sensor.get_power() == 7720 +# # Test a battery site (Powerwall) +# _data = _mock.data_request_battery_combined_data() +# _sensor = SolarPowerSensor(_data, _controller) + +# assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" +# assert _sensor.get_power() == 7720 + + +# @pytest.mark.asyncio +# async def test_load_power_sensor(monkeypatch): +# """Test LoadPowerSensor class.""" +# _mock = TeslaMock(monkeypatch) +# _controller = Controller(None) +# # Test a solar only site (no Powerwall) +# _data = _mock.data_request_solar_combined_data() +# _sensor = LoadPowerSensor(_data, _controller) + +# assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" +# assert _sensor.get_power() == 4517.14990234375 +# # Test a battery site (Powerwall) +# _data = _mock.data_request_battery_combined_data() +# _sensor = LoadPowerSensor(_data, _controller) + +# assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" +# assert _sensor.get_power() == 4517.14990234375 + + +# @pytest.mark.asyncio +# async def test_grid_power_sensor(monkeypatch): +# """Test GridPowerSensor class.""" +# _mock = TeslaMock(monkeypatch) +# _controller = Controller(None) +# # Test a solar only site (no Powerwall) +# _data = _mock.data_request_solar_combined_data() +# _sensor = GridPowerSensor(_data, _controller) + +# assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" +# assert _sensor.get_power() == -3202.85009765625 +# # Test a battery site (Powerwall) +# _data = _mock.data_request_battery_combined_data() +# _sensor = GridPowerSensor(_data, _controller) + +# assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" +# assert _sensor.get_power() == -3202.85009765625 + + +# @pytest.mark.asyncio +# async def test_battery_power_sensor(monkeypatch): +# """Test BatteryPowerSensor class.""" +# _mock = TeslaMock(monkeypatch) +# _controller = Controller(None) +# _data = _mock.data_request_battery_combined_data() +# _sensor = BatteryPowerSensor(_data, _controller) + +# assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" +# assert _sensor.get_power() == 0 + + +# def test_site_without_name(monkeypatch): +# """Test site with no site_name in json data.""" +# _mock = TeslaMock(monkeypatch) +# _controller = Controller(None) +# _data = _mock.data_request_solar_combined_data_no_name() +# _sensor = LoadPowerSensor(_data, _controller) + +# assert _sensor.site_name() == "My Home" + + +# @pytest.mark.asyncio +# async def test_get_power_after_update(monkeypatch): +# """Test get_power() after an update.""" +# _mock = TeslaMock(monkeypatch) +# _controller = Controller(None) +# _data = _mock.data_request_solar_combined_data() +# _data["solar_power"] = 1800 +# _data["load_power"] = 1800 +# _data["grid_power"] = 1800 + +# _sensor = SolarPowerSensor(_data, _controller) + +# await _sensor.async_update() +# assert _sensor.get_power() == 7720 + +# _sensor = LoadPowerSensor(_data, _controller) + +# await _sensor.async_update() +# assert _sensor.get_power() == 4517.14990234375 + +# _sensor = GridPowerSensor(_data, _controller) + +# await _sensor.async_update() +# assert _sensor.get_power() == -3202.85009765625 + + +# @pytest.mark.asyncio +# async def test_get_power_after_update_with_unknown_status(monkeypatch): +# """Test get_power() after an update with unknown grid status.""" +# _mock = TeslaMock(monkeypatch) +# monkeypatch.setattr( +# Controller, "get_power_params", _mock.mock_get_power_unknown_grid_params +# ) +# _controller = Controller(None) +# _data = _mock.data_request_solar_combined_data() +# _sensor = SolarPowerSensor(_data, _controller) + +# await _sensor.async_update() +# assert _sensor.get_power() == 1750 From b3b694a84dbbd809b13d43acc616f3613b7c2426 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sat, 27 Aug 2022 17:21:36 -0700 Subject: [PATCH 30/84] Fix for grid_status --- teslajsonpy/controller.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 1524db8e..d90ad0c7 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -924,21 +924,23 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: data = None if data and data["response"]: response = data["response"] - # Note: Some systems that pre-date Tesla aquisition of SolarCity - # and systems with a Tesla inverter (non-Powerwall) will have - # `grid_status: Unknown`, but will have solar power values. - # At the same time, newer systems maye report spurious reads of 0 Watts - # and grid status unknown. In this case, remove values but update - # self.__power_data with remaining data (grid and load power). - if response.get("grid_status") == "Active": + # Some setups always report grid_status of "Unknown" regardless + # of the actual grid status. Others only report grid_status "Unknown" + # when the actual grid status is unknown. These setups also sometimes + # report an incorrect solar_power value of 0. + if ( + "grid_status" not in response + or response.get("grid_status") != "Unknown" + ): self.__grid_status[energysite_id]["grid_always_unk"] = False if not self.__grid_status[energysite_id]["grid_always_unk"] and ( response.get("grid_status") == "Unknown" and response.get("solar_power") == 0 ): - _LOGGER.debug("Possible spurious energy site power read") - del response["grid_status"] + _LOGGER.debug( + "Ignoring possible spurious energy site solar power read." + ) del response["solar_power"] self.__power_data[energysite_id].update(response) From 967e8896bd29b4b828e363e2dd70283534597772 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sat, 27 Aug 2022 18:48:06 -0700 Subject: [PATCH 31/84] Change to get method and add tests --- teslajsonpy/car.py | 50 +++++----- teslajsonpy/controller.py | 10 +- tests/tesla_mock.py | 97 +++++++++++++++----- tests/unit_tests/test_car.py | 157 ++++++++++++++++++++++++++++++++ tests/unit_tests/test_energy.py | 130 +++----------------------- 5 files changed, 275 insertions(+), 169 deletions(-) create mode 100644 tests/unit_tests/test_car.py diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index e7c8a7fe..6a87a4a7 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -29,27 +29,27 @@ def __init__(self, car, controller) -> None: @property def display_name(self) -> str: """Return State Data.""" - return self._car["display_name"] + return self._car.get("display_name") @property def id(self) -> int: """Return State Data.""" - return self._car["id"] + return self._car.get("id") @property def state(self) -> str: """Return State Data.""" - return self._car["state"] + return self._car.get("state") @property def vehicle_id(self) -> int: """Return State Data.""" - return self._car["vehicle_id"] + return self._car.get("vehicle_id") @property def vin(self) -> str: """Return State Data.""" - return self._car["vin"] + return self._car.get("vin") @property def data_available(self) -> bool: @@ -82,7 +82,7 @@ def car_type(self) -> str: @property def car_version(self) -> str: """Return installed car software version.""" - return self._controller.get_state_params(vin=self.vin)["car_version"] + return self._controller.get_state_params(vin=self.vin).get("car_version") @property def charger_actual_current(self) -> int: @@ -171,7 +171,7 @@ def charger_power(self) -> int: @property def charge_rate(self) -> str: """Return charge rate.""" - return self._controller.get_charging_params(vin=self.vin)["charge_rate"] + return self._controller.get_charging_params(vin=self.vin).get("charge_rate") @property def charging_state(self) -> str: @@ -387,7 +387,7 @@ def native_latitude(self) -> float: @property def odometer(self) -> float: """Return odometer.""" - return self._controller.get_state_params(vin=self.vin)["odometer"] + return self._controller.get_state_params(vin=self.vin).get("odometer") @property def outside_temp(self) -> float: @@ -487,7 +487,7 @@ async def change_charge_limit(self, value: float) -> None: wake_if_asleep=True, ) - if data and data["response"]["result"] is True: + if data and data.get("response").get("result") is True: params = {"charge_limit_soc": int(value)} self._controller.update_charging_params(vin=self.vin, params=params) @@ -499,7 +499,7 @@ async def charge_port_door_close(self) -> None: wake_if_asleep=True, ) - if data and data["response"]["result"] is True: + if data and data.get("response").get("result") is True: params = {"charge_port_door_open": False} self._controller.update_state_params(vin=self.vin, params=params) @@ -511,7 +511,7 @@ async def charge_port_door_open(self) -> None: wake_if_asleep=True, ) - if data and data["response"]["result"] is True: + if data and data.get("response").get("result") is True: params = {"charge_port_door_open": True} self._controller.update_state_params(vin=self.vin, params=params) @@ -540,7 +540,7 @@ async def lock(self): path_vars={"vehicle_id": self.id}, wake_if_asleep=True, ) - if data and data["response"]["result"] is True: + if data and data.get("response").get("result") is True: params = {"locked": True} self._controller.update_state_params(vin=self.vin, params=params) @@ -560,7 +560,7 @@ async def remote_seat_heater_request(self, level: int, seat_id: int) -> None: level=level, wake_if_asleep=True, ) - if data and data["response"]["result"] is True: + if data and data.get("response").get("result") is True: params = {f"seat_heater_{SEAT_NAME_MAP[seat_id]}": level} self._controller.update_climate_params(vin=self.vin, params=params) @@ -587,7 +587,7 @@ async def set_charging_amps(self, value: float) -> None: wake_if_asleep=True, ) - if data and data["response"]["result"] is True: + if data and data.get("response").get("result") is True: params = {"charge_amps": int(value)} self._controller.update_charging_params(vin=self.vin, params=params) @@ -615,7 +615,7 @@ async def set_cabin_overheat_protection(self, option: str) -> None: fan_only=fan_only, wake_if_asleep=True, ) - if data and data["response"]["result"]: + if data and data.get("response").get("result"): params = {"cabin_overheat_protection": option} self._controller.update_climate_params(vin=self.vin, params=params) @@ -641,7 +641,7 @@ async def set_heated_steering_wheel(self, value: bool) -> None: wake_if_asleep=True, ) - if data and data["response"]["result"] is True: + if data and data.get("response").get("result") is True: params = {"steering_wheel_heater": value} self._controller.update_climate_params(vin=self.vin, params=params) @@ -683,7 +683,7 @@ async def set_sentry_mode(self, value: bool) -> None: wake_if_asleep=True, ) - if data and data["response"]["result"] is True: + if data and data.get("response").get("result") is True: params = {"sentry_mode": value} self._controller.update_state_params(vin=self.vin, params=params) @@ -696,7 +696,7 @@ async def set_temperature(self, temp: float) -> None: passenger_temp=temp, wake_if_asleep=True, ) - if data and data["response"]["result"] is True: + if data and data.get("response").get("result") is True: params = {"driver_temp_setting": temp} self._controller.update_climate_params(vin=self.vin, params=params) @@ -709,7 +709,7 @@ async def start_charge(self) -> None: wake_if_asleep=True, ) - if data and data["response"]["result"] is True: + if data and data.get("response").get("result") is True: params = {"charging_state": "Charging"} self._controller.update_charging_params(vin=self.vin, params=params) @@ -721,7 +721,7 @@ async def stop_charge(self) -> None: wake_if_asleep=True, ) - if data and data["response"]["result"] is True: + if data and data.get("response").get("result") is True: params = {"charging_state": None} self._controller.update_charging_params(vin=self.vin, params=params) @@ -741,7 +741,7 @@ async def toggle_trunk(self) -> None: which_trunk="rear", wake_if_asleep=True, ) - if data and data["response"]["result"] is True: + if data and data.get("response").get("result") is True: if self.is_trunk_locked: params = {"rt": 0} self._controller.update_state_params(vin=self.vin, params=params) @@ -757,7 +757,7 @@ async def toggle_frunk(self) -> None: which_trunk="front", wake_if_asleep=True, ) - if data and data["response"]["result"] is True: + if data and data.get("response").get("result") is True: if self.is_frunk_locked: params = {"ft": 0} self._controller.update_state_params(vin=self.vin, params=params) @@ -785,8 +785,8 @@ async def trigger_homelink(self) -> None: if data and data.get("response"): _LOGGER.debug("Homelink response: %s", data.get("response")) - result = data["response"].get("result") - reason = data["response"].get("reason") + result = data.get("response").get("result") + reason = data.get("response").get("reason") if result is False: raise HomelinkError(f"Error calling trigger_homelink: {reason}") @@ -797,6 +797,6 @@ async def unlock(self) -> None: path_vars={"vehicle_id": self.id}, wake_if_asleep=True, ) - if data and data["response"]["result"] is True: + if data and data.get("response").get("result") is True: params = {"locked": False} self._controller.update_state_params(vin=self.vin, params=params) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index d90ad0c7..e7348e1e 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -878,7 +878,7 @@ async def _get_and_process_car_data(vin: Text) -> None: ) except TeslaException: data = None - if data and data["response"]: + if data and data.get("response"): response = data["response"] self.set_climate_params(vin=vin, params=response["climate_state"]) self.set_charging_params(vin=vin, params=response["charge_state"]) @@ -922,7 +922,7 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: ) except TeslaException: data = None - if data and data["response"]: + if data and data.get("response"): response = data["response"] # Some setups always report grid_status of "Unknown" regardless # of the actual grid status. Others only report grid_status "Unknown" @@ -958,7 +958,7 @@ async def _get_and_process_battery_data( ) except TeslaException: data = None - if data and data["response"]: + if data and data.get("response").get("power_reading"): response = data["response"] params = response["power_reading"][0] @@ -967,6 +967,8 @@ async def _get_and_process_battery_data( params["operation"] = response.get("operation") # Use energysite_id since that's how it's retrieved self.__power_data[energysite_id].update(params) + else: + _LOGGER.info("No power readings for energy site %s", energysite_id) async def _get_and_process_battery_summary( energysite_id: Text, battery_id: Text @@ -983,7 +985,7 @@ async def _get_and_process_battery_summary( ) except TeslaException: data = None - if data and data["response"]: + if data and data.get("response"): self.__power_data[energysite_id].update(data["response"]) async with self.__update_lock: diff --git a/tests/tesla_mock.py b/tests/tesla_mock.py index 102042ae..7f080d63 100644 --- a/tests/tesla_mock.py +++ b/tests/tesla_mock.py @@ -21,21 +21,21 @@ def __init__(self, monkeypatch) -> None: # self._monkeypatch.setattr(Controller, "connect", self.mock_connect) self._monkeypatch.setattr(Controller, "command", self.mock_command) self._monkeypatch.setattr(Controller, "api", self.mock_api) - # self._monkeypatch.setattr( - # Controller, "get_charging_params", self.mock_get_charging_params - # ) - # self._monkeypatch.setattr( - # Controller, "get_climate_params", self.mock_get_climate_params - # ) - # self._monkeypatch.setattr( - # Controller, "get_drive_params", self.mock_get_drive_params - # ) - # self._monkeypatch.setattr( - # Controller, "get_gui_params", self.mock_get_gui_params - # ) - # self._monkeypatch.setattr( - # Controller, "get_state_params", self.mock_get_state_params - # ) + self._monkeypatch.setattr( + Controller, "get_charging_params", self.mock_get_charging_params + ) + self._monkeypatch.setattr( + Controller, "get_climate_params", self.mock_get_climate_params + ) + self._monkeypatch.setattr( + Controller, "get_drive_params", self.mock_get_drive_params + ) + self._monkeypatch.setattr( + Controller, "get_gui_params", self.mock_get_gui_params + ) + self._monkeypatch.setattr( + Controller, "get_state_params", self.mock_get_state_params + ) self._monkeypatch.setattr( Controller, "get_product_list", self.mock_get_product_list ) @@ -64,6 +64,7 @@ def __init__(self, monkeypatch) -> None: self._monkeypatch.setattr( Controller, "get_power_params", self.mock_get_power_params ) + self._energysites = copy.deepcopy(ENERGYSITES) self._product_list = copy.deepcopy(PRODUCT_LIST) self._vehicle_data = copy.deepcopy(VEHICLE_DATA) self._site_data = copy.deepcopy(SITE_DATA) @@ -77,7 +78,7 @@ def __init__(self, monkeypatch) -> None: self._solar_combined_data = copy.deepcopy(SOLAR_COMBINED_DATA) self._solar_combined_data_no_name = copy.deepcopy(SOLAR_COMBINED_DATA_NO_NAME) self._site_config = copy.deepcopy(SITE_CONFIG) - self._site_state_unknown_grid = copy.deepcopy(SITE_STATE_UNKNOWN_GRID) + self._site_data_unknown_grid = copy.deepcopy(SITE_DATA_UNKNOWN_GRID) self._vehicle = copy.deepcopy(VEHICLE) self._vehicle["drive_state"] = self._drive_state self._vehicle["climate_state"] = self._climate_state @@ -196,11 +197,11 @@ def controller_get_climate_params(self): def controller_get_power_params(self): """Monkeypatch for controller.get_climate_params().""" - return self._site_state + return self._site_data def controller_get_power_unknown_grid_params(self): """Monkeypatch for controller.get_climate_params().""" - return self._site_state_unknown_grid + return self._site_data_unknown_grid def controller_get_drive_params(self): """Monkeypatch for controller.get_drive_params().""" @@ -260,17 +261,17 @@ def data_request_vehicle_state(self): """Simulate the result of vehicle state data request.""" return self._vehicle_state - def data_request_solar_combined_data(self): + def data_request_energysites(self): """Similate the result of combined product list & site config request.""" - return self._solar_combined_data[0] + return self._energysites def data_request_solar_combined_data_no_name(self): """Similate the result of combined product list & site config without name.""" return self._solar_combined_data_no_name - def data_request_site_state_unknown_grid(self): + def data_request_site_data_unknown_grid(self): """Similate the result of site state with unknown grid data request.""" - return self._site_state_unknown_grid + return self._site_data_unknown_grid @staticmethod def command_ok(): @@ -573,6 +574,56 @@ def command_ok(): "vehicle_config": None, } +ENERGYSITES = [ + { + "energy_site_id": 12345, + "resource_type": "solar", + "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", + "asset_site_id": "12345", + "solar_power": 2260, + "solar_type": "pv_panel", + "storm_mode_enabled": None, + "powerwall_onboarding_settings_set": None, + "sync_grid_alert_enabled": False, + "breaker_alert_enabled": False, + "components": { + "battery": False, + "solar": True, + "solar_type": "pv_panel", + "grid": True, + "load_meter": True, + "market_type": "residential", + }, + }, + { + "energy_site_id": 67890, + "resource_type": "battery", + "site_name": "My Battery Home", + "id": "212dbc27-333c-45b1-81bb-31e2zd2fs2cm", + "gateway_id": "67890", + "asset_site_id": "67890", + "energy_left": 2864.7368421052633, + "total_pack_energy": 14070, + "percentage_charged": 20.360603000037408, + "battery_type": "ac_powerwall", + "backup_capable": True, + "battery_power": 3080, + "storm_mode_enabled": True, + "powerwall_onboarding_settings_set": True, + "sync_grid_alert_enabled": True, + "breaker_alert_enabled": True, + "components": { + "battery": True, + "battery_type": "ac_powerwall", + "solar": True, + "solar_type": "pv_panel", + "grid": True, + "load_meter": True, + "market_type": "residential", + }, + }, +] + SITE_CONFIG = { "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", "site_name": "My Solar Home", @@ -811,7 +862,7 @@ def command_ok(): "wall_connectors": None, } -SITE_STATE_UNKNOWN_GRID = { +SITE_DATA_UNKNOWN_GRID = { "id": 12345678901234567, "timestamp": "2011-01-01", "solar_power": 1750, diff --git a/tests/unit_tests/test_car.py b/tests/unit_tests/test_car.py new file mode 100644 index 00000000..df34ca2a --- /dev/null +++ b/tests/unit_tests/test_car.py @@ -0,0 +1,157 @@ +"""Test cars.""" + +import pytest + +from teslajsonpy.controller import Controller + +from tests.tesla_mock import TeslaMock + + +@pytest.mark.asyncio +async def test_car_properties(monkeypatch): + """Test SolarSite class.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + + _car = _controller.cars["5YJSA11111111111"] + + assert _car._car is not None + assert _car._controller is not None + + assert _car.display_name == "Nikola 2.0" + + assert _car.id == 12345678901234567 + + assert _car.state == "online" + + assert _car.vehicle_id == 1234567890 + + assert _car.vin == "5YJSA11111111111" + + # assert _car.data_available == True + + assert _car.battery_level == 64 + + assert _car.battery_range == 167.96 + + # assert _car.cabin_overheat_protection == "" + + assert _car.car_type == "Model S" + + # assert _car.car_version == "" + + assert _car.charger_actual_current == 0 + + assert _car.charge_current_request == 48 + + assert _car.charge_current_request_max == 48 + + assert _car.charge_port_latch == "Engaged" + + assert _car.charge_energy_added == 12.41 + + assert _car.charge_limit_soc == 90 + + assert _car.charge_limit_soc_max == 100 + + assert _car.charge_limit_soc_min == 50 + + assert _car.charge_miles_added_ideal == 50.0 + + assert _car.charge_miles_added_rated == 40.0 + + assert _car.charger_phases is None + + assert _car.charger_power == 0 + + assert _car.charge_rate == 0.0 + + assert _car.charging_state == "Disconnected" + + assert _car.charger_voltage == 0 + + assert _car.climate_keeper_mode == "dog" + + assert _car.conn_charge_cable == "" + + assert _car.defrost_mode == 0 + + assert _car.driver_temp_setting == 21.6 + + assert not _car.fast_charger_present + + assert _car.fast_charger_brand == "" + + assert _car.fast_charger_type == "" + + assert _car.gui_distance_units == "mi/hr" + + assert _car.gui_range_display == "Rated" + + assert _car.homelink_device_count == 0 + + assert _car.homelink_nearby + + assert _car.ideal_battery_range == 209.95 + + assert _car.inside_temp is None + + assert not _car.is_charge_port_door_open + + assert not _car.is_climate_on + + assert _car.is_frunk_locked + + assert _car.is_locked + + assert not _car.is_steering_wheel_heater_on + + assert _car.is_trunk_locked + + # assert _car.is_on == "" + + assert _car.longitude == -88.111111 + + assert _car.latitude == 33.111111 + + assert _car.max_avail_temp == 28.0 + + assert _car.min_avail_temp == 15.0 + + # assert not _car.native_heading + + assert _car.native_location_supported == 1 + + assert _car.native_longitude == -88.111111 + + assert _car.native_latitude == 33.111111 + + assert _car.odometer == 33561.422505 + + assert not _car.outside_temp + + assert _car.rear_heated_seats + + assert _car.sentry_mode + + assert _car.sentry_mode_available + + assert not _car.shift_state + + assert not _car.speed + + assert _car.software_update == { + "download_perc": 100, + "expected_duration_sec": 2700, + "install_perc": 10, + "scheduled_time_ms": 1575689678432, + "status": "scheduled", + "version": "2019.40.2.1", + } + + assert not _car.steering_wheel_heater + + assert not _car.third_row_seats + + assert _car.time_to_full_charge == 0.0 diff --git a/tests/unit_tests/test_energy.py b/tests/unit_tests/test_energy.py index f781ac8c..4de28eb8 100644 --- a/tests/unit_tests/test_energy.py +++ b/tests/unit_tests/test_energy.py @@ -1,8 +1,9 @@ -"""Test power sensor.""" +"""Test energy sites.""" import pytest from teslajsonpy.controller import Controller +from teslajsonpy.energy import EnergySite from tests.tesla_mock import TeslaMock @@ -84,121 +85,16 @@ async def test_powerwall_site(monkeypatch): assert _solar_powerwall_site.solar_type == "pv_panel" -# @pytest.mark.asyncio -# async def test_solar_power_sensor(monkeypatch): -# """Test SolarPowerSensor class.""" -# _mock = TeslaMock(monkeypatch) -# _controller = Controller(None) -# # Test a solar only site (no Powerwall) -# _data = _mock.data_request_solar_combined_data() -# _sensor = SolarPowerSensor(_data, _controller) +@pytest.mark.asyncio +async def test_energysite_with_no_name(monkeypatch): + """Test EnergySite base class with no name.""" + _mock = TeslaMock(monkeypatch) + _api = Controller(None) + _energysite = _mock.data_request_energysites()[0] + _power_data = _mock.controller_get_power_params() + _sensor = EnergySite(_api, _energysite, _power_data) + + assert _sensor.site_name == "My Home" -# assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" -# assert _sensor.get_power() == 7720 -# # Test a battery site (Powerwall) -# _data = _mock.data_request_battery_combined_data() -# _sensor = SolarPowerSensor(_data, _controller) - -# assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" -# assert _sensor.get_power() == 7720 - - -# @pytest.mark.asyncio -# async def test_load_power_sensor(monkeypatch): -# """Test LoadPowerSensor class.""" -# _mock = TeslaMock(monkeypatch) -# _controller = Controller(None) -# # Test a solar only site (no Powerwall) -# _data = _mock.data_request_solar_combined_data() -# _sensor = LoadPowerSensor(_data, _controller) - -# assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" -# assert _sensor.get_power() == 4517.14990234375 -# # Test a battery site (Powerwall) -# _data = _mock.data_request_battery_combined_data() -# _sensor = LoadPowerSensor(_data, _controller) - -# assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" -# assert _sensor.get_power() == 4517.14990234375 - - -# @pytest.mark.asyncio -# async def test_grid_power_sensor(monkeypatch): -# """Test GridPowerSensor class.""" -# _mock = TeslaMock(monkeypatch) -# _controller = Controller(None) -# # Test a solar only site (no Powerwall) -# _data = _mock.data_request_solar_combined_data() -# _sensor = GridPowerSensor(_data, _controller) - -# assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" -# assert _sensor.get_power() == -3202.85009765625 -# # Test a battery site (Powerwall) -# _data = _mock.data_request_battery_combined_data() -# _sensor = GridPowerSensor(_data, _controller) - -# assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" -# assert _sensor.get_power() == -3202.85009765625 - - -# @pytest.mark.asyncio -# async def test_battery_power_sensor(monkeypatch): -# """Test BatteryPowerSensor class.""" -# _mock = TeslaMock(monkeypatch) -# _controller = Controller(None) -# _data = _mock.data_request_battery_combined_data() -# _sensor = BatteryPowerSensor(_data, _controller) - -# assert _sensor.name == f"{_sensor._site_name} {_sensor.type}" -# assert _sensor.get_power() == 0 - - -# def test_site_without_name(monkeypatch): -# """Test site with no site_name in json data.""" -# _mock = TeslaMock(monkeypatch) -# _controller = Controller(None) -# _data = _mock.data_request_solar_combined_data_no_name() -# _sensor = LoadPowerSensor(_data, _controller) - -# assert _sensor.site_name() == "My Home" - - -# @pytest.mark.asyncio -# async def test_get_power_after_update(monkeypatch): -# """Test get_power() after an update.""" -# _mock = TeslaMock(monkeypatch) -# _controller = Controller(None) -# _data = _mock.data_request_solar_combined_data() -# _data["solar_power"] = 1800 -# _data["load_power"] = 1800 -# _data["grid_power"] = 1800 - -# _sensor = SolarPowerSensor(_data, _controller) - -# await _sensor.async_update() -# assert _sensor.get_power() == 7720 - -# _sensor = LoadPowerSensor(_data, _controller) - -# await _sensor.async_update() -# assert _sensor.get_power() == 4517.14990234375 - -# _sensor = GridPowerSensor(_data, _controller) - -# await _sensor.async_update() -# assert _sensor.get_power() == -3202.85009765625 - - -# @pytest.mark.asyncio -# async def test_get_power_after_update_with_unknown_status(monkeypatch): -# """Test get_power() after an update with unknown grid status.""" -# _mock = TeslaMock(monkeypatch) -# monkeypatch.setattr( -# Controller, "get_power_params", _mock.mock_get_power_unknown_grid_params -# ) -# _controller = Controller(None) -# _data = _mock.data_request_solar_combined_data() -# _sensor = SolarPowerSensor(_data, _controller) -# await _sensor.async_update() -# assert _sensor.get_power() == 1750 +# Test reponse with "grid_status" of "Unknown" From 4d3bfc50e448398cd20516d2d8d9337a6cc0053d Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 28 Aug 2022 07:41:09 -0700 Subject: [PATCH 32/84] Remove unused code --- teslajsonpy/controller.py | 166 +------------------------------------- teslajsonpy/energy.py | 8 +- 2 files changed, 7 insertions(+), 167 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index e7348e1e..ef9a0fd3 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -30,7 +30,6 @@ UPDATE_INTERVAL, SLEEP_INTERVAL, PRODUCT_TYPE_ENERGY_SITES, - TESLA_PRODUCT_TYPE_VEHICLES, RESOURCE_TYPE, RESOURCE_TYPE_SOLAR, RESOURCE_TYPE_BATTERY, @@ -423,6 +422,7 @@ async def connect( "load_power": 0, "grid_power": 0, "battery_power": 0, + "battery_percentage": 0, } # Default to True but check in first update self.__grid_status[energysite_id] = {"grid_always_unk": True} @@ -532,166 +532,6 @@ async def get_site_config(self, energysite_id: int) -> dict: "response" ] - @wake_up - async def post( - self, - car_id, - command, - data=None, - wake_if_asleep=True, - product_type: str = TESLA_PRODUCT_TYPE_VEHICLES, - ): - # pylint: disable=unused-argument - """Send post command to the car_id. - - This is a wrapped function by wake_up. - - Parameters - ---------- - car_id : string - Identifier for the car on the owner-api endpoint. It is the id - field for identifying the car across the owner-api endpoint. - 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. - product_type: string - Indicates whether this is a vehicle or a energy site. Defaults to TESLA_PRODUCT_TYPE_VEHICLES - - Returns - ------- - dict - Tesla json object. - - """ - car_id = self._update_id(car_id) - data = data or {} - return await self.__connection.post( - f"{product_type}/{car_id}/{command}", data=data - ) - - @wake_up - async def get( - self, - car_id, - command, - wake_if_asleep=False, - product_type: str = TESLA_PRODUCT_TYPE_VEHICLES, - ): - # pylint: disable=unused-argument - """Send get command to the car_id. - - This is a wrapped function by wake_up. - - Parameters - ---------- - car_id : string - Identifier for the car on the owner-api endpoint. It is the id - field for identifying the car across the owner-api endpoint. - 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. - product_type: string - Indicates whether this is a vehicle or a energy site. Defaults to TESLA_PRODUCT_TYPE_VEHICLES - - Returns - ------- - dict - Tesla json object. - - """ - car_id = self._update_id(car_id) - return await self.__connection.get(f"{product_type}/{car_id}/{command}") - - async def vehicle_data_request(self, car_id, name, wake_if_asleep=False): - """Get requested data from car_id. - - Parameters - ---------- - car_id : string - Identifier for the car on the owner-api endpoint. It is the id - field for identifying the car across the owner-api endpoint. - 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. - - """ - car_id = self._update_id(car_id) - return ( - await self.get( - car_id, f"vehicle_data/{name}", wake_if_asleep=wake_if_asleep - ) - )["response"] - - @backoff.on_exception( - min_expo, - TeslaException, - max_time=60, - logger=__name__, - min_value=15, - giveup=should_giveup, - ) - async def command( - self, - car_id, - name, - data=None, - wake_if_asleep=True, - product_type: str = TESLA_PRODUCT_TYPE_VEHICLES, - ): - """Post name command to the car_id. - - This will be deprecated. Use :meth:`teslajsonpy.Controller.api` instead. - - Parameters - ---------- - car_id : string - Identifier for the car on the owner-api endpoint. It is the id - field for identifying the car across the owner-api endpoint. - 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. - product_type: string - Indicates whether this is a vehicle or a energy site. Defaults to TESLA_PRODUCT_TYPE_VEHICLES - - Returns - ------- - dict - Tesla json object. - - """ - car_id = self._update_id(car_id) - data = data or {} - return await self.post( - car_id, - f"command/{name}", - data=data, - wake_if_asleep=wake_if_asleep, - product_type=product_type, - ) - def _generate_car_objects(self) -> None: """Generate car objects.""" for car in self.__vehicle_list: @@ -732,8 +572,8 @@ async def _wake_up(self, car_id): if not self.is_car_online(vin=car_vin) or ( self._last_wake_up_attempt[car_vin] < self._last_attempted_update_time ): - result = await self.post( - car_id, "wake_up", wake_if_asleep=False + result = await self.api( + "WAKE_UP", path_vars={"vehicle_id": car_id}, wake_if_asleep=False ) # avoid wrapper loop self.set_car_online( car_id=car_id, online_status=result["response"]["state"] == "online" diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index 9408458e..cec0d2eb 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -18,17 +18,17 @@ def __init__(self, api, energysite, power_data) -> None: @property def energysite_id(self) -> int: """Return energy site id (aka site_id).""" - return self._energysite["energy_site_id"] + return self._energysite.get("energy_site_id") @property def has_load_meter(self) -> bool: """Return True if energy site has a load meter.""" - return self._energysite["components"]["load_meter"] + return self._energysite.get("components").get("load_meter") @property def id(self) -> int: """Return id (aka battery_id).""" - return self._energysite["id"] + return self._energysite.get("id") @property def resource_type(self) -> str: @@ -73,7 +73,7 @@ def solar_power(self) -> float: @property def solar_type(self) -> str: """Return type of solar (e.g. pv_panels or roof).""" - return self._energysite["components"]["solar_type"] + return self._energysite.get("components").get("solar_type") class PowerwallSite(EnergySite): From c872293355b210b9af257c7109f26fff0bf172eb Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 28 Aug 2022 11:31:44 -0700 Subject: [PATCH 33/84] Update tests and get method usage --- teslajsonpy/car.py | 45 +-- tests/tesla_mock.py | 677 +++++++++++--------------------- tests/unit_tests/test_car.py | 432 ++++++++++++++++---- tests/unit_tests/test_energy.py | 46 ++- 4 files changed, 636 insertions(+), 564 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 6a87a4a7..303c7035 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -23,7 +23,6 @@ class TeslaCar: def __init__(self, car, controller) -> None: """Initialize EnergySite.""" self._car = car - # Temporary access to controller for now for rewrite self._controller = controller @property @@ -318,6 +317,7 @@ def is_locked(self) -> bool: @property def is_steering_wheel_heater_on(self) -> bool: """Return steering wheel heater.""" + # Not seeing this in the JSON response for 2015 Model S 85D on 28 Aug 2022 return self._controller.get_climate_params(vin=self.vin).get( "steering_wheel_heater" ) @@ -364,7 +364,7 @@ def min_avail_temp(self) -> float: @property def native_heading(self) -> int: """Return native heading.""" - # Not seeing this in the JSON response + # Not seeing this in the JSON response for 2015 Model S 85D on 28 Aug 2022 return self._controller.get_drive_params(vin=self.vin).get("native_heading") @property @@ -437,6 +437,7 @@ def software_update(self) -> dict: @property def steering_wheel_heater(self) -> bool: """Return steering wheel heater option.""" + # Not seeing this in the JSON response for 2015 Model S 85D on 28 Aug 2022 return self._controller.get_climate_params(vin=self.vin).get( "steering_wheel_heater" ) @@ -487,7 +488,7 @@ async def change_charge_limit(self, value: float) -> None: wake_if_asleep=True, ) - if data and data.get("response").get("result") is True: + if data and data["response"]["result"] is True: params = {"charge_limit_soc": int(value)} self._controller.update_charging_params(vin=self.vin, params=params) @@ -499,7 +500,7 @@ async def charge_port_door_close(self) -> None: wake_if_asleep=True, ) - if data and data.get("response").get("result") is True: + if data and data["response"]["result"] is True: params = {"charge_port_door_open": False} self._controller.update_state_params(vin=self.vin, params=params) @@ -511,7 +512,7 @@ async def charge_port_door_open(self) -> None: wake_if_asleep=True, ) - if data and data.get("response").get("result") is True: + if data and data["response"]["result"] is True: params = {"charge_port_door_open": True} self._controller.update_state_params(vin=self.vin, params=params) @@ -540,7 +541,7 @@ async def lock(self): path_vars={"vehicle_id": self.id}, wake_if_asleep=True, ) - if data and data.get("response").get("result") is True: + if data and data["response"]["result"] is True: params = {"locked": True} self._controller.update_state_params(vin=self.vin, params=params) @@ -560,7 +561,7 @@ async def remote_seat_heater_request(self, level: int, seat_id: int) -> None: level=level, wake_if_asleep=True, ) - if data and data.get("response").get("result") is True: + if data and data["response"]["result"] is True: params = {f"seat_heater_{SEAT_NAME_MAP[seat_id]}": level} self._controller.update_climate_params(vin=self.vin, params=params) @@ -571,7 +572,7 @@ def get_seat_heater_status(self, seat_id: int) -> int: async def schedule_software_update(self, offset_sec: Optional[int] = 0) -> None: """Send command to install software update.""" - await self._coordinator.controller.api( + await self._send_command( "SCHEDULE_SOFTWARE_UPDATE", path_vars={"vehicle_id": self.id}, offset_sec=offset_sec, @@ -587,7 +588,7 @@ async def set_charging_amps(self, value: float) -> None: wake_if_asleep=True, ) - if data and data.get("response").get("result") is True: + if data and data["response"]["result"] is True: params = {"charge_amps": int(value)} self._controller.update_charging_params(vin=self.vin, params=params) @@ -615,7 +616,7 @@ async def set_cabin_overheat_protection(self, option: str) -> None: fan_only=fan_only, wake_if_asleep=True, ) - if data and data.get("response").get("result"): + if data and data["response"]["result"]: params = {"cabin_overheat_protection": option} self._controller.update_climate_params(vin=self.vin, params=params) @@ -641,7 +642,7 @@ async def set_heated_steering_wheel(self, value: bool) -> None: wake_if_asleep=True, ) - if data and data.get("response").get("result") is True: + if data and data["response"]["result"] is True: params = {"steering_wheel_heater": value} self._controller.update_climate_params(vin=self.vin, params=params) @@ -683,7 +684,7 @@ async def set_sentry_mode(self, value: bool) -> None: wake_if_asleep=True, ) - if data and data.get("response").get("result") is True: + if data and data["response"]["result"] is True: params = {"sentry_mode": value} self._controller.update_state_params(vin=self.vin, params=params) @@ -696,7 +697,7 @@ async def set_temperature(self, temp: float) -> None: passenger_temp=temp, wake_if_asleep=True, ) - if data and data.get("response").get("result") is True: + if data and data["response"]["result"] is True: params = {"driver_temp_setting": temp} self._controller.update_climate_params(vin=self.vin, params=params) @@ -709,7 +710,7 @@ async def start_charge(self) -> None: wake_if_asleep=True, ) - if data and data.get("response").get("result") is True: + if data and data["response"]["result"] is True: params = {"charging_state": "Charging"} self._controller.update_charging_params(vin=self.vin, params=params) @@ -721,7 +722,7 @@ async def stop_charge(self) -> None: wake_if_asleep=True, ) - if data and data.get("response").get("result") is True: + if data and data["response"]["result"] is True: params = {"charging_state": None} self._controller.update_charging_params(vin=self.vin, params=params) @@ -741,7 +742,7 @@ async def toggle_trunk(self) -> None: which_trunk="rear", wake_if_asleep=True, ) - if data and data.get("response").get("result") is True: + if data and data["response"]["result"] is True: if self.is_trunk_locked: params = {"rt": 0} self._controller.update_state_params(vin=self.vin, params=params) @@ -757,7 +758,7 @@ async def toggle_frunk(self) -> None: which_trunk="front", wake_if_asleep=True, ) - if data and data.get("response").get("result") is True: + if data and data["response"]["result"] is True: if self.is_frunk_locked: params = {"ft": 0} self._controller.update_state_params(vin=self.vin, params=params) @@ -783,10 +784,10 @@ async def trigger_homelink(self) -> None: wake_if_asleep=True, ) - if data and data.get("response"): - _LOGGER.debug("Homelink response: %s", data.get("response")) - result = data.get("response").get("result") - reason = data.get("response").get("reason") + if data and data["response"]: + _LOGGER.debug("Homelink response: %s", data["response"]) + result = data["response"]["result"] + reason = data["response"]["reason"] if result is False: raise HomelinkError(f"Error calling trigger_homelink: {reason}") @@ -797,6 +798,6 @@ async def unlock(self) -> None: path_vars={"vehicle_id": self.id}, wake_if_asleep=True, ) - if data and data.get("response").get("result") is True: + if data and data["response"]["result"] is True: params = {"locked": False} self._controller.update_state_params(vin=self.vin, params=params) diff --git a/tests/tesla_mock.py b/tests/tesla_mock.py index 7f080d63..871ac852 100644 --- a/tests/tesla_mock.py +++ b/tests/tesla_mock.py @@ -18,8 +18,6 @@ def __init__(self, monkeypatch) -> None: """ self._monkeypatch = monkeypatch - # self._monkeypatch.setattr(Controller, "connect", self.mock_connect) - self._monkeypatch.setattr(Controller, "command", self.mock_command) self._monkeypatch.setattr(Controller, "api", self.mock_api) self._monkeypatch.setattr( Controller, "get_charging_params", self.mock_get_charging_params @@ -69,23 +67,14 @@ def __init__(self, monkeypatch) -> None: self._vehicle_data = copy.deepcopy(VEHICLE_DATA) self._site_data = copy.deepcopy(SITE_DATA) self._battery_data = copy.deepcopy(BATTERY_DATA) - self._drive_state = copy.deepcopy(DRIVE_STATE) - self._climate_state = copy.deepcopy(CLIMATE_STATE) - self._charge_state = copy.deepcopy(CHARGE_STATE) - self._gui_settings = copy.deepcopy(GUI_SETTINGS) - self._vehicle_state = copy.deepcopy(VEHICLE_STATE) - self._vehicle_config = copy.deepcopy(VEHICLE_CONFIG) - self._solar_combined_data = copy.deepcopy(SOLAR_COMBINED_DATA) - self._solar_combined_data_no_name = copy.deepcopy(SOLAR_COMBINED_DATA_NO_NAME) + self._drive_state = copy.deepcopy(VEHICLE_DATA["drive_state"]) + self._climate_state = copy.deepcopy(VEHICLE_DATA["climate_state"]) + self._charge_state = copy.deepcopy(VEHICLE_DATA["charge_state"]) + self._gui_settings = copy.deepcopy(VEHICLE_DATA["gui_settings"]) + self._vehicle_state = copy.deepcopy(VEHICLE_DATA["vehicle_state"]) + self._vehicle_config = copy.deepcopy(VEHICLE_DATA["vehicle_config"]) self._site_config = copy.deepcopy(SITE_CONFIG) self._site_data_unknown_grid = copy.deepcopy(SITE_DATA_UNKNOWN_GRID) - self._vehicle = copy.deepcopy(VEHICLE) - self._vehicle["drive_state"] = self._drive_state - self._vehicle["climate_state"] = self._climate_state - self._vehicle["charge_state"] = self._charge_state - self._vehicle["gui_settings"] = self._gui_settings - self._vehicle["vehicle_state"] = self._vehicle_state - self._vehicle["vehicle_config"] = self._vehicle_config def mock_api(self, *args, **kwargs): # pylint: disable=unused-argument @@ -196,11 +185,11 @@ def controller_get_climate_params(self): return self._climate_state def controller_get_power_params(self): - """Monkeypatch for controller.get_climate_params().""" + """Monkeypatch for controller.get_power_params().""" return self._site_data def controller_get_power_unknown_grid_params(self): - """Monkeypatch for controller.get_climate_params().""" + """Monkeypatch for controller.get_power_params() with grid unknown.""" return self._site_data_unknown_grid def controller_get_drive_params(self): @@ -247,7 +236,7 @@ def connection_generate_oauth(): def data_request_vehicle(self): """Simulate the result of vehicle data request.""" - return self._vehicle + return self._vehicle_data def data_request_charge_state(self): """Simulate the result of charge state data request.""" @@ -265,10 +254,6 @@ def data_request_energysites(self): """Similate the result of combined product list & site config request.""" return self._energysites - def data_request_solar_combined_data_no_name(self): - """Similate the result of combined product list & site config without name.""" - return self._solar_combined_data_no_name - def data_request_site_data_unknown_grid(self): """Similate the result of site state with unknown grid data request.""" return self._site_data_unknown_grid @@ -297,7 +282,7 @@ def command_ok(): "id": 12345678901234567, "vehicle_id": 1234567890, "vin": "5YJSA11111111111", - "display_name": "Nikola 2.0", + "display_name": "My Model S", "option_codes": "AD15,MDL3,PBSB,RENA,BT37,ID3W,RF3G,S3PB,DRLH,DV2W,W39B,APF0,COUS,BC3B,CH07,PC30,FC3P,FG31,GLFR,HL31,HM31,IL31,LTPB,MR31,FM3B,RS3H,SA3P,STCP,SC04,SU3C,T3CA,TW00,TM00,UT3P,WR00,AU3P,APH3,AF00,ZCST,MI00,CDM0", "color": None, "access_type": "OWNER", @@ -359,271 +344,236 @@ def command_ok(): }, ] -# Temporary -VEHICLE_DATA = 123 - -DRIVE_STATE = { - "gps_as_of": 1538363883, - "heading": 5, - "latitude": 33.111111, - "longitude": -88.111111, - "native_latitude": 33.111111, - "native_location_supported": 1, - "native_longitude": -88.111111, - "native_type": "wgs", - "power": 0, - "shift_state": None, - "speed": None, - "timestamp": 1538364666096, -} - -CLIMATE_STATE = { - "battery_heater": False, - "battery_heater_no_power": False, - "climate_keeper_mode": "dog", - "defrost_mode": 0, - "driver_temp_setting": 21.6, - "fan_status": 0, - "inside_temp": None, - "is_auto_conditioning_on": None, - "is_climate_on": False, - "is_front_defroster_on": False, - "is_preconditioning": False, - "is_rear_defroster_on": False, - "left_temp_direction": None, - "max_avail_temp": 28.0, - "min_avail_temp": 15.0, - "outside_temp": None, - "passenger_temp_setting": 21.6, - "remote_heater_control_enabled": True, - "right_temp_direction": None, - "seat_heater_left": 3, - "seat_heater_rear_center": 0, - "seat_heater_rear_left": 1, - "seat_heater_rear_left_back": 0, - "seat_heater_rear_right": 1, - "seat_heater_rear_right_back": 0, - "seat_heater_right": 2, - "side_mirror_heaters": False, - "steering_wheel_heater": False, - "timestamp": 1543186971731, - "wiper_blade_heater": False, -} - -CHARGE_STATE = { - "battery_heater_on": False, - "battery_level": 64, - "battery_range": 167.96, - "charge_current_request": 48, - "charge_current_request_max": 48, - "charge_enable_request": True, - "charge_energy_added": 12.41, - "charge_limit_soc": 90, - "charge_limit_soc_max": 100, - "charge_limit_soc_min": 50, - "charge_limit_soc_std": 90, - "charge_miles_added_ideal": 50.0, - "charge_miles_added_rated": 40.0, - "charge_port_cold_weather_mode": False, - "charge_port_door_open": False, - "charge_port_latch": "Engaged", - "charge_rate": 0.0, - "charge_to_max_range": False, - "charger_actual_current": 0, - "charger_phases": None, - "charger_pilot_current": 48, - "charger_power": 0, - "charger_voltage": 0, - "charging_state": "Disconnected", - "conn_charge_cable": "", - "est_battery_range": 118.38, - "fast_charger_brand": "", - "fast_charger_present": False, - "fast_charger_type": "", - "ideal_battery_range": 209.95, - "managed_charging_active": False, - "managed_charging_start_time": None, - "managed_charging_user_canceled": False, - "max_range_charge_counter": 0, - "minutes_to_full_charge": 0, - "not_enough_power_to_heat": False, - "scheduled_charging_pending": False, - "scheduled_charging_start_time": None, - "time_to_full_charge": 0.0, - "timestamp": 1543186971727, - "trip_charging": False, - "usable_battery_level": 64, - "user_charge_enable_request": None, -} - -GUI_SETTINGS = { - "gui_24_hour_time": False, - "gui_charge_rate_units": "mi/hr", - "gui_distance_units": "mi/hr", - "gui_range_display": "Rated", - "gui_temperature_units": "F", - "show_range_units": True, - "timestamp": 1543186971728, -} +CAR_LIST = PRODUCT_LIST[0:1] -VEHICLE_STATE = { - "api_version": 7, - "autopark_state_v2": "standby", - "autopark_style": "standard", - "calendar_supported": True, - "car_version": "2019.40.2.1 38f55d9f9205", - "center_display_state": 0, - "df": 0, - "dr": 0, - "fd_window": 0, - "fp_window": 0, - "ft": 0, - "homelink_device_count": 0, - "homelink_nearby": True, - "is_user_present": False, - "last_autopark_error": "no_error", - "locked": True, - "media_state": {"remote_control_enabled": True}, - "notifications_supported": True, - "odometer": 33561.422505, - "parsed_calendar_supported": True, - "pf": 0, - "pr": 0, - "rd_window": 0, - "remote_start": False, - "remote_start_enabled": True, - "remote_start_supported": True, - "rp_window": 0, - "rt": 0, - "sentry_mode": True, - "sentry_mode_available": True, - "smart_summon_available": True, - "software_update": { - "download_perc": 100, - "expected_duration_sec": 2700, - "install_perc": 10, - "scheduled_time_ms": 1575689678432, - "status": "scheduled", - "version": "2019.40.2.1", - }, - "speed_limit_mode": { - "active": False, - "current_limit_mph": 75.0, - "max_limit_mph": 90, - "min_limit_mph": 50, - "pin_code_set": False, - }, - "summon_standby_mode_enabled": True, - "sun_roof_percent_open": 0, - "sun_roof_state": "unknown", - "timestamp": 1538364666096, - "valet_mode": False, - "valet_pin_needed": True, - "vehicle_name": "Nikola 2.0", -} - -VEHICLE_CONFIG = { - "can_accept_navigation_requests": True, - "can_actuate_trunks": True, - "car_special_type": "base", - "car_type": "models2", - "charge_port_type": "US", - "eu_vehicle": False, - "exterior_color": "White", - "has_air_suspension": True, - "has_ludicrous_mode": False, - "key_version": 1, - "motorized_charge_port": True, - "perf_config": "P2", - "plg": True, - "rear_seat_heaters": 0, - "rear_seat_type": 0, - "rhd": False, - "roof_color": "None", - "seat_type": 2, - "spoiler_type": "None", - "sun_roof_installed": 2, - "third_row_seats": "None", - "timestamp": 1538364666096, - "trim_badging": "p90d", - "use_range_badging": False, - "wheel_type": "AeroTurbine19", -} - -VEHICLE = { +# 2015 Model S 85D from 28 Aug 2022 +VEHICLE_DATA = { "id": 12345678901234567, - "user_id": 123, + "user_id": 123456, "vehicle_id": 1234567890, "vin": "5YJSA11111111111", - "display_name": "Nikola 2.0", - "option_codes": "MDLS,RENA,AF02,APF1,APH2,APPB,AU01,BC0R,BP00,BR00,BS00,CDM0,CH05,PBCW,CW00,DCF0,DRLH,DSH7,DV4W,FG02,FR04,HP00,IDBA,IX01,LP01,ME02,MI01,PF01,PI01,PK00,PS01,PX00,PX4D,QTVB,RFP2,SC01,SP00,SR01,SU01,TM00,TP03,TR00,UTAB,WTAS,X001,X003,X007,X011,X013,X021,X024,X027,X028,X031,X037,X040,X044,YFFC,COUS", + "display_name": "My Model S", + "option_codes": "AD15,MDL3,PBSB,RENA,BT37,ID3W,RF3G,S3PB,DRLH,DV2W,W39B,APF0,COUS,BC3B,CH07,PC30,FC3P,FG31,GLFR,HL31,HM31,IL31,LTPB,MR31,FM3B,RS3H,SA3P,STCP,SC04,SU3C,T3CA,TW00,TM00,UT3P,WR00,AU3P,APH3,AF00,ZCST,MI00,CDM0", "color": None, - "tokens": ["abcdef1234567890", "1234567890abcdef"], + "access_type": "OWNER", + "tokens": ["redacted", "redacted"], "state": "online", "in_service": False, "id_s": "12345678901234567", "calendar_enabled": True, - "api_version": 7, + "api_version": 36, "backseat_token": None, "backseat_token_updated_at": None, - "drive_state": None, - "climate_state": None, - "charge_state": None, - "gui_settings": None, - "vehicle_state": None, - "vehicle_config": None, -} - -ENERGYSITES = [ - { - "energy_site_id": 12345, - "resource_type": "solar", - "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", - "asset_site_id": "12345", - "solar_power": 2260, - "solar_type": "pv_panel", - "storm_mode_enabled": None, - "powerwall_onboarding_settings_set": None, - "sync_grid_alert_enabled": False, - "breaker_alert_enabled": False, - "components": { - "battery": False, - "solar": True, - "solar_type": "pv_panel", - "grid": True, - "load_meter": True, - "market_type": "residential", - }, + "charge_state": { + "battery_heater_on": False, + "battery_level": 78, + "battery_range": 169.08, + "charge_amps": 32, + "charge_current_request": 32, + "charge_current_request_max": 32, + "charge_enable_request": True, + "charge_energy_added": 13.57, + "charge_limit_soc": 80, + "charge_limit_soc_max": 100, + "charge_limit_soc_min": 50, + "charge_limit_soc_std": 90, + "charge_miles_added_ideal": 59.0, + "charge_miles_added_rated": 47.0, + "charge_port_cold_weather_mode": None, + "charge_port_color": "FlashingGreen", + "charge_port_door_open": True, + "charge_port_latch": "Engaged", + "charge_rate": 23.2, + "charge_to_max_range": False, + "charger_actual_current": 32, + "charger_phases": 1, + "charger_pilot_current": 32, + "charger_power": 7, + "charger_voltage": 242, + "charging_state": "Charging", + "conn_charge_cable": "SAE", + "est_battery_range": 150.09, + "fast_charger_brand": "", + "fast_charger_present": False, + "fast_charger_type": "MCSingleWireCAN", + "ideal_battery_range": 213.19, + "managed_charging_active": False, + "managed_charging_start_time": None, + "managed_charging_user_canceled": False, + "max_range_charge_counter": 0, + "minutes_to_full_charge": 15, + "not_enough_power_to_heat": False, + "off_peak_charging_enabled": True, + "off_peak_charging_times": "weekdays", + "off_peak_hours_end_time": 360, + "preconditioning_enabled": False, + "preconditioning_times": "all_week", + "scheduled_charging_mode": "DepartBy", + "scheduled_charging_pending": False, + "scheduled_charging_start_time": None, + "scheduled_charging_start_time_app": 0, + "scheduled_departure_time": 1661515200, + "scheduled_departure_time_minutes": 300, + "supercharger_session_trip_planner": False, + "time_to_full_charge": 0.25, + "timestamp": 1661641175268, + "trip_charging": False, + "usable_battery_level": 78, + "user_charge_enable_request": None, }, - { - "energy_site_id": 67890, - "resource_type": "battery", - "site_name": "My Battery Home", - "id": "212dbc27-333c-45b1-81bb-31e2zd2fs2cm", - "gateway_id": "67890", - "asset_site_id": "67890", - "energy_left": 2864.7368421052633, - "total_pack_energy": 14070, - "percentage_charged": 20.360603000037408, - "battery_type": "ac_powerwall", - "backup_capable": True, - "battery_power": 3080, - "storm_mode_enabled": True, - "powerwall_onboarding_settings_set": True, - "sync_grid_alert_enabled": True, - "breaker_alert_enabled": True, - "components": { - "battery": True, - "battery_type": "ac_powerwall", - "solar": True, - "solar_type": "pv_panel", - "grid": True, - "load_meter": True, - "market_type": "residential", + "climate_state": { + "allow_cabin_overheat_protection": True, + "battery_heater": False, + "battery_heater_no_power": False, + "cabin_overheat_protection": "Off", + "climate_keeper_mode": "off", + "defrost_mode": 0, + "driver_temp_setting": 23.3, + "fan_status": 0, + "hvac_auto_request": "On", + "inside_temp": 35.5, + "is_auto_conditioning_on": False, + "is_climate_on": False, + "is_front_defroster_on": False, + "is_preconditioning": False, + "is_rear_defroster_on": False, + "left_temp_direction": -309, + "max_avail_temp": 28.0, + "min_avail_temp": 15.0, + "outside_temp": 32.5, + "passenger_temp_setting": 23.3, + "remote_heater_control_enabled": False, + "right_temp_direction": -309, + "seat_heater_left": 0, + "seat_heater_right": 0, + "side_mirror_heaters": False, + "supports_fan_only_cabin_overheat_protection": False, + "timestamp": 1661641175268, + "wiper_blade_heater": False, + }, + "drive_state": { + "gps_as_of": 1661641173, + "heading": 182, + "latitude": 33.111111, + "longitude": -88.111111, + "native_latitude": 33.111111, + "native_location_supported": 1, + "native_longitude": -88.111111, + "native_type": "wgs", + "power": -7, + "shift_state": None, + "speed": None, + "timestamp": 1661641175268, + }, + "gui_settings": { + "gui_24_hour_time": False, + "gui_charge_rate_units": "mi/hr", + "gui_distance_units": "mi/hr", + "gui_range_display": "Rated", + "gui_temperature_units": "F", + "show_range_units": True, + "timestamp": 1661641175268, + }, + "vehicle_config": { + "can_accept_navigation_requests": True, + "can_actuate_trunks": True, + "car_special_type": "base", + "car_type": "models", + "charge_port_type": "US", + "dashcam_clip_save_supported": False, + "default_charge_to_max": False, + "driver_assist": "MonoCam", + "ece_restrictions": False, + "efficiency_package": "Default", + "eu_vehicle": False, + "exterior_color": "White", + "front_drive_unit": "NoneOrSmall", + "has_air_suspension": False, + "has_ludicrous_mode": False, + "has_seat_cooling": False, + "headlamp_type": "Hid", + "interior_trim_type": "AllBlack", + "motorized_charge_port": True, + "plg": True, + "pws": False, + "rear_drive_unit": "Small", + "rear_seat_heaters": 0, + "rear_seat_type": 1, + "rhd": False, + "roof_color": "Colored", + "seat_type": 1, + "spoiler_type": "None", + "sun_roof_installed": 0, + "third_row_seats": "None", + "timestamp": 1661641175269, + "trim_badging": "85d", + "use_range_badging": False, + "utc_offset": -25200, + "wheel_type": "Base19", + }, + "vehicle_state": { + "api_version": 36, + "autopark_state_v2": "standby", + "autopark_style": "standard", + "calendar_supported": True, + "car_version": "2022.8.10.1 171f0fe61c20", + "center_display_state": 0, + "dashcam_clip_save_available": False, + "dashcam_state": "", + "df": 0, + "dr": 0, + "fd_window": 0, + "feature_bitmask": "5,0", + "fp_window": 0, + "ft": 0, + "homelink_device_count": 2, + "homelink_nearby": True, + "is_user_present": False, + "last_autopark_error": "no_error", + "locked": False, + "media_state": {"remote_control_enabled": True}, + "notifications_supported": True, + "odometer": 70915.596752, + "parsed_calendar_supported": True, + "pf": 0, + "pr": 0, + "rd_window": 0, + "remote_start": False, + "remote_start_enabled": True, + "remote_start_supported": True, + "rp_window": 0, + "rt": 0, + "santa_mode": 0, + "smart_summon_available": False, + "software_update": { + "download_perc": 0, + "expected_duration_sec": 2700, + "install_perc": 1, + "status": "", + "version": " ", + }, + "speed_limit_mode": { + "active": False, + "current_limit_mph": 85.0, + "max_limit_mph": 90, + "min_limit_mph": 50.0, + "pin_code_set": False, }, + "summon_standby_mode_enabled": False, + "timestamp": 1661641175268, + "tpms_pressure_fl": None, + "tpms_pressure_fr": None, + "tpms_pressure_rl": None, + "tpms_pressure_rr": None, + "valet_mode": False, + "valet_pin_needed": True, + "vehicle_name": "My Model S", }, -] +} +ENERGYSITES = PRODUCT_LIST[1:3] + +# Tesla solar with Tesla inverter (no Powerwalls) SITE_CONFIG = { "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", "site_name": "My Solar Home", @@ -667,183 +617,8 @@ def command_ok(): "country": "US", }, } -# Likely a rare setup simulating a home with two energy sites,one solar system with and -# another without Powerwall. However, this enables testing multiple scenarios. -# Simulates the list returned from Controller.get_energysites() -SOLAR_COMBINED_DATA = [ - { - "energy_site_id": 12345, - "resource_type": "solar", - "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", - "asset_site_id": "12345", - "solar_power": 0, - "solar_type": "pv_panel", - "storm_mode_enabled": None, - "powerwall_onboarding_settings_set": None, - "sync_grid_alert_enabled": False, - "breaker_alert_enabled": False, - "components": { - "solar": True, - "solar_type": "pv_panel", - "battery": False, - "grid": True, - "backup": False, - "gateway": "gateway_type_none", - "load_meter": True, - "tou_capable": False, - "storm_mode_capable": False, - "flex_energy_request_capable": False, - "car_charging_data_supported": False, - "off_grid_vehicle_charging_reserve_supported": False, - "vehicle_charging_performance_view_enabled": False, - "vehicle_charging_solar_offset_view_enabled": False, - "battery_solar_offset_view_enabled": False, - "energy_service_self_scheduling_enabled": True, - "rate_plan_manager_supported": True, - "configurable": False, - "grid_services_enabled": False, - }, - "load_power": 0, - "grid_power": 0, - "battery_power": 0, - "site_name": "My Solar Home", - "site_number": "STE16235182-31459", - "installation_date": "2022-02-07T13:51:26-07:00", - "user_settings": { - "storm_mode_enabled": None, - "powerwall_onboarding_settings_set": None, - "sync_grid_alert_enabled": False, - "breaker_alert_enabled": False, - }, - "installation_time_zone": "America/Los_Angeles", - "time_zone_offset": -420, - "geolocation": {"latitude": 31.12345600000001, "longitude": -119.1234567}, - "address": { - "address_line1": "1234 Tesla Energy Ave", - "city": "Austin", - "state": "TX", - "zip": "12345", - "country": "US", - }, - }, - { - "energy_site_id": 67890, - "resource_type": "battery", - "site_name": "My Battery Home", - "id": "212dbc27-333c-45b1-81bb-31e2zd2fs2cm", - "gateway_id": "67890", - "asset_site_id": "67890", - "energy_left": 2864.7368421052633, - "total_pack_energy": 14070, - "percentage_charged": 20.360603000037408, - "battery_type": "ac_powerwall", - "backup_capable": True, - "storm_mode_enabled": True, - "powerwall_onboarding_settings_set": True, - "sync_grid_alert_enabled": True, - "breaker_alert_enabled": True, - "components": { - "battery": True, - "battery_type": "ac_powerwall", - "solar": True, - "solar_type": "pv_panel", - "grid": True, - "load_meter": True, - "market_type": "residential", - }, - "solar_power": 0, - "load_power": 0, - "grid_power": 0, - "battery_power": 0, - }, -] - -SOLAR_COMBINED_DATA_NO_NAME = { - "energy_site_id": 12345, - "resource_type": "solar", - "id": "313dbc37-555c-45b1-83aa-62a4ef9ff7ac", - "asset_site_id": "12345", - "solar_power": 0, - "solar_type": "pv_panel", - "storm_mode_enabled": None, - "powerwall_onboarding_settings_set": None, - "sync_grid_alert_enabled": False, - "breaker_alert_enabled": False, - "components": { - "solar": True, - "solar_type": "pv_panel", - "battery": False, - "grid": True, - "backup": False, - "gateway": "gateway_type_none", - "load_meter": True, - "tou_capable": False, - "storm_mode_capable": False, - "flex_energy_request_capable": False, - "car_charging_data_supported": False, - "off_grid_vehicle_charging_reserve_supported": False, - "vehicle_charging_performance_view_enabled": False, - "vehicle_charging_solar_offset_view_enabled": False, - "battery_solar_offset_view_enabled": False, - "energy_service_self_scheduling_enabled": True, - "rate_plan_manager_supported": True, - "configurable": False, - "grid_services_enabled": False, - }, - "load_power": 0, - "grid_power": 0, - "battery_power": 0, - "site_number": "STE16235182-31459", - "installation_date": "2022-02-07T13:51:26-07:00", - "user_settings": { - "storm_mode_enabled": None, - "powerwall_onboarding_settings_set": None, - "sync_grid_alert_enabled": False, - "breaker_alert_enabled": False, - }, - "installation_time_zone": "America/Los_Angeles", - "time_zone_offset": -420, - "geolocation": {"latitude": 31.12345600000001, "longitude": -119.1234567}, - "address": { - "address_line1": "1234 Tesla Energy Ave", - "city": "Austin", - "state": "TX", - "zip": "12345", - "country": "US", - }, -} -# Data added from Controller.connect() initialization (solar_power, load_power, etc.) -# BATTERY_COMBINED_DATA = { -# "energy_site_id": 67890, -# "resource_type": "battery", -# "site_name": "My Battery Home", -# "id": "212dbc27-333c-45b1-81bb-31e2zd2fs2cm", -# "gateway_id": "67890", -# "asset_site_id": "67890", -# "energy_left": 2864.7368421052633, -# "total_pack_energy": 14070, -# "percentage_charged": 20.360603000037408, -# "battery_type": "ac_powerwall", -# "backup_capable": True, -# "battery_power": 0, -# "storm_mode_enabled": True, -# "powerwall_onboarding_settings_set": True, -# "sync_grid_alert_enabled": True, -# "breaker_alert_enabled": True, -# "components": { -# "battery": True, -# "battery_type": "ac_powerwall", -# "solar": True, -# "solar_type": "pv_panel", -# "grid": True, -# "load_meter": True, -# "market_type": "residential", -# }, -# "solar_power": 0, -# "load_power": 0, -# "grid_power": 0, -# } +# Tesla solar with Tesla inverter (no Powerwalls) SITE_DATA = { "solar_power": 7720, "energy_left": 0, diff --git a/tests/unit_tests/test_car.py b/tests/unit_tests/test_car.py index df34ca2a..351d174b 100644 --- a/tests/unit_tests/test_car.py +++ b/tests/unit_tests/test_car.py @@ -4,154 +4,442 @@ from teslajsonpy.controller import Controller -from tests.tesla_mock import TeslaMock +from tests.tesla_mock import ( + TeslaMock, + VEHICLE_DATA, + VIN, +) @pytest.mark.asyncio async def test_car_properties(monkeypatch): - """Test SolarSite class.""" + """Test TeslaCar class properties.""" TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _car = _controller.cars["5YJSA11111111111"] + _car = _controller.cars[VIN] assert _car._car is not None assert _car._controller is not None - assert _car.display_name == "Nikola 2.0" + assert _car.display_name == VEHICLE_DATA["display_name"] - assert _car.id == 12345678901234567 + assert _car.id == VEHICLE_DATA["id"] - assert _car.state == "online" + assert _car.state == VEHICLE_DATA["state"] - assert _car.vehicle_id == 1234567890 + assert _car.vehicle_id == VEHICLE_DATA["vehicle_id"] - assert _car.vin == "5YJSA11111111111" + assert _car.vin == VEHICLE_DATA["vin"] - # assert _car.data_available == True + assert _car.data_available - assert _car.battery_level == 64 + assert _car.battery_level == VEHICLE_DATA["charge_state"]["battery_level"] - assert _car.battery_range == 167.96 + assert _car.battery_range == VEHICLE_DATA["charge_state"]["battery_range"] - # assert _car.cabin_overheat_protection == "" + assert ( + _car.cabin_overheat_protection + == VEHICLE_DATA["climate_state"]["cabin_overheat_protection"] + ) assert _car.car_type == "Model S" - # assert _car.car_version == "" + assert _car.car_version == VEHICLE_DATA["vehicle_state"]["car_version"] - assert _car.charger_actual_current == 0 + assert ( + _car.charger_actual_current + == VEHICLE_DATA["charge_state"]["charger_actual_current"] + ) - assert _car.charge_current_request == 48 + assert ( + _car.charge_current_request + == VEHICLE_DATA["charge_state"]["charge_current_request"] + ) - assert _car.charge_current_request_max == 48 + assert ( + _car.charge_current_request_max + == VEHICLE_DATA["charge_state"]["charge_current_request_max"] + ) - assert _car.charge_port_latch == "Engaged" + assert _car.charge_port_latch == VEHICLE_DATA["charge_state"]["charge_port_latch"] - assert _car.charge_energy_added == 12.41 + assert ( + _car.charge_energy_added == VEHICLE_DATA["charge_state"]["charge_energy_added"] + ) - assert _car.charge_limit_soc == 90 + assert _car.charge_limit_soc == VEHICLE_DATA["charge_state"]["charge_limit_soc"] - assert _car.charge_limit_soc_max == 100 + assert ( + _car.charge_limit_soc_max + == VEHICLE_DATA["charge_state"]["charge_limit_soc_max"] + ) - assert _car.charge_limit_soc_min == 50 + assert ( + _car.charge_limit_soc_min + == VEHICLE_DATA["charge_state"]["charge_limit_soc_min"] + ) - assert _car.charge_miles_added_ideal == 50.0 + assert ( + _car.charge_miles_added_ideal + == VEHICLE_DATA["charge_state"]["charge_miles_added_ideal"] + ) - assert _car.charge_miles_added_rated == 40.0 + assert ( + _car.charge_miles_added_rated + == VEHICLE_DATA["charge_state"]["charge_miles_added_rated"] + ) - assert _car.charger_phases is None + assert _car.charger_phases == VEHICLE_DATA["charge_state"]["charger_phases"] - assert _car.charger_power == 0 + assert _car.charger_power == VEHICLE_DATA["charge_state"]["charger_power"] - assert _car.charge_rate == 0.0 + assert _car.charge_rate == VEHICLE_DATA["charge_state"]["charge_rate"] - assert _car.charging_state == "Disconnected" + assert _car.charging_state == VEHICLE_DATA["charge_state"]["charging_state"] - assert _car.charger_voltage == 0 + assert _car.charger_voltage == VEHICLE_DATA["charge_state"]["charger_voltage"] - assert _car.climate_keeper_mode == "dog" + assert ( + _car.climate_keeper_mode == VEHICLE_DATA["climate_state"]["climate_keeper_mode"] + ) - assert _car.conn_charge_cable == "" + assert _car.conn_charge_cable == VEHICLE_DATA["charge_state"]["conn_charge_cable"] - assert _car.defrost_mode == 0 + assert _car.defrost_mode == VEHICLE_DATA["climate_state"]["defrost_mode"] - assert _car.driver_temp_setting == 21.6 + assert ( + _car.driver_temp_setting == VEHICLE_DATA["climate_state"]["driver_temp_setting"] + ) - assert not _car.fast_charger_present + assert ( + _car.fast_charger_present + == VEHICLE_DATA["charge_state"]["fast_charger_present"] + ) - assert _car.fast_charger_brand == "" + assert _car.fast_charger_brand == VEHICLE_DATA["charge_state"]["fast_charger_brand"] - assert _car.fast_charger_type == "" + assert _car.fast_charger_type == VEHICLE_DATA["charge_state"]["fast_charger_type"] - assert _car.gui_distance_units == "mi/hr" + assert _car.gui_distance_units == VEHICLE_DATA["gui_settings"]["gui_distance_units"] - assert _car.gui_range_display == "Rated" + assert _car.gui_range_display == VEHICLE_DATA["gui_settings"]["gui_range_display"] - assert _car.homelink_device_count == 0 + assert _car.heading == VEHICLE_DATA["drive_state"]["heading"] - assert _car.homelink_nearby + assert ( + _car.homelink_device_count + == VEHICLE_DATA["vehicle_state"]["homelink_device_count"] + ) - assert _car.ideal_battery_range == 209.95 + assert _car.homelink_nearby == VEHICLE_DATA["vehicle_state"]["homelink_nearby"] - assert _car.inside_temp is None + assert ( + _car.ideal_battery_range == VEHICLE_DATA["charge_state"]["ideal_battery_range"] + ) - assert not _car.is_charge_port_door_open + assert _car.inside_temp == VEHICLE_DATA["climate_state"]["inside_temp"] - assert not _car.is_climate_on + assert ( + _car.is_charge_port_door_open + == VEHICLE_DATA["charge_state"]["charge_port_door_open"] + ) + + assert _car.is_climate_on == VEHICLE_DATA["climate_state"]["is_climate_on"] assert _car.is_frunk_locked - assert _car.is_locked + assert _car.is_locked == VEHICLE_DATA["vehicle_state"]["locked"] - assert not _car.is_steering_wheel_heater_on + assert _car.is_steering_wheel_heater_on == VEHICLE_DATA["climate_state"].get( + "steering_wheel_heater" + ) assert _car.is_trunk_locked - # assert _car.is_on == "" + assert _car.is_on + + assert _car.longitude == VEHICLE_DATA["drive_state"]["longitude"] + + assert _car.latitude == VEHICLE_DATA["drive_state"]["latitude"] + + assert _car.max_avail_temp == VEHICLE_DATA["climate_state"]["max_avail_temp"] + + assert _car.min_avail_temp == VEHICLE_DATA["climate_state"]["min_avail_temp"] + + assert _car.native_heading == VEHICLE_DATA["drive_state"].get("native_heading") + + assert ( + _car.native_location_supported + == VEHICLE_DATA["drive_state"]["native_location_supported"] + ) + + assert _car.native_longitude == VEHICLE_DATA["drive_state"]["native_longitude"] + + assert _car.native_latitude == VEHICLE_DATA["drive_state"]["native_latitude"] + + assert _car.odometer == VEHICLE_DATA["vehicle_state"]["odometer"] + + assert _car.outside_temp == VEHICLE_DATA["climate_state"]["outside_temp"] + + assert _car.rear_heated_seats == VEHICLE_DATA["climate_state"].get( + "seat_heater_rear_left", False + ) + + assert _car.sentry_mode == VEHICLE_DATA["vehicle_state"].get("sentry_mode") + + assert _car.sentry_mode_available == VEHICLE_DATA["vehicle_state"].get( + "sentry_mode_available" + ) + + assert _car.shift_state == VEHICLE_DATA["drive_state"]["shift_state"] + + assert _car.speed == VEHICLE_DATA["drive_state"]["speed"] + + assert _car.software_update == VEHICLE_DATA["vehicle_state"]["software_update"] + + assert _car.steering_wheel_heater == VEHICLE_DATA["climate_state"].get( + "steering_wheel_heater" + ) + + assert _car.third_row_seats == VEHICLE_DATA["vehicle_state"].get("third_row_seats") + + assert ( + _car.time_to_full_charge == VEHICLE_DATA["charge_state"]["time_to_full_charge"] + ) + + +@pytest.mark.asyncio +async def test_change_charge_limit(monkeypatch): + """Test change charge limit.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] + + assert await _car.change_charge_limit(70.0) is None + + +@pytest.mark.asyncio +async def test_charge_port_door_open_close(monkeypatch): + """Test charge port door open/close command.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] + + assert await _car.charge_port_door_open() is None + + assert await _car.charge_port_door_close() is None + + +@pytest.mark.asyncio +async def test_flash_lights(monkeypatch): + """Test flash lights command.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] + + assert await _car.flash_lights() is None + + +@pytest.mark.asyncio +async def test_honk_horn(monkeypatch): + """Test honk horn command.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] + + assert await _car.honk_horn() is None + + +@pytest.mark.asyncio +async def test_lock(monkeypatch): + """Test lock command.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] + + assert await _car.lock() is None + + +@pytest.mark.asyncio +async def test_remote_seat_heater_request(monkeypatch): + """Test remote seat heater request.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] + + assert await _car.remote_seat_heater_request(3, 1) is None + + +@pytest.mark.asyncio +async def test_schedule_software_update(monkeypatch): + """Test scheduling software update.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] + + assert await _car.schedule_software_update() is None + + +@pytest.mark.asyncio +async def test_set_charging_amps(monkeypatch): + """Test setting charging amps.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] + + assert await _car.set_charging_amps(32.0) is None + + +@pytest.mark.asyncio +async def test_set_cabin_overheat_protection(monkeypatch): + """Test setting heated steering wheel.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] - assert _car.longitude == -88.111111 + assert await _car.set_cabin_overheat_protection("On") is None - assert _car.latitude == 33.111111 - assert _car.max_avail_temp == 28.0 +@pytest.mark.asyncio +async def test_set_climate_keeper_mode(monkeypatch): + """Test setting climate keeper mode.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] + + assert await _car.set_climate_keeper_mode(1) is None + + +@pytest.mark.asyncio +async def test_set_heated_steering_wheel(monkeypatch): + """Test setting heated steering wheel.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] - assert _car.min_avail_temp == 15.0 + assert await _car.set_heated_steering_wheel(True) is None - # assert not _car.native_heading - assert _car.native_location_supported == 1 +@pytest.mark.asyncio +async def test_set_hvac_mode(monkeypatch): + """Test setting HVAC mode.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] - assert _car.native_longitude == -88.111111 + assert await _car.set_hvac_mode("on") is None - assert _car.native_latitude == 33.111111 - assert _car.odometer == 33561.422505 +@pytest.mark.asyncio +async def test_set_max_defrost(monkeypatch): + """Test wake up.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] - assert not _car.outside_temp + assert await _car.set_max_defrost(2) is None - assert _car.rear_heated_seats - assert _car.sentry_mode +@pytest.mark.asyncio +async def test_set_sentry_mode(monkeypatch): + """Test wake up.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] - assert _car.sentry_mode_available + assert await _car.set_sentry_mode(True) is None - assert not _car.shift_state - assert not _car.speed +@pytest.mark.asyncio +async def test_set_temperature(monkeypatch): + """Test wake up.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] - assert _car.software_update == { - "download_perc": 100, - "expected_duration_sec": 2700, - "install_perc": 10, - "scheduled_time_ms": 1575689678432, - "status": "scheduled", - "version": "2019.40.2.1", - } + assert await _car.set_temperature(22.0) is None - assert not _car.steering_wheel_heater - assert not _car.third_row_seats +@pytest.mark.asyncio +async def test_start_stop_charge(monkeypatch): + """Test wake up.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] + + assert await _car.start_charge() is None + + assert await _car.stop_charge() is None + + +@pytest.mark.asyncio +async def test_wake_up(monkeypatch): + """Test wake up.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] + + assert await _car.wake_up() is None + + +@pytest.mark.asyncio +async def test_toggle_trunk(monkeypatch): + """Test toggle trunk.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] + + assert await _car.toggle_trunk() is None + + +@pytest.mark.asyncio +async def test_toggle_frunk(monkeypatch): + """Test toggle frunk.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] + + assert await _car.toggle_frunk() is None + + +@pytest.mark.asyncio +async def test_trigger_homelink(monkeypatch): + """Test unlock.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] + + assert await _car.trigger_homelink() is None + + +@pytest.mark.asyncio +async def test_unlock(monkeypatch): + """Test unlock.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _car = _controller.cars[VIN] - assert _car.time_to_full_charge == 0.0 + assert await _car.unlock() is None diff --git a/tests/unit_tests/test_energy.py b/tests/unit_tests/test_energy.py index 4de28eb8..bb2a774e 100644 --- a/tests/unit_tests/test_energy.py +++ b/tests/unit_tests/test_energy.py @@ -2,10 +2,11 @@ import pytest +from teslajsonpy.const import DEFAULT_ENERGYSITE_NAME from teslajsonpy.controller import Controller from teslajsonpy.energy import EnergySite -from tests.tesla_mock import TeslaMock +from tests.tesla_mock import TeslaMock, ENERGYSITES, SITE_CONFIG @pytest.mark.asyncio @@ -19,8 +20,8 @@ async def test_energysite_setup(monkeypatch): powerwall_site = _controller.energysites[67890] assert _controller.energysites is not None - assert solar_site.resource_type == "solar" - assert powerwall_site.resource_type == "battery" + assert solar_site.resource_type == ENERGYSITES[0]["resource_type"] + assert powerwall_site.resource_type == ENERGYSITES[1]["resource_type"] @pytest.mark.asyncio @@ -39,18 +40,19 @@ async def test_solar_site(monkeypatch): "load_power": 0, "grid_power": 0, "battery_power": 0, + "battery_percentage": 0, } - assert _solar_site.energysite_id == 12345 - assert _solar_site.has_load_meter - assert _solar_site.id == "313dbc37-555c-45b1-83aa-62a4ef9ff7ac" - assert _solar_site.resource_type == "solar" - assert _solar_site.site_name == "My Solar Home" + assert _solar_site.energysite_id == ENERGYSITES[0]["energy_site_id"] + assert _solar_site.has_load_meter == ENERGYSITES[0]["components"]["load_meter"] + assert _solar_site.id == ENERGYSITES[0]["id"] + assert _solar_site.resource_type == ENERGYSITES[0]["resource_type"] + assert _solar_site.site_name == SITE_CONFIG["site_name"] assert _solar_site.grid_power == 0 assert _solar_site.load_power == 0 assert _solar_site.solar_power == 0 - assert _solar_site.solar_type == "pv_panel" + assert _solar_site.solar_type == ENERGYSITES[0]["components"]["solar_type"] @pytest.mark.asyncio @@ -69,20 +71,26 @@ async def test_powerwall_site(monkeypatch): "load_power": 0, "grid_power": 0, "battery_power": 0, + "battery_percentage": 0, } - assert _solar_powerwall_site.energysite_id == 67890 - assert _solar_powerwall_site.has_load_meter - assert _solar_powerwall_site.id == "212dbc27-333c-45b1-81bb-31e2zd2fs2cm" - assert _solar_powerwall_site.resource_type == "battery" - assert _solar_powerwall_site.site_name == "My Battery Home" - - # assert _solar_powerwall_site.battery_percent == 0 - # assert _solar_powerwall_site.battery_power == 0 + assert _solar_powerwall_site.energysite_id == ENERGYSITES[1]["energy_site_id"] + assert ( + _solar_powerwall_site.has_load_meter + == ENERGYSITES[1]["components"]["load_meter"] + ) + assert _solar_powerwall_site.id == ENERGYSITES[1]["id"] + assert _solar_powerwall_site.resource_type == ENERGYSITES[1]["resource_type"] + assert _solar_powerwall_site.site_name == ENERGYSITES[1]["site_name"] + + assert _solar_powerwall_site.battery_percent == 0 + assert _solar_powerwall_site.battery_power == 0 assert _solar_powerwall_site.grid_power == 0 assert _solar_powerwall_site.load_power == 0 assert _solar_powerwall_site.solar_power == 0 - assert _solar_powerwall_site.solar_type == "pv_panel" + assert ( + _solar_powerwall_site.solar_type == ENERGYSITES[1]["components"]["solar_type"] + ) @pytest.mark.asyncio @@ -94,7 +102,7 @@ async def test_energysite_with_no_name(monkeypatch): _power_data = _mock.controller_get_power_params() _sensor = EnergySite(_api, _energysite, _power_data) - assert _sensor.site_name == "My Home" + assert _sensor.site_name == DEFAULT_ENERGYSITE_NAME # Test reponse with "grid_status" of "Unknown" From c15f073520585ccee7dbc09550f7021b932ae38e Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 28 Aug 2022 17:45:57 -0700 Subject: [PATCH 34/84] Remove creating objects in connect method --- poetry.lock | 23 +++++++---------------- teslajsonpy/controller.py | 11 ++++++----- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/poetry.lock b/poetry.lock index 383a012a..db07f818 100644 --- a/poetry.lock +++ b/poetry.lock @@ -207,7 +207,7 @@ python-versions = ">=3.6" [[package]] name = "charset-normalizer" -version = "2.1.0" +version = "2.1.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." category = "main" optional = false @@ -263,7 +263,7 @@ graph = ["objgraph (>=1.7.2)"] [[package]] name = "distlib" -version = "0.3.5" +version = "0.3.6" description = "Distribution utilities" category = "dev" optional = false @@ -998,7 +998,7 @@ python-versions = ">=3.5" [[package]] name = "urllib3" -version = "1.26.11" +version = "1.26.12" description = "HTTP library with thread-safe connection pooling, file post, and more." category = "dev" optional = false @@ -1006,7 +1006,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, [package.extras] brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"] +secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] @@ -1219,10 +1219,7 @@ certifi = [ {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, ] -charset-normalizer = [ - {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, - {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, -] +charset-normalizer = [] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, @@ -1287,10 +1284,7 @@ dill = [ {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, ] -distlib = [ - {file = "distlib-0.3.5-py2.py3-none-any.whl", hash = "sha256:b710088c59f06338ca514800ad795a132da19fda270e3ce4affc74abf955a26c"}, - {file = "distlib-0.3.5.tar.gz", hash = "sha256:a7f75737c70be3b25e2bee06288cec4e4c221de18455b2dd037fe2a795cab2fe"}, -] +distlib = [] docutils = [] filelock = [] flake8 = [] @@ -1702,10 +1696,7 @@ unidecode = [ {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, ] -urllib3 = [ - {file = "urllib3-1.26.11-py2.py3-none-any.whl", hash = "sha256:c33ccba33c819596124764c23a97d25f32b28433ba0dedeb77d873a38722c9bc"}, - {file = "urllib3-1.26.11.tar.gz", hash = "sha256:ea6e8fb210b19d950fab93b60c9009226c63a28808bc8386e05301e25883ac0a"}, -] +urllib3 = [] virtualenv = [] wrapt = [ {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index ef9a0fd3..2ba78865 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -435,9 +435,6 @@ async def connect( except (TeslaException, RetryLimitError): pass - self._generate_car_objects() - self._generate_energysite_objects() - return { "refresh_token": self.__connection.refresh_token, "access_token": self.__connection.access_token, @@ -532,13 +529,15 @@ async def get_site_config(self, energysite_id: int) -> dict: "response" ] - def _generate_car_objects(self) -> None: + def generate_car_objects(self) -> Dict[str, TeslaCar]: """Generate car objects.""" for car in self.__vehicle_list: vin = car["vin"] self.cars[vin] = TeslaCar(car, self) - def _generate_energysite_objects(self) -> None: + return self.cars + + def generate_energysite_objects(self) -> Dict[int, EnergySite]: """Generate energy site objects.""" for energysite in self.__energysite_list: energysite_id = energysite["energy_site_id"] @@ -564,6 +563,8 @@ def _generate_energysite_objects(self) -> None: self.api, energysite, self.__power_data[energysite_id] ) + return self.energysites + async def _wake_up(self, car_id): car_vin = self._id_to_vin(car_id) car_id = self._update_id(car_id) From a8416d54ae5d08beef9d14c7e41ee6ceb3e30187 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 28 Aug 2022 18:24:23 -0700 Subject: [PATCH 35/84] Update docs and clean up --- README.md | 2 + docs/teslajsonpy/teslajsonpy.car.rst | 10 + docs/teslajsonpy/teslajsonpy.energy.rst | 10 + .../teslajsonpy.homeassistant.alerts.rst | 10 - ...slajsonpy.homeassistant.battery_sensor.rst | 10 - ...eslajsonpy.homeassistant.binary_sensor.rst | 10 - .../teslajsonpy.homeassistant.charger.rst | 10 - .../teslajsonpy.homeassistant.climate.rst | 10 - .../teslajsonpy.homeassistant.gps.rst | 10 - ...teslajsonpy.homeassistant.heated_seats.rst | 10 - ...py.homeassistant.heated_steering_wheel.rst | 10 - .../teslajsonpy.homeassistant.homelink.rst | 10 - .../teslajsonpy.homeassistant.lock.rst | 10 - .../teslajsonpy.homeassistant.power.rst | 10 - .../teslajsonpy/teslajsonpy.homeassistant.rst | 32 --- .../teslajsonpy.homeassistant.sentry_mode.rst | 10 - .../teslajsonpy.homeassistant.trunk.rst | 10 - .../teslajsonpy.homeassistant.vehicle.rst | 10 - ...teslajsonpy.homeassistant.vehicle_data.rst | 10 - docs/teslajsonpy/teslajsonpy.rst | 213 ++---------------- teslajsonpy/car.py | 18 +- teslajsonpy/connection.py | 7 +- teslajsonpy/controller.py | 13 +- teslajsonpy/energy.py | 1 - 24 files changed, 59 insertions(+), 397 deletions(-) create mode 100644 docs/teslajsonpy/teslajsonpy.car.rst create mode 100644 docs/teslajsonpy/teslajsonpy.energy.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.alerts.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.battery_sensor.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.binary_sensor.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.charger.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.climate.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.gps.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.heated_seats.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.heated_steering_wheel.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.homelink.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.lock.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.power.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.sentry_mode.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.trunk.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.vehicle.rst delete mode 100644 docs/teslajsonpy/teslajsonpy.homeassistant.vehicle_data.rst diff --git a/README.md b/README.md index 33ea077a..6cd6f8fa 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # teslajsonpy + [![Version status](https://img.shields.io/pypi/status/teslajsonpy)](https://pypi.org/project/teslajsonpy) [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![Python version compatibility](https://img.shields.io/pypi/pyversions/teslajsonpy)](https://pypi.org/project/teslajsonpy) @@ -35,6 +36,7 @@ Also thanks to [Tim Dorr](https://tesla-api.timdorr.com/) for documenting the AP 12. Submit a [pull request](https://github.com/zabuldon/teslajsonpy/pulls)! # Documentation + [API docs](https://teslajsonpy.readthedocs.io/en/latest/). # License diff --git a/docs/teslajsonpy/teslajsonpy.car.rst b/docs/teslajsonpy/teslajsonpy.car.rst new file mode 100644 index 00000000..f7044be2 --- /dev/null +++ b/docs/teslajsonpy/teslajsonpy.car.rst @@ -0,0 +1,10 @@ +========================== +``teslajsonpy.car`` +========================== + +.. automodule:: teslajsonpy.car + + .. contents:: + :local: + +.. currentmodule:: teslajsonpy.car diff --git a/docs/teslajsonpy/teslajsonpy.energy.rst b/docs/teslajsonpy/teslajsonpy.energy.rst new file mode 100644 index 00000000..39d5ca6e --- /dev/null +++ b/docs/teslajsonpy/teslajsonpy.energy.rst @@ -0,0 +1,10 @@ +========================== +``teslajsonpy.energy`` +========================== + +.. automodule:: teslajsonpy.energy + + .. contents:: + :local: + +.. currentmodule:: teslajsonpy.energy diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.alerts.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.alerts.rst deleted file mode 100644 index 95d9bfc3..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.alerts.rst +++ /dev/null @@ -1,10 +0,0 @@ -==================================== -``teslajsonpy.homeassistant.alerts`` -==================================== - -.. automodule:: teslajsonpy.homeassistant.alerts - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.alerts diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.battery_sensor.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.battery_sensor.rst deleted file mode 100644 index 4d41d8eb..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.battery_sensor.rst +++ /dev/null @@ -1,10 +0,0 @@ -============================================ -``teslajsonpy.homeassistant.battery_sensor`` -============================================ - -.. automodule:: teslajsonpy.homeassistant.battery_sensor - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.battery_sensor diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.binary_sensor.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.binary_sensor.rst deleted file mode 100644 index 1af9c79f..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.binary_sensor.rst +++ /dev/null @@ -1,10 +0,0 @@ -=========================================== -``teslajsonpy.homeassistant.binary_sensor`` -=========================================== - -.. automodule:: teslajsonpy.homeassistant.binary_sensor - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.binary_sensor diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.charger.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.charger.rst deleted file mode 100644 index 2f2638b1..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.charger.rst +++ /dev/null @@ -1,10 +0,0 @@ -===================================== -``teslajsonpy.homeassistant.charger`` -===================================== - -.. automodule:: teslajsonpy.homeassistant.charger - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.charger diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.climate.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.climate.rst deleted file mode 100644 index ea61da69..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.climate.rst +++ /dev/null @@ -1,10 +0,0 @@ -===================================== -``teslajsonpy.homeassistant.climate`` -===================================== - -.. automodule:: teslajsonpy.homeassistant.climate - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.climate diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.gps.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.gps.rst deleted file mode 100644 index 3cb430bb..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.gps.rst +++ /dev/null @@ -1,10 +0,0 @@ -================================= -``teslajsonpy.homeassistant.gps`` -================================= - -.. automodule:: teslajsonpy.homeassistant.gps - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.gps diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.heated_seats.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.heated_seats.rst deleted file mode 100644 index e85962bc..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.heated_seats.rst +++ /dev/null @@ -1,10 +0,0 @@ -========================================== -``teslajsonpy.homeassistant.heated_seats`` -========================================== - -.. automodule:: teslajsonpy.homeassistant.heated_seats - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.heated_seats diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.heated_steering_wheel.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.heated_steering_wheel.rst deleted file mode 100644 index e76239b6..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.heated_steering_wheel.rst +++ /dev/null @@ -1,10 +0,0 @@ -=================================================== -``teslajsonpy.homeassistant.heated_steering_wheel`` -=================================================== - -.. automodule:: teslajsonpy.homeassistant.heated_steering_wheel - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.heated_steering_wheel diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.homelink.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.homelink.rst deleted file mode 100644 index 08c59fa7..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.homelink.rst +++ /dev/null @@ -1,10 +0,0 @@ -====================================== -``teslajsonpy.homeassistant.homelink`` -====================================== - -.. automodule:: teslajsonpy.homeassistant.homelink - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.homelink diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.lock.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.lock.rst deleted file mode 100644 index 386704bb..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.lock.rst +++ /dev/null @@ -1,10 +0,0 @@ -================================== -``teslajsonpy.homeassistant.lock`` -================================== - -.. automodule:: teslajsonpy.homeassistant.lock - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.lock diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.power.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.power.rst deleted file mode 100644 index 1906c285..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.power.rst +++ /dev/null @@ -1,10 +0,0 @@ -=================================== -``teslajsonpy.homeassistant.power`` -=================================== - -.. automodule:: teslajsonpy.homeassistant.power - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.power diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.rst deleted file mode 100644 index 098c134e..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.rst +++ /dev/null @@ -1,32 +0,0 @@ -============================= -``teslajsonpy.homeassistant`` -============================= - -.. automodule:: teslajsonpy.homeassistant - - .. contents:: - :local: - - -Submodules -========== - -.. toctree:: - - teslajsonpy.homeassistant.alerts - teslajsonpy.homeassistant.battery_sensor - teslajsonpy.homeassistant.binary_sensor - teslajsonpy.homeassistant.charger - teslajsonpy.homeassistant.climate - teslajsonpy.homeassistant.gps - teslajsonpy.homeassistant.heated_seats - teslajsonpy.homeassistant.heated_steering_wheel - teslajsonpy.homeassistant.homelink - teslajsonpy.homeassistant.lock - teslajsonpy.homeassistant.power - teslajsonpy.homeassistant.sentry_mode - teslajsonpy.homeassistant.trunk - teslajsonpy.homeassistant.vehicle - teslajsonpy.homeassistant.vehicle_data - -.. currentmodule:: teslajsonpy.homeassistant diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.sentry_mode.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.sentry_mode.rst deleted file mode 100644 index e631a543..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.sentry_mode.rst +++ /dev/null @@ -1,10 +0,0 @@ -========================================= -``teslajsonpy.homeassistant.sentry_mode`` -========================================= - -.. automodule:: teslajsonpy.homeassistant.sentry_mode - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.sentry_mode diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.trunk.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.trunk.rst deleted file mode 100644 index a0f3a2ad..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.trunk.rst +++ /dev/null @@ -1,10 +0,0 @@ -=================================== -``teslajsonpy.homeassistant.trunk`` -=================================== - -.. automodule:: teslajsonpy.homeassistant.trunk - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.trunk diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.vehicle.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.vehicle.rst deleted file mode 100644 index 363d607d..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.vehicle.rst +++ /dev/null @@ -1,10 +0,0 @@ -===================================== -``teslajsonpy.homeassistant.vehicle`` -===================================== - -.. automodule:: teslajsonpy.homeassistant.vehicle - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.vehicle diff --git a/docs/teslajsonpy/teslajsonpy.homeassistant.vehicle_data.rst b/docs/teslajsonpy/teslajsonpy.homeassistant.vehicle_data.rst deleted file mode 100644 index ed993f4d..00000000 --- a/docs/teslajsonpy/teslajsonpy.homeassistant.vehicle_data.rst +++ /dev/null @@ -1,10 +0,0 @@ -========================================== -``teslajsonpy.homeassistant.vehicle_data`` -========================================== - -.. automodule:: teslajsonpy.homeassistant.vehicle_data - - .. contents:: - :local: - -.. currentmodule:: teslajsonpy.homeassistant.vehicle_data diff --git a/docs/teslajsonpy/teslajsonpy.rst b/docs/teslajsonpy/teslajsonpy.rst index bb64b34e..543b2e4c 100644 --- a/docs/teslajsonpy/teslajsonpy.rst +++ b/docs/teslajsonpy/teslajsonpy.rst @@ -14,11 +14,12 @@ Submodules .. toctree:: teslajsonpy.__version__ + teslajsonpy.car teslajsonpy.connection teslajsonpy.const teslajsonpy.controller + teslajsonpy.energy teslajsonpy.exceptions - teslajsonpy.homeassistant teslajsonpy.teslaproxy .. currentmodule:: teslajsonpy @@ -27,75 +28,28 @@ Submodules Classes ======= +- :py:class:`TeslaCar`: + Class to handle car attributes and methods. + - :py:class:`Connection`: Connection to Tesla Motors API. - :py:class:`Controller`: Controller for connections to Tesla Motors API. +- :py:class:`Energy`: + Class to handle energy site attributes and methods. + - :py:class:`TeslaProxy`: Class to handle proxy login connections to Alexa. -- :py:class:`Battery`: - Home-Assistant battery class for a Tesla VehicleDevice. - -- :py:class:`Range`: - Home-Assistant class of the battery range for a Tesla VehicleDevice. - -- :py:class:`ChargerConnectionSensor`: - Home-assistant charger connection class for Tesla vehicles. - -- :py:class:`ChargingSensor`: - Home-Assistant charging sensor class for a Tesla VehicleDevice. - -- :py:class:`OnlineSensor`: - Home-Assistant Online sensor class for a Tesla VehicleDevice. - -- :py:class:`ParkingSensor`: - Home-assistant parking brake class for Tesla vehicles. - -- :py:class:`UpdateSensor`: - Home-Assistant update sensor class for a Tesla VehicleDevice. - -- :py:class:`ChargerSwitch`: - Home-Assistant class for the charger of a Tesla VehicleDevice. - -- :py:class:`RangeSwitch`: - Home-Assistant class for setting range limit for charger. - -- :py:class:`Climate`: - Home-assistant class of HVAC for Tesla vehicles. - -- :py:class:`TempSensor`: - Home-assistant class of temperature sensors for Tesla vehicles. - -- :py:class:`GPS`: - Home-assistant class for GPS of Tesla vehicles. - -- :py:class:`Odometer`: - Home-assistant class for odometer of Tesla vehicles. - -- :py:class:`Lock`: - Home-assistant lock class for Tesla vehicles. - -- :py:class:`SentryModeSwitch`: - Home-Assistant class for sentry mode of Tesla vehicles. -- :py:class:`Horn`: - Home-Assistant class for horn of Tesla vehicles. - -- :py:class:`FlashLights`: - Home-Assistant class for flash lights of Tesla vehicles. - -- :py:class:`TriggerHomelink`: - Home-Assistant class for trigger homelink of Tesla vehicles. - -- :py:class:`TrunkLock`: - Home-Assistant rear trunk lock for a Tesla VehicleDevice. - -- :py:class:`FrunkLock`: - Home-Assistant front trunk (frunk) lock for a Tesla VehicleDevice. +.. autoclass:: TeslaCar + :members: + .. rubric:: Inheritance + .. inheritance-diagram:: TeslaCar + :parts: 1 .. autoclass:: Connection :members: @@ -111,151 +65,18 @@ Classes .. inheritance-diagram:: Controller :parts: 1 -.. autoclass:: TeslaProxy - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: TeslaProxy - :parts: 1 - -.. autoclass:: Battery - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: Battery - :parts: 1 - -.. autoclass:: Range - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: Range - :parts: 1 - -.. autoclass:: ChargerConnectionSensor - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: ChargerConnectionSensor - :parts: 1 - -.. autoclass:: ChargingSensor - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: ChargingSensor - :parts: 1 - -.. autoclass:: OnlineSensor - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: OnlineSensor - :parts: 1 - -.. autoclass:: ParkingSensor - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: ParkingSensor - :parts: 1 - -.. autoclass:: UpdateSensor - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: UpdateSensor - :parts: 1 - -.. autoclass:: ChargerSwitch - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: ChargerSwitch - :parts: 1 - -.. autoclass:: RangeSwitch - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: RangeSwitch - :parts: 1 - -.. autoclass:: Climate +.. autoclass:: Energy :members: .. rubric:: Inheritance - .. inheritance-diagram:: Climate + .. inheritance-diagram:: Energy :parts: 1 -.. autoclass:: TempSensor - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: TempSensor - :parts: 1 - -.. autoclass:: GPS - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: GPS - :parts: 1 - -.. autoclass:: Odometer - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: Odometer - :parts: 1 - -.. autoclass:: Lock - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: Lock - :parts: 1 - -.. autoclass:: SentryModeSwitch - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: SentryModeSwitch - :parts: 1 - -.. autoclass:: Horn - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: Horn - :parts: 1 - -.. autoclass:: FlashLights - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: FlashLights - :parts: 1 - -.. autoclass:: TriggerHomelink - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: TriggerHomelink - :parts: 1 - -.. autoclass:: TrunkLock - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: TrunkLock - :parts: 1 - -.. autoclass:: FrunkLock +.. autoclass:: TeslaProxy :members: .. rubric:: Inheritance - .. inheritance-diagram:: FrunkLock + .. inheritance-diagram:: TeslaProxy :parts: 1 diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 303c7035..ccf6719d 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -18,36 +18,40 @@ class TeslaCar: - """Base class to represents a Tesla car.""" + """Represents a Tesla car. + + This class shouldn't be instantiated directly; it will be instantiated + by :meth:`teslajsonpy.controller.generate_car_objects`. + """ def __init__(self, car, controller) -> None: - """Initialize EnergySite.""" + """Initialize TeslaCar.""" self._car = car self._controller = controller @property def display_name(self) -> str: - """Return State Data.""" + """Return display name.""" return self._car.get("display_name") @property def id(self) -> int: - """Return State Data.""" + """Return id.""" return self._car.get("id") @property def state(self) -> str: - """Return State Data.""" + """Return car state.""" return self._car.get("state") @property def vehicle_id(self) -> int: - """Return State Data.""" + """Return car id.""" return self._car.get("vehicle_id") @property def vin(self) -> str: - """Return State Data.""" + """Return car vin.""" return self._car.get("vin") @property diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index 079a539d..2b380a27 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -8,20 +8,19 @@ For more details about this api, please refer to the documentation at https://github.com/zabuldon/teslajsonpy """ +import aiohttp import asyncio import base64 +from bs4 import BeautifulSoup import calendar import datetime import hashlib +import httpx import json import logging import secrets import time from typing import Dict, Text - -import aiohttp -from bs4 import BeautifulSoup -import httpx import yarl from yarl import URL diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 2ba78865..fc57b019 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -9,16 +9,15 @@ https://github.com/zabuldon/teslajsonpy """ import asyncio -import logging +import backoff +import httpx import json +import logging import pkgutil import time -from typing import Any, Callable, Dict, List, Optional, Text - -import backoff -import httpx -import wrapt +from typing import Callable, Dict, List, Optional, Text from yarl import URL +import wrapt from teslajsonpy.car import TeslaCar from teslajsonpy.connection import Connection @@ -35,7 +34,7 @@ RESOURCE_TYPE_BATTERY, ) from teslajsonpy.energy import EnergySite, SolarSite, PowerwallSite, SolarPowerwallSite -from teslajsonpy.exceptions import should_giveup, RetryLimitError, TeslaException +from teslajsonpy.exceptions import RetryLimitError, TeslaException _LOGGER = logging.getLogger(__name__) diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index cec0d2eb..5e05aa69 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -1,5 +1,4 @@ """Tesla Energy energy site.""" - from teslajsonpy.const import ( RESOURCE_TYPE, DEFAULT_ENERGYSITE_NAME, From 9c986c24b9869330ffe5a6b0d534312d74bc164d Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 28 Aug 2022 19:06:02 -0700 Subject: [PATCH 36/84] Revert some unnecessary get methods --- teslajsonpy/controller.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index fc57b019..9aa65cc2 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -718,7 +718,7 @@ async def _get_and_process_car_data(vin: Text) -> None: ) except TeslaException: data = None - if data and data.get("response"): + if data and data["response"]: response = data["response"] self.set_climate_params(vin=vin, params=response["climate_state"]) self.set_charging_params(vin=vin, params=response["charge_state"]) @@ -762,7 +762,7 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: ) except TeslaException: data = None - if data and data.get("response"): + if data and data["response"]: response = data["response"] # Some setups always report grid_status of "Unknown" regardless # of the actual grid status. Others only report grid_status "Unknown" @@ -798,7 +798,7 @@ async def _get_and_process_battery_data( ) except TeslaException: data = None - if data and data.get("response").get("power_reading"): + if data and data["response"].get("power_reading"): response = data["response"] params = response["power_reading"][0] @@ -825,7 +825,7 @@ async def _get_and_process_battery_summary( ) except TeslaException: data = None - if data and data.get("response"): + if data and data["response"]: self.__power_data[energysite_id].update(data["response"]) async with self.__update_lock: From b5577e6cf2de0be7c97fca179e376a0df57a299f Mon Sep 17 00:00:00 2001 From: shred86 Date: Mon, 29 Aug 2022 07:06:37 -0700 Subject: [PATCH 37/84] Add asyncio.Lock() for battery_id --- teslajsonpy/controller.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 9aa65cc2..9bcd785c 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -409,7 +409,8 @@ async def connect( ] for energysite in self.__energysite_list: - energysite_id = energysite["energy_site_id"] + energysite_id = energysite.get["energy_site_id"] + battery_id = energysite.get("id") if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: # Non-powerwall sites "site_name" in "SITE_DATA" endpoint @@ -427,6 +428,7 @@ async def connect( self.__grid_status[energysite_id] = {"grid_always_unk": True} self.__lock[energysite_id] = asyncio.Lock() + self.__lock[battery_id] = asyncio.Lock() if not test_login: try: From 44ba18aaa432d0c78ffadea4ca828e55c867d48d Mon Sep 17 00:00:00 2001 From: shred86 Date: Mon, 29 Aug 2022 07:14:29 -0700 Subject: [PATCH 38/84] Fix typo --- teslajsonpy/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 9bcd785c..68d0aeb8 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -409,7 +409,7 @@ async def connect( ] for energysite in self.__energysite_list: - energysite_id = energysite.get["energy_site_id"] + energysite_id = energysite.get("energy_site_id") battery_id = energysite.get("id") if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: From eef07e1491c69469e830136c713ecd4da5c722d1 Mon Sep 17 00:00:00 2001 From: shred86 Date: Mon, 29 Aug 2022 20:26:48 -0700 Subject: [PATCH 39/84] Adjust naming and add more battery site properties --- teslajsonpy/const.py | 4 +- teslajsonpy/controller.py | 30 ++++++++------ teslajsonpy/energy.py | 85 +++++++++++++++++++++++---------------- 3 files changed, 69 insertions(+), 50 deletions(-) diff --git a/teslajsonpy/const.py b/teslajsonpy/const.py index 13a49d6d..d3d6ee2a 100644 --- a/teslajsonpy/const.py +++ b/teslajsonpy/const.py @@ -19,10 +19,10 @@ TESLA_PRODUCT_TYPE_VEHICLES = "vehicles" CHARGE_CURRENT_MIN = 5 - +DEFAULT_ENERGYSITE_NAME = "My Home" +GRID_ACTIVE = "Active" PRODUCT_TYPE_ENERGY_SITES = "energy_sites" PRODUCT_TYPE_POWERWALLS = "powerwalls" -DEFAULT_ENERGYSITE_NAME = "My Home" RESOURCE_TYPE = "resource_type" RESOURCE_TYPE_SOLAR = "solar" RESOURCE_TYPE_BATTERY = "battery" diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 68d0aeb8..1f998c95 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -339,9 +339,9 @@ def __init__( self.enable_websocket = enable_websocket self.endpoints = {} self.polling_policy = polling_policy + self.__energysite_data: Dict[int, dict] = {} self.__energysite_list: List[dict] = [] self.__grid_status: Dict[int, dict] = {} - self.__power_data: Dict[int, dict] = {} self.__vehicle_list: List[dict] = [] self.cars: Dict[str, TeslaCar] = {} self.energysites: Dict[int, EnergySite] = {} @@ -417,12 +417,12 @@ async def connect( site_config = await self.get_site_config(energysite_id) energysite.update(site_config) - self.__power_data[energysite_id] = { + self.__energysite_data[energysite_id] = { "solar_power": 0, "load_power": 0, "grid_power": 0, "battery_power": 0, - "battery_percentage": 0, + "percentage_charged": 0, } # Default to True but check in first update self.__grid_status[energysite_id] = {"grid_always_unk": True} @@ -545,7 +545,7 @@ def generate_energysite_objects(self) -> Dict[int, EnergySite]: # Solar only systems (no Powerwalls) are listed as "solar" if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: self.energysites[energysite_id] = SolarSite( - self.api, energysite, self.__power_data[energysite_id] + self.api, energysite, self.__energysite_data[energysite_id] ) # Solar with Powerwall are listed as "battery" if ( @@ -553,7 +553,7 @@ def generate_energysite_objects(self) -> Dict[int, EnergySite]: and energysite["components"]["solar"] ): self.energysites[energysite_id] = SolarPowerwallSite( - self.api, energysite, self.__power_data[energysite_id] + self.api, energysite, self.__energysite_data[energysite_id] ) # Assumed Powerwall only (no solar) is listed as "battery" if ( @@ -561,7 +561,7 @@ def generate_energysite_objects(self) -> Dict[int, EnergySite]: and not energysite["components"]["solar"] ): self.energysites[energysite_id] = PowerwallSite( - self.api, energysite, self.__power_data[energysite_id] + self.api, energysite, self.__energysite_data[energysite_id] ) return self.energysites @@ -785,7 +785,7 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: ) del response["solar_power"] - self.__power_data[energysite_id].update(response) + self.__energysite_data[energysite_id].update(response) async def _get_and_process_battery_data( energysite_id: Text, battery_id: Text @@ -800,15 +800,19 @@ async def _get_and_process_battery_data( ) except TeslaException: data = None - if data and data["response"].get("power_reading"): + if data and data["response"]: response = data["response"] - - params = response["power_reading"][0] + params = {} + if response.get("power_reading"): + params = response["power_reading"][0] + params["backup_reserve_percent"] = response.get("backup").get( + "backup_reserve_percent" + ) params["grid_status"] = response.get("grid_status") params["default_real_mode"] = response.get("default_real_mode") params["operation"] = response.get("operation") # Use energysite_id since that's how it's retrieved - self.__power_data[energysite_id].update(params) + self.__energysite_data[energysite_id].update(params) else: _LOGGER.info("No power readings for energy site %s", energysite_id) @@ -828,7 +832,7 @@ async def _get_and_process_battery_summary( except TeslaException: data = None if data and data["response"]: - self.__power_data[energysite_id].update(data["response"]) + self.__energysite_data[energysite_id].update(data["response"]) async with self.__update_lock: cur_time = round(time.time()) @@ -1524,7 +1528,7 @@ def get_update_interval_vin(self, car_id: Text = None, vin: Text = None) -> int: def get_power_params(self, energysite_id: Text) -> Dict: """Return cached copy of power_params for energysite_id.""" - return self.__power_data[energysite_id] + return self.__energysite_data[energysite_id] def _id_to_vin(self, car_id: Text) -> Optional[Text]: """Return vin for a car_id.""" diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index 5e05aa69..8170a372 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -8,11 +8,11 @@ class EnergySite: """Base class to represents a Tesla Energy site.""" - def __init__(self, api, energysite, power_data) -> None: + def __init__(self, api, energysite, data) -> None: """Initialize EnergySite.""" self._api = api self._energysite = energysite - self._power_data = power_data + self._data = data @property def energysite_id(self) -> int: @@ -48,26 +48,26 @@ class SolarSite(EnergySite): by :meth:`teslajsonpy.controller.generate_energysite_objects`. """ - def __init__(self, api, energysite, power_data) -> None: + def __init__(self, api, energysite, data) -> None: """Initialize SolarSite.""" - super().__init__(api, energysite, power_data) + super().__init__(api, energysite, data) @property def grid_power(self) -> float: """Return grid power in Watts.""" # Add check to see if site has power metering? - return self._power_data["grid_power"] + return self._data["grid_power"] @property def load_power(self) -> float: """Return load power in Watts.""" # Add check to see if site has power metering? - return self._power_data["load_power"] + return self._data["load_power"] @property def solar_power(self) -> float: """Return solar power in Watts.""" - return self._power_data["solar_power"] + return self._data["solar_power"] @property def solar_type(self) -> str: @@ -82,46 +82,61 @@ class PowerwallSite(EnergySite): by :meth:`teslajsonpy.controller.generate_energysite_objects`. """ - def __init__(self, api, energysite, power_data) -> None: + def __init__(self, api, energysite, data) -> None: """Initialize PowerwallSite.""" - super().__init__(api, energysite, power_data) - - @property - def battery_percent(self) -> float: - """Return battery charge level percentage.""" - return self._power_data["battery_percentage"] + super().__init__(api, energysite, data) @property def battery_power(self) -> float: """Return battery power in Watts.""" - return self._power_data["battery_power"] + return self._data["battery_power"] + + @property + def battery_reserve_percent(self) -> float: + """Return battery reserve percentage.""" + return self._data["backup_reserve_percent"] + + @property + def energy_left(self) -> float: + """Return battery energy left in Watt hours.""" + return self._data["energy_left"] @property def grid_power(self) -> float: # Grid and load power are the same in SolarSite because of how we store - # the data. It comes from two different endpoints but we stored in self._power_data - return self._power_data["grid_power"] + # the data. It comes from two different endpoints but we stored in self._data + return self._data["grid_power"] + + @property + def grid_status(self) -> str: + """Return grid status.""" + return self._data["grid_status"] @property def load_power(self) -> float: """Return load power in Watts.""" - return self._power_data["load_power"] + return self._data["load_power"] - # async def set_operation_mode(self, real_mode: str, value: int) -> None: - # """Set operation mode of Powerwall. - - # Mode: "self_consumption", "backup", "autonomous" - # Value: 0-100 - # """ - # data = await self._api( - # "BATTERY_OPERATION_MODE", - # path_vars={"battery_id": self.id}, - # default_real_mode=real_mode, - # backup_reserve_percent=int(value), - # ) - # if data and data["response"]["result"]: - # self.__default_real_mode = real_mode - # self.__backup_reserve_percent = value + @property + def percentage_charged(self) -> float: + """Return battery percentage charged.""" + return self._data["percentage_charged"] + + async def set_operation_mode(self, real_mode: str, value: int) -> None: + """Set operation mode of Powerwall. + + Mode: "self_consumption", "backup", "autonomous" + Value: 0-100 + """ + data = await self._api( + "BATTERY_OPERATION_MODE", + path_vars={"battery_id": self.id}, + default_real_mode=real_mode, + backup_reserve_percent=int(value), + ) + if data and data["response"]["result"] is True: + self._data["default_real_mode"] = real_mode + self._data["backup_reserve_percent"] = value class SolarPowerwallSite(PowerwallSite, SolarSite): @@ -131,6 +146,6 @@ class SolarPowerwallSite(PowerwallSite, SolarSite): by :meth:`teslajsonpy.controller.generate_energysite_objects`. """ - def __init__(self, api, energysite, power_data) -> None: + def __init__(self, api, energysite, data) -> None: """Initialize SolarPowerwallSite.""" - super().__init__(api, energysite, power_data) + super().__init__(api, energysite, data) From 139d2ed186467f82a08d3df2da0733a9c65ac41d Mon Sep 17 00:00:00 2001 From: shred86 Date: Tue, 30 Aug 2022 21:56:11 -0700 Subject: [PATCH 40/84] Add operation mode --- teslajsonpy/__init__.py | 5 ++++ teslajsonpy/const.py | 2 ++ teslajsonpy/energy.py | 51 +++++++++++++++++++++++++++++++++-------- 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/teslajsonpy/__init__.py b/teslajsonpy/__init__.py index ff240e1b..3d3cd99f 100644 --- a/teslajsonpy/__init__.py +++ b/teslajsonpy/__init__.py @@ -17,10 +17,15 @@ from .__version__ import __version__ __all__ = [ + "TeslaCar", "Connection", "Controller", + "EnergySite", + "PowerwallSite", "TeslaProxy", "TeslaException", + "SolarPowerwallSite", + "SolarSite", "UnknownPresetMode", "__version__", "RetryLimitError", diff --git a/teslajsonpy/const.py b/teslajsonpy/const.py index d3d6ee2a..96f48bc1 100644 --- a/teslajsonpy/const.py +++ b/teslajsonpy/const.py @@ -18,6 +18,8 @@ TESLA_PRODUCT_TYPE_VEHICLES = "vehicles" +BACKUP_RESERVE_MAX = 100 +BACKUP_RESERVE_MIN = 0 CHARGE_CURRENT_MIN = 5 DEFAULT_ENERGYSITE_NAME = "My Home" GRID_ACTIVE = "Active" diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index 8170a372..f2930a5d 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -1,9 +1,13 @@ """Tesla Energy energy site.""" +import logging + from teslajsonpy.const import ( RESOURCE_TYPE, DEFAULT_ENERGYSITE_NAME, ) +_LOGGER = logging.getLogger(__name__) + class EnergySite: """Base class to represents a Tesla Energy site.""" @@ -40,6 +44,17 @@ def site_name(self) -> str: # "site_name" not a valid key if name never set in Tesla app return self._energysite.get("site_name", DEFAULT_ENERGYSITE_NAME) + async def _send_command( + self, name: str, *, path_vars: dict, wake_if_asleep: bool = False, **kwargs + ) -> dict: + """Wrapper for sending commands to the Tesla API.""" + _LOGGER.debug("Sending command: %s", name) + data = await self._controller.api( + name, path_vars=path_vars, wake_if_asleep=wake_if_asleep, **kwargs + ) + _LOGGER.debug("Response from command %s: %s", name, data) + return data + class SolarSite(EnergySite): """Represents a Tesla Energy Solar site. @@ -86,16 +101,16 @@ def __init__(self, api, energysite, data) -> None: """Initialize PowerwallSite.""" super().__init__(api, energysite, data) + @property + def backup_reserve_percent(self) -> int: + """Return backup reserve percentage.""" + return self._data["backup_reserve_percent"] + @property def battery_power(self) -> float: """Return battery power in Watts.""" return self._data["battery_power"] - @property - def battery_reserve_percent(self) -> float: - """Return battery reserve percentage.""" - return self._data["backup_reserve_percent"] - @property def energy_left(self) -> float: """Return battery energy left in Watt hours.""" @@ -117,25 +132,41 @@ def load_power(self) -> float: """Return load power in Watts.""" return self._data["load_power"] + @property + def operation_mode(self) -> str: + """Return operation mode.""" + return self._data["operation"] + @property def percentage_charged(self) -> float: """Return battery percentage charged.""" return self._data["percentage_charged"] - async def set_operation_mode(self, real_mode: str, value: int) -> None: + async def set_operation_mode(self, real_mode: str) -> None: """Set operation mode of Powerwall. Mode: "self_consumption", "backup", "autonomous" - Value: 0-100 """ - data = await self._api( + data = await self._send_command( "BATTERY_OPERATION_MODE", - path_vars={"battery_id": self.id}, + path_vars={"site_id": self.energysite_id}, default_real_mode=real_mode, - backup_reserve_percent=int(value), + backup_reserve_percent=self.battery_reserve_percent, ) if data and data["response"]["result"] is True: self._data["default_real_mode"] = real_mode + + async def set_reserve_percent(self, value: int) -> None: + """Set reserve percentage of Powerwall. + + Value: 0-100 + """ + data = await self._send_command( + "BACKUP_RESERVE", + path_vars={"site_id": self.energysite_id}, + backup_reserve_percent=int(value), + ) + if data and data["response"]["result"] is True: self._data["backup_reserve_percent"] = value From 1f975f3f0dfa1afdb216266846924432870f20a2 Mon Sep 17 00:00:00 2001 From: shred86 Date: Wed, 31 Aug 2022 05:03:37 -0700 Subject: [PATCH 41/84] Fix typos --- teslajsonpy/energy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index f2930a5d..42ab8531 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -49,7 +49,7 @@ async def _send_command( ) -> dict: """Wrapper for sending commands to the Tesla API.""" _LOGGER.debug("Sending command: %s", name) - data = await self._controller.api( + data = await self._api( name, path_vars=path_vars, wake_if_asleep=wake_if_asleep, **kwargs ) _LOGGER.debug("Response from command %s: %s", name, data) @@ -151,7 +151,7 @@ async def set_operation_mode(self, real_mode: str) -> None: "BATTERY_OPERATION_MODE", path_vars={"site_id": self.energysite_id}, default_real_mode=real_mode, - backup_reserve_percent=self.battery_reserve_percent, + backup_reserve_percent=self.backup_reserve_percent, ) if data and data["response"]["result"] is True: self._data["default_real_mode"] = real_mode From 6f168ee90675daa2922fd313edd65e3c26c76100 Mon Sep 17 00:00:00 2001 From: shred86 Date: Wed, 31 Aug 2022 19:28:43 -0700 Subject: [PATCH 42/84] Use battery_id for changing operation mode --- teslajsonpy/energy.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index 42ab8531..1e511997 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -149,9 +149,8 @@ async def set_operation_mode(self, real_mode: str) -> None: """ data = await self._send_command( "BATTERY_OPERATION_MODE", - path_vars={"site_id": self.energysite_id}, + path_vars={"battery_id": self.id}, default_real_mode=real_mode, - backup_reserve_percent=self.backup_reserve_percent, ) if data and data["response"]["result"] is True: self._data["default_real_mode"] = real_mode From 13e6a4c641bf80f8e2dc0a63a03a874dea624f85 Mon Sep 17 00:00:00 2001 From: shred86 Date: Wed, 31 Aug 2022 21:48:52 -0700 Subject: [PATCH 43/84] Add export rule and grid charging --- teslajsonpy/controller.py | 3 +++ teslajsonpy/energy.py | 48 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 1f998c95..c032de99 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -808,6 +808,9 @@ async def _get_and_process_battery_data( params["backup_reserve_percent"] = response.get("backup").get( "backup_reserve_percent" ) + params["customer_preferred_export_rule"] = response.get( + "components" + ).get("customer_preferred_export_rule") params["grid_status"] = response.get("grid_status") params["default_real_mode"] = response.get("default_real_mode") params["operation"] = response.get("operation") diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index 1e511997..4d4c10be 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -28,6 +28,16 @@ def has_load_meter(self) -> bool: """Return True if energy site has a load meter.""" return self._energysite.get("components").get("load_meter") + @property + def has_battery(self) -> bool: + """Return True if energy site has battery.""" + return self._energysite.get("components").get("battery") + + @property + def has_solar(self) -> bool: + """Return True if energy site has solar.""" + return self._energysite.get("components").get("solar") + @property def id(self) -> int: """Return id (aka battery_id).""" @@ -111,11 +121,21 @@ def battery_power(self) -> float: """Return battery power in Watts.""" return self._data["battery_power"] + @property + def disallow_grid_charging(self) -> bool: + """Return disallow grid charging.""" + return + @property def energy_left(self) -> float: """Return battery energy left in Watt hours.""" return self._data["energy_left"] + @property + def export_rule(self) -> str: + """Return energy export rule setting.""" + return self._data["customer_preferred_export_rule"] + @property def grid_power(self) -> float: # Grid and load power are the same in SolarSite because of how we store @@ -142,6 +162,34 @@ def percentage_charged(self) -> float: """Return battery percentage charged.""" return self._data["percentage_charged"] + async def set_export_rule(self, setting: str) -> None: + """Set energy export setting of Powerwall. + + Settings + Solar: "pv_only" + Everything: "battery_ok" + """ + await self._send_command( + "ENERGY_SITE_IMPORT_EXPORT_CONFIG", + path_vars={"site_id": self.energysite_id}, + customer_preferred_export_rule=setting, + ) + # This endpoint returns an empty response instead of a result code + # Add check to only set if not a bad response? + self._data["customer_preferred_export_rule"] = setting + + async def set_grid_charging(self, disallow_grid_charging: bool) -> None: + """Set grid charging setting of Powerwall.""" + await self._send_command( + "ENERGY_SITE_IMPORT_EXPORT_CONFIG", + path_vars={"site_id": self.energysite_id}, + disallow_charge_from_grid_with_solar_installed=disallow_grid_charging, + ) + # This endpoint returns an empty response instead of a result code + self._data[ + "disallow_charge_from_grid_with_solar_installed" + ] = disallow_grid_charging + async def set_operation_mode(self, real_mode: str) -> None: """Set operation mode of Powerwall. From f892eb549039567bab9710897a7a67932e21f8bc Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 1 Sep 2022 18:06:08 -0700 Subject: [PATCH 44/84] Check return code instead of response --- teslajsonpy/energy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index 4d4c10be..923fabe3 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -200,7 +200,7 @@ async def set_operation_mode(self, real_mode: str) -> None: path_vars={"battery_id": self.id}, default_real_mode=real_mode, ) - if data and data["response"]["result"] is True: + if data and data["response"]["code"] == 201: self._data["default_real_mode"] = real_mode async def set_reserve_percent(self, value: int) -> None: From e5e573fd988d77fe3572034fec1b381118a395d1 Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 1 Sep 2022 18:27:10 -0700 Subject: [PATCH 45/84] Fix for percentage_charged reporting 0 in error --- teslajsonpy/controller.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index c032de99..2713fe28 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -814,10 +814,7 @@ async def _get_and_process_battery_data( params["grid_status"] = response.get("grid_status") params["default_real_mode"] = response.get("default_real_mode") params["operation"] = response.get("operation") - # Use energysite_id since that's how it's retrieved self.__energysite_data[energysite_id].update(params) - else: - _LOGGER.info("No power readings for energy site %s", energysite_id) async def _get_and_process_battery_summary( energysite_id: Text, battery_id: Text @@ -835,6 +832,12 @@ async def _get_and_process_battery_summary( except TeslaException: data = None if data and data["response"]: + current_val = self.__energysite_data["percentage_charged"] + new_val = data["response"]["percentage_charged"] + # percentage_charged sometimes incorrectly reports 0 so ignore + # it if the current percentage_charged is > 5 + if current_val > 5 and new_val == 0: + return self.__energysite_data[energysite_id].update(data["response"]) async with self.__update_lock: From 2a76312fbb57cef3c2224ef78ee78df0f3057011 Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 1 Sep 2022 18:57:10 -0700 Subject: [PATCH 46/84] Use get method for percentage_charged --- teslajsonpy/controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 2713fe28..cf4748a6 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -832,8 +832,8 @@ async def _get_and_process_battery_summary( except TeslaException: data = None if data and data["response"]: - current_val = self.__energysite_data["percentage_charged"] - new_val = data["response"]["percentage_charged"] + current_val = self.__energysite_data.get("percentage_charged") + new_val = data["response"].get("percentage_charged") # percentage_charged sometimes incorrectly reports 0 so ignore # it if the current percentage_charged is > 5 if current_val > 5 and new_val == 0: From 812074b4eee024a11a67529636b1c4774d96715b Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 1 Sep 2022 21:06:55 -0700 Subject: [PATCH 47/84] Add grid charging property and method --- teslajsonpy/controller.py | 25 ++++++++++++++++++------- teslajsonpy/energy.py | 15 +++++++-------- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index cf4748a6..64ac1258 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -802,15 +802,21 @@ async def _get_and_process_battery_data( data = None if data and data["response"]: response = data["response"] + components = response.get("components") params = {} if response.get("power_reading"): params = response["power_reading"][0] params["backup_reserve_percent"] = response.get("backup").get( "backup_reserve_percent" ) - params["customer_preferred_export_rule"] = response.get( - "components" - ).get("customer_preferred_export_rule") + params["customer_preferred_export_rule"] = components.get( + "customer_preferred_export_rule" + ) + params[ + "disallow_charge_from_grid_with_solar_installed" + ] = components.get( + "disallow_charge_from_grid_with_solar_installed", False + ) params["grid_status"] = response.get("grid_status") params["default_real_mode"] = response.get("default_real_mode") params["operation"] = response.get("operation") @@ -832,10 +838,15 @@ async def _get_and_process_battery_summary( except TeslaException: data = None if data and data["response"]: - current_val = self.__energysite_data.get("percentage_charged") - new_val = data["response"].get("percentage_charged") - # percentage_charged sometimes incorrectly reports 0 so ignore - # it if the current percentage_charged is > 5 + current_val = self.__energysite_data[energysite_id][ + "percentage_charged" + ] + # Default to current_val to prevent None type + new_val = int( + data["response"].get("percentage_charged", current_val) + ) + # percentage_charged sometimes incorrectly reports 0 + # Ignore if the current percentage_charged is > 5 if current_val > 5 and new_val == 0: return self.__energysite_data[energysite_id].update(data["response"]) diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index 923fabe3..4a4e6f54 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -122,9 +122,9 @@ def battery_power(self) -> float: return self._data["battery_power"] @property - def disallow_grid_charging(self) -> bool: - """Return disallow grid charging.""" - return + def grid_charging(self) -> bool: + """Return grid charging.""" + return not self._data["disallow_charge_from_grid_with_solar_installed"] @property def energy_left(self) -> float: @@ -178,17 +178,16 @@ async def set_export_rule(self, setting: str) -> None: # Add check to only set if not a bad response? self._data["customer_preferred_export_rule"] = setting - async def set_grid_charging(self, disallow_grid_charging: bool) -> None: + async def set_grid_charging(self, value: bool) -> None: """Set grid charging setting of Powerwall.""" + param = not value await self._send_command( "ENERGY_SITE_IMPORT_EXPORT_CONFIG", path_vars={"site_id": self.energysite_id}, - disallow_charge_from_grid_with_solar_installed=disallow_grid_charging, + disallow_charge_from_grid_with_solar_installed=param, ) # This endpoint returns an empty response instead of a result code - self._data[ - "disallow_charge_from_grid_with_solar_installed" - ] = disallow_grid_charging + self._data["disallow_charge_from_grid_with_solar_installed"] = param async def set_operation_mode(self, real_mode: str) -> None: """Set operation mode of Powerwall. From 30897aae37c5b1c2d75b89e0f282246719d69bca Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 1 Sep 2022 21:19:48 -0700 Subject: [PATCH 48/84] Organize properties and methods --- teslajsonpy/controller.py | 1 - teslajsonpy/energy.py | 76 +++++++++++++++++++-------------------- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 64ac1258..afe7b31d 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -818,7 +818,6 @@ async def _get_and_process_battery_data( "disallow_charge_from_grid_with_solar_installed", False ) params["grid_status"] = response.get("grid_status") - params["default_real_mode"] = response.get("default_real_mode") params["operation"] = response.get("operation") self.__energysite_data[energysite_id].update(params) diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index 4a4e6f54..d29d5564 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -121,21 +121,11 @@ def battery_power(self) -> float: """Return battery power in Watts.""" return self._data["battery_power"] - @property - def grid_charging(self) -> bool: - """Return grid charging.""" - return not self._data["disallow_charge_from_grid_with_solar_installed"] - @property def energy_left(self) -> float: """Return battery energy left in Watt hours.""" return self._data["energy_left"] - @property - def export_rule(self) -> str: - """Return energy export rule setting.""" - return self._data["customer_preferred_export_rule"] - @property def grid_power(self) -> float: # Grid and load power are the same in SolarSite because of how we store @@ -162,33 +152,6 @@ def percentage_charged(self) -> float: """Return battery percentage charged.""" return self._data["percentage_charged"] - async def set_export_rule(self, setting: str) -> None: - """Set energy export setting of Powerwall. - - Settings - Solar: "pv_only" - Everything: "battery_ok" - """ - await self._send_command( - "ENERGY_SITE_IMPORT_EXPORT_CONFIG", - path_vars={"site_id": self.energysite_id}, - customer_preferred_export_rule=setting, - ) - # This endpoint returns an empty response instead of a result code - # Add check to only set if not a bad response? - self._data["customer_preferred_export_rule"] = setting - - async def set_grid_charging(self, value: bool) -> None: - """Set grid charging setting of Powerwall.""" - param = not value - await self._send_command( - "ENERGY_SITE_IMPORT_EXPORT_CONFIG", - path_vars={"site_id": self.energysite_id}, - disallow_charge_from_grid_with_solar_installed=param, - ) - # This endpoint returns an empty response instead of a result code - self._data["disallow_charge_from_grid_with_solar_installed"] = param - async def set_operation_mode(self, real_mode: str) -> None: """Set operation mode of Powerwall. @@ -200,7 +163,7 @@ async def set_operation_mode(self, real_mode: str) -> None: default_real_mode=real_mode, ) if data and data["response"]["code"] == 201: - self._data["default_real_mode"] = real_mode + self._data["operation"] = real_mode async def set_reserve_percent(self, value: int) -> None: """Set reserve percentage of Powerwall. @@ -226,3 +189,40 @@ class SolarPowerwallSite(PowerwallSite, SolarSite): def __init__(self, api, energysite, data) -> None: """Initialize SolarPowerwallSite.""" super().__init__(api, energysite, data) + + @property + def export_rule(self) -> str: + """Return energy export rule setting.""" + return self._data["customer_preferred_export_rule"] + + @property + def grid_charging(self) -> bool: + """Return grid charging.""" + return not self._data["disallow_charge_from_grid_with_solar_installed"] + + async def set_grid_charging(self, value: bool) -> None: + """Set grid charging setting of Powerwall.""" + param = not value + await self._send_command( + "ENERGY_SITE_IMPORT_EXPORT_CONFIG", + path_vars={"site_id": self.energysite_id}, + disallow_charge_from_grid_with_solar_installed=param, + ) + # This endpoint returns an empty response instead of a result code + self._data["disallow_charge_from_grid_with_solar_installed"] = param + + async def set_export_rule(self, setting: str) -> None: + """Set energy export setting of Powerwall. + + Settings + Solar: "pv_only" + Everything: "battery_ok" + """ + await self._send_command( + "ENERGY_SITE_IMPORT_EXPORT_CONFIG", + path_vars={"site_id": self.energysite_id}, + customer_preferred_export_rule=setting, + ) + # This endpoint returns an empty response instead of a result code + # Add check to only set if not a bad response? + self._data["customer_preferred_export_rule"] = setting From 5473de33fa53b186f067b5d53712170404803936 Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 1 Sep 2022 22:10:34 -0700 Subject: [PATCH 49/84] Check for code instead of result --- teslajsonpy/energy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index d29d5564..91f97165 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -175,7 +175,7 @@ async def set_reserve_percent(self, value: int) -> None: path_vars={"site_id": self.energysite_id}, backup_reserve_percent=int(value), ) - if data and data["response"]["result"] is True: + if data and data["response"]["code"] == 201: self._data["backup_reserve_percent"] = value From 0243dbe257ef6116ee5c829aad08251f5fe81b6d Mon Sep 17 00:00:00 2001 From: shred86 Date: Fri, 2 Sep 2022 09:36:59 -0700 Subject: [PATCH 50/84] Update tests --- tests/tesla_mock.py | 3 +- tests/unit_tests/test_car.py | 22 +++++++++++ tests/unit_tests/test_energy.py | 69 +++++++++++++++++++++++++++++---- 3 files changed, 86 insertions(+), 8 deletions(-) diff --git a/tests/tesla_mock.py b/tests/tesla_mock.py index 871ac852..d50f619b 100644 --- a/tests/tesla_mock.py +++ b/tests/tesla_mock.py @@ -264,7 +264,8 @@ def command_ok(): return RESULT_OK -RESULT_OK = {"response": {"reason": "", "result": True}} +# Response includes either result or code, not both. Combined here for now. +RESULT_OK = {"response": {"reason": "", "result": True, "code": 201}} RESULT_NOT_OK = {"response": {"reason": "", "result": False}} # 408 - Request Timeout diff --git a/tests/unit_tests/test_car.py b/tests/unit_tests/test_car.py index 351d174b..2b99f440 100644 --- a/tests/unit_tests/test_car.py +++ b/tests/unit_tests/test_car.py @@ -17,6 +17,7 @@ async def test_car_properties(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -216,6 +217,7 @@ async def test_change_charge_limit(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.change_charge_limit(70.0) is None @@ -227,6 +229,7 @@ async def test_charge_port_door_open_close(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.charge_port_door_open() is None @@ -240,6 +243,7 @@ async def test_flash_lights(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.flash_lights() is None @@ -251,6 +255,7 @@ async def test_honk_horn(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.honk_horn() is None @@ -262,6 +267,7 @@ async def test_lock(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.lock() is None @@ -273,6 +279,7 @@ async def test_remote_seat_heater_request(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.remote_seat_heater_request(3, 1) is None @@ -284,6 +291,7 @@ async def test_schedule_software_update(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.schedule_software_update() is None @@ -295,6 +303,7 @@ async def test_set_charging_amps(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_charging_amps(32.0) is None @@ -306,6 +315,7 @@ async def test_set_cabin_overheat_protection(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_cabin_overheat_protection("On") is None @@ -317,6 +327,7 @@ async def test_set_climate_keeper_mode(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_climate_keeper_mode(1) is None @@ -328,6 +339,7 @@ async def test_set_heated_steering_wheel(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_heated_steering_wheel(True) is None @@ -339,6 +351,7 @@ async def test_set_hvac_mode(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_hvac_mode("on") is None @@ -350,6 +363,7 @@ async def test_set_max_defrost(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_max_defrost(2) is None @@ -361,6 +375,7 @@ async def test_set_sentry_mode(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_sentry_mode(True) is None @@ -372,6 +387,7 @@ async def test_set_temperature(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_temperature(22.0) is None @@ -383,6 +399,7 @@ async def test_start_stop_charge(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.start_charge() is None @@ -396,6 +413,7 @@ async def test_wake_up(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.wake_up() is None @@ -407,6 +425,7 @@ async def test_toggle_trunk(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.toggle_trunk() is None @@ -418,6 +437,7 @@ async def test_toggle_frunk(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.toggle_frunk() is None @@ -429,6 +449,7 @@ async def test_trigger_homelink(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.trigger_homelink() is None @@ -440,6 +461,7 @@ async def test_unlock(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.unlock() is None diff --git a/tests/unit_tests/test_energy.py b/tests/unit_tests/test_energy.py index bb2a774e..5399d8e1 100644 --- a/tests/unit_tests/test_energy.py +++ b/tests/unit_tests/test_energy.py @@ -15,6 +15,7 @@ async def test_energysite_setup(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_energysite_objects() solar_site = _controller.energysites[12345] powerwall_site = _controller.energysites[67890] @@ -30,21 +31,24 @@ async def test_solar_site(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_energysite_objects() _solar_site = _controller.energysites[12345] assert _solar_site._api is not None assert _solar_site._energysite is not None - assert _solar_site._power_data == { + assert _solar_site._data == { "solar_power": 0, "load_power": 0, "grid_power": 0, "battery_power": 0, - "battery_percentage": 0, + "percentage_charged": 0, } assert _solar_site.energysite_id == ENERGYSITES[0]["energy_site_id"] + assert _solar_site.has_battery == ENERGYSITES[0]["components"]["battery"] assert _solar_site.has_load_meter == ENERGYSITES[0]["components"]["load_meter"] + assert _solar_site.has_solar == ENERGYSITES[0]["components"]["solar"] assert _solar_site.id == ENERGYSITES[0]["id"] assert _solar_site.resource_type == ENERGYSITES[0]["resource_type"] assert _solar_site.site_name == SITE_CONFIG["site_name"] @@ -61,17 +65,18 @@ async def test_powerwall_site(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller.generate_energysite_objects() _solar_powerwall_site = _controller.energysites[67890] assert _solar_powerwall_site._api is not None assert _solar_powerwall_site._energysite is not None - assert _solar_powerwall_site._power_data == { + assert _solar_powerwall_site._data == { "solar_power": 0, "load_power": 0, "grid_power": 0, "battery_power": 0, - "battery_percentage": 0, + "percentage_charged": 0, } assert _solar_powerwall_site.energysite_id == ENERGYSITES[1]["energy_site_id"] @@ -80,10 +85,12 @@ async def test_powerwall_site(monkeypatch): == ENERGYSITES[1]["components"]["load_meter"] ) assert _solar_powerwall_site.id == ENERGYSITES[1]["id"] + assert _solar_powerwall_site.has_battery == ENERGYSITES[1]["components"]["battery"] + assert _solar_powerwall_site.has_solar == ENERGYSITES[1]["components"]["solar"] assert _solar_powerwall_site.resource_type == ENERGYSITES[1]["resource_type"] assert _solar_powerwall_site.site_name == ENERGYSITES[1]["site_name"] - assert _solar_powerwall_site.battery_percent == 0 + assert _solar_powerwall_site.percentage_charged == 0 assert _solar_powerwall_site.battery_power == 0 assert _solar_powerwall_site.grid_power == 0 assert _solar_powerwall_site.load_power == 0 @@ -99,10 +106,58 @@ async def test_energysite_with_no_name(monkeypatch): _mock = TeslaMock(monkeypatch) _api = Controller(None) _energysite = _mock.data_request_energysites()[0] - _power_data = _mock.controller_get_power_params() - _sensor = EnergySite(_api, _energysite, _power_data) + _energysite_data = _mock.controller_get_power_params() + _sensor = EnergySite(_api, _energysite, _energysite_data) assert _sensor.site_name == DEFAULT_ENERGYSITE_NAME +@pytest.mark.asyncio +async def test_set_operation_mode(monkeypatch): + """Test set operation mode.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _controller.generate_energysite_objects() + _energysite = _controller.energysites[67890] + + assert await _energysite.set_operation_mode("autonomous") is None + + +@pytest.mark.asyncio +async def test_set_reserve_percent(monkeypatch): + """Test set reserve percent.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _controller.generate_energysite_objects() + _energysite = _controller.energysites[67890] + + assert await _energysite.set_reserve_percent(10) is None + + +@pytest.mark.asyncio +async def test_set_grid_charging(monkeypatch): + """Test set grid charging.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _controller.generate_energysite_objects() + _energysite = _controller.energysites[67890] + + assert await _energysite.set_grid_charging(True) is None + + +@pytest.mark.asyncio +async def test_set_export_rule(monkeypatch): + """Test set export rule.""" + TeslaMock(monkeypatch) + _controller = Controller(None) + await _controller.connect() + _controller.generate_energysite_objects() + _energysite = _controller.energysites[67890] + + assert await _energysite.set_export_rule("pv_only") is None + + # Test reponse with "grid_status" of "Unknown" From d0b5591ae93ceb2caab2fdb8ac040e02f69eb545 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sat, 3 Sep 2022 17:22:28 -0700 Subject: [PATCH 51/84] Add include option, restructure, tests --- teslajsonpy/car.py | 2 +- teslajsonpy/controller.py | 382 ++++++++++++++++---------------- teslajsonpy/energy.py | 122 ++++++---- tests/tesla_mock.py | 94 +++----- tests/unit_tests/test_energy.py | 88 +++++--- 5 files changed, 348 insertions(+), 340 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index ccf6719d..9f785435 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -538,7 +538,7 @@ async def honk_horn(self) -> None: wake_if_asleep=True, ) - async def lock(self): + async def lock(self) -> None: """Send lock command.""" data = await self._send_command( "LOCK", diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index afe7b31d..6d09db43 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -16,8 +16,8 @@ import pkgutil import time from typing import Callable, Dict, List, Optional, Text -from yarl import URL import wrapt +from yarl import URL from teslajsonpy.car import TeslaCar from teslajsonpy.connection import Connection @@ -339,10 +339,17 @@ def __init__( self.enable_websocket = enable_websocket self.endpoints = {} self.polling_policy = polling_policy - self.__energysite_data: Dict[int, dict] = {} - self.__energysite_list: List[dict] = [] - self.__grid_status: Dict[int, dict] = {} - self.__vehicle_list: List[dict] = [] + + self._include_vehicles: bool = True + self._include_energysites: bool = True + self._product_list: List[dict] = [] + self._vehicle_list: List[dict] = [] + self._energysite_list: List[dict] = [] + self._site_config: Dict[int:dict] = {} + self._site_data: Dict[int:dict] = {} + self._battery_data: Dict[int:dict] = {} + self._battery_summary: Dict[int:dict] = {} + self._grid_status_unknown: Dict[int, bool] = {} self.cars: Dict[str, TeslaCar] = {} self.energysites: Dict[int, EnergySite] = {} @@ -351,6 +358,8 @@ async def connect( test_login: bool = False, wake_if_asleep: bool = False, filtered_vins: Optional[List[Text]] = None, + include_vehicles: bool = True, + include_energysites: bool = True, mfa_code: Text = "", ) -> Dict[Text, Text]: """Connect controller to Tesla. @@ -359,6 +368,8 @@ async def connect( test_login (bool, optional): Whether to test credentials only. Defaults to False. wake_if_asleep (bool, optional): Whether to wake up any sleeping cars to update state. Defaults to False. filtered_vins (list, optional): If not empty, filters the cars by the provided VINs. + include_vehicles (bool, optional): Whether to include vehicles. Defaults to True. + include_energysites(bool, optional): Whether to include energysites. Defaults to True. mfa_code (Text, optional): MFA code to use for connection Returns @@ -369,66 +380,73 @@ async def connect( if mfa_code: self.__connection.mfa_code = mfa_code - product_list = await self.get_product_list() - self._last_attempted_update_time = round(time.time()) self.__update_lock = asyncio.Lock() + self._include_vehicles = include_vehicles + self._include_energysites = include_energysites - self.__vehicle_list = [cars for cars in product_list if "vehicle_id" in cars] + self._product_list = await self.get_product_list() - for car in self.__vehicle_list: - vin = car["vin"] - if filtered_vins and vin not in filtered_vins: - _LOGGER.debug("Skipping car with VIN: %s", vin) - continue + if self._include_vehicles: + self._vehicle_list = [ + cars for cars in self._product_list if "vehicle_id" in cars + ] - self.set_id_vin(car_id=car["id"], vin=vin) - self.set_vehicle_id_vin(vehicle_id=car["vehicle_id"], vin=vin) - self.__lock[vin] = asyncio.Lock() - self.__wakeup_conds[vin] = asyncio.Lock() - self._last_update_time[vin] = 0 - self._last_wake_up_attempt[vin] = 0 - self._last_wake_up_time[vin] = 0 - self.__update[vin] = True - self.__update_state[vin] = "normal" - self.car_state[vin] = car - self.set_car_online(vin=vin, online_status=car["state"] == "online") - self.set_last_park_time(vin=vin, timestamp=self._last_attempted_update_time) - self.__climate[vin] = {} - self.__charging[vin] = {} - self.__state[vin] = {} - self.__config[vin] = {} - self.__driving[vin] = {} - self.__gui[vin] = {} - - self.__energysite_list = [ - p - for p in product_list - if p.get(RESOURCE_TYPE) == RESOURCE_TYPE_SOLAR - or p.get(RESOURCE_TYPE) == RESOURCE_TYPE_BATTERY - ] + for car in self._vehicle_list: + vin = car["vin"] + if filtered_vins and vin not in filtered_vins: + _LOGGER.debug("Skipping car with VIN: %s", vin) + continue - for energysite in self.__energysite_list: - energysite_id = energysite.get("energy_site_id") - battery_id = energysite.get("id") + self.set_id_vin(car_id=car["id"], vin=vin) + self.set_vehicle_id_vin(vehicle_id=car["vehicle_id"], vin=vin) + self.__lock[vin] = asyncio.Lock() + self.__wakeup_conds[vin] = asyncio.Lock() + self._last_update_time[vin] = 0 + self._last_wake_up_attempt[vin] = 0 + self._last_wake_up_time[vin] = 0 + self.__update[vin] = True + self.__update_state[vin] = "normal" + self.car_state[vin] = car + self.set_car_online(vin=vin, online_status=car["state"] == "online") + self.set_last_park_time( + vin=vin, timestamp=self._last_attempted_update_time + ) + self.__climate[vin] = {} + self.__charging[vin] = {} + self.__state[vin] = {} + self.__config[vin] = {} + self.__driving[vin] = {} + self.__gui[vin] = {} + + if self._include_energysites: + self._energysite_list = [ + p + for p in self._product_list + if p.get(RESOURCE_TYPE) == RESOURCE_TYPE_SOLAR + or p.get(RESOURCE_TYPE) == RESOURCE_TYPE_BATTERY + ] - if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: - # Non-powerwall sites "site_name" in "SITE_DATA" endpoint - site_config = await self.get_site_config(energysite_id) - energysite.update(site_config) - - self.__energysite_data[energysite_id] = { - "solar_power": 0, - "load_power": 0, - "grid_power": 0, - "battery_power": 0, - "percentage_charged": 0, - } - # Default to True but check in first update - self.__grid_status[energysite_id] = {"grid_always_unk": True} - - self.__lock[energysite_id] = asyncio.Lock() - self.__lock[battery_id] = asyncio.Lock() + for energysite in self._energysite_list: + energysite_id = energysite.get("energy_site_id") + battery_id = energysite.get("id") + + if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: + # site_name comes from site_config for non-Powerwall sites + self._site_config[energysite_id] = await self.get_site_config( + energysite_id + ) + self._site_data[energysite_id] = {} + + if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_BATTERY: + self._battery_data[energysite_id] = {} + self._battery_summary[energysite_id] = {} + # For dealing with sites that always report "Unknown" + # Default to True and check during updates + self._grid_status_unknown = {energysite_id: True} + + self.__lock[energysite_id] = asyncio.Lock() + self.__lock[battery_id] = asyncio.Lock() if not test_login: try: @@ -532,7 +550,7 @@ async def get_site_config(self, energysite_id: int) -> dict: def generate_car_objects(self) -> Dict[str, TeslaCar]: """Generate car objects.""" - for car in self.__vehicle_list: + for car in self._vehicle_list: vin = car["vin"] self.cars[vin] = TeslaCar(car, self) @@ -540,12 +558,15 @@ def generate_car_objects(self) -> Dict[str, TeslaCar]: def generate_energysite_objects(self) -> Dict[int, EnergySite]: """Generate energy site objects.""" - for energysite in self.__energysite_list: + for energysite in self._energysite_list: energysite_id = energysite["energy_site_id"] # Solar only systems (no Powerwalls) are listed as "solar" if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: self.energysites[energysite_id] = SolarSite( - self.api, energysite, self.__energysite_data[energysite_id] + self.api, + energysite, + self._site_config[energysite_id], + self._site_data[energysite_id], ) # Solar with Powerwall are listed as "battery" if ( @@ -553,7 +574,10 @@ def generate_energysite_objects(self) -> Dict[int, EnergySite]: and energysite["components"]["solar"] ): self.energysites[energysite_id] = SolarPowerwallSite( - self.api, energysite, self.__energysite_data[energysite_id] + self.api, + energysite, + self._battery_data[energysite_id], + self._battery_summary[energysite_id], ) # Assumed Powerwall only (no solar) is listed as "battery" if ( @@ -561,7 +585,10 @@ def generate_energysite_objects(self) -> Dict[int, EnergySite]: and not energysite["components"]["solar"] ): self.energysites[energysite_id] = PowerwallSite( - self.api, energysite, self.__energysite_data[energysite_id] + self.api, + energysite, + self._battery_data[energysite_id], + self._battery_summary[energysite_id], ) return self.energysites @@ -753,17 +780,17 @@ async def _get_and_process_car_data(vin: Text) -> None: ) ) - async def _get_and_process_site_data(energysite_id: Text) -> None: + async def _get_and_process_site_data(energysite_id: int) -> None: async with self.__lock[energysite_id]: - _LOGGER.debug("Updating energysite site data %s", energysite_id) + _LOGGER.debug("Updating SITE_DATA for energysite: %s", energysite_id) + try: data = await self.api( - "SITE_DATA", - path_vars={"site_id": energysite_id}, - wake_if_asleep=wake_if_asleep, + "SITE_DATA", path_vars={"site_id": energysite_id} ) except TeslaException: data = None + if data and data["response"]: response = data["response"] # Some setups always report grid_status of "Unknown" regardless @@ -774,9 +801,9 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: "grid_status" not in response or response.get("grid_status") != "Unknown" ): - self.__grid_status[energysite_id]["grid_always_unk"] = False + self._grid_status_unknown[energysite_id] = False - if not self.__grid_status[energysite_id]["grid_always_unk"] and ( + if not self._grid_status_unknown[energysite_id] and ( response.get("grid_status") == "Unknown" and response.get("solar_power") == 0 ): @@ -785,151 +812,124 @@ async def _get_and_process_site_data(energysite_id: Text) -> None: ) del response["solar_power"] - self.__energysite_data[energysite_id].update(response) + self._site_data[energysite_id].update(response) async def _get_and_process_battery_data( - energysite_id: Text, battery_id: Text + energysite_id: int, battery_id: str ) -> None: async with self.__lock[battery_id]: - _LOGGER.debug("Updating energysite battery data %s", battery_id) + _LOGGER.debug("Updating BATTERY_DATA for energysite: %s", energysite_id) + try: data = await self.api( - "BATTERY_DATA", - path_vars={"battery_id": battery_id}, - wake_if_asleep=wake_if_asleep, + "BATTERY_DATA", path_vars={"battery_id": battery_id} ) except TeslaException: data = None - if data and data["response"]: - response = data["response"] - components = response.get("components") - params = {} - if response.get("power_reading"): - params = response["power_reading"][0] - params["backup_reserve_percent"] = response.get("backup").get( - "backup_reserve_percent" - ) - params["customer_preferred_export_rule"] = components.get( - "customer_preferred_export_rule" - ) - params[ - "disallow_charge_from_grid_with_solar_installed" - ] = components.get( - "disallow_charge_from_grid_with_solar_installed", False - ) - params["grid_status"] = response.get("grid_status") - params["operation"] = response.get("operation") - self.__energysite_data[energysite_id].update(params) + + self._battery_data[energysite_id].update(data) async def _get_and_process_battery_summary( - energysite_id: Text, battery_id: Text + energysite_id: int, battery_id: str ) -> None: - # Battery stats are 0 in BATTERY_DATA - # Must get from BATTERY_SUMMARY async with self.__lock[battery_id]: - _LOGGER.debug("Updating energysite battery summary %s", battery_id) + _LOGGER.debug( + "Updating BATTERY_SUMMARY for energysite: %s", energysite_id + ) + try: data = await self.api( - "BATTERY_SUMMARY", - path_vars={"battery_id": battery_id}, - wake_if_asleep=wake_if_asleep, + "BATTERY_SUMMARY", path_vars={"battery_id": battery_id} ) except TeslaException: data = None - if data and data["response"]: - current_val = self.__energysite_data[energysite_id][ - "percentage_charged" - ] - # Default to current_val to prevent None type - new_val = int( - data["response"].get("percentage_charged", current_val) - ) - # percentage_charged sometimes incorrectly reports 0 - # Ignore if the current percentage_charged is > 5 - if current_val > 5 and new_val == 0: - return - self.__energysite_data[energysite_id].update(data["response"]) + + self._battery_summary[energysite_id].update(data) async with self.__update_lock: - cur_time = round(time.time()) - # Update the online cars using get_vehicles() - last_update = self._last_attempted_update_time - _LOGGER.debug( - "Get vehicles. Force: %s Time: %s Interval %s", - force, - cur_time - last_update, - ONLINE_INTERVAL, - ) - if force or cur_time - last_update >= ONLINE_INTERVAL: - cars = await self.get_vehicles() - for car in cars: - self.set_id_vin(car_id=car["id"], vin=car["vin"]) - self.set_vehicle_id_vin( - vehicle_id=car["vehicle_id"], vin=car["vin"] - ) - self.set_car_online( - vin=car["vin"], online_status=car["state"] == "online" - ) - self.car_state[car["vin"]] = car - 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. - car_id = self._update_id(car_id) - car_vin = self._id_to_vin(car_id) - tasks = [] - for vin, online in self.get_car_online().items(): - # If specific car_id provided, only update match - if ( - (car_vin and car_vin != vin) - or vin not in self.__lock - or (vin and self.car_state[vin].get("in_service")) - ): - continue - async with self.__lock[vin]: - car_state = self.car_state[vin].get("state") - if ( - ( - online - or (wake_if_asleep and car_state in ["asleep", "offline"]) + if self._include_vehicles: + cur_time = round(time.time()) + # Update the online cars using get_vehicles() + last_update = self._last_attempted_update_time + _LOGGER.debug( + "Get vehicles. Force: %s Time: %s Interval %s", + force, + cur_time - last_update, + ONLINE_INTERVAL, + ) + if force or cur_time - last_update >= ONLINE_INTERVAL: + cars = await self.get_vehicles() + for car in cars: + self.set_id_vin(car_id=car["id"], vin=car["vin"]) + self.set_vehicle_id_vin( + vehicle_id=car["vehicle_id"], vin=car["vin"] ) - and ( # pylint: disable=too-many-boolean-expressions - self.__update.get(vin) - ) # Only update cars with update flag on - and ( - force - or vin not in self._last_update_time - or ( - cur_time - self._last_update_time[vin] - >= self._calculate_next_interval(vin) - ) + self.set_car_online( + vin=car["vin"], online_status=car["state"] == "online" ) + self.car_state[car["vin"]] = car + 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. + car_id = self._update_id(car_id) + car_vin = self._id_to_vin(car_id) + tasks = [] + for vin, online in self.get_car_online().items(): + # If specific car_id provided, only update match + if ( + (car_vin and car_vin != vin) + or vin not in self.__lock + or (vin and self.car_state[vin].get("in_service")) ): - tasks.append(_get_and_process_car_data(vin)) - else: - _LOGGER.debug( + continue + async with self.__lock[vin]: + car_state = self.car_state[vin].get("state") + if ( ( - "%s: Skipping update with state %s. Polling: %s. " - "Last update: %s ago. Last parked: %s ago. " - "Last wake_up %s ago. " - ), - vin[-5:], - car_state, - self.__update.get(vin), - cur_time - self._last_update_time[vin], - cur_time - self.get_last_park_time(vin=vin), - cur_time - self.get_last_wake_up_time(vin=vin), - ) - if self.__energysite_list and not car_id: + online + or ( + wake_if_asleep + and car_state in ["asleep", "offline"] + ) + ) + and ( # pylint: disable=too-many-boolean-expressions + self.__update.get(vin) + ) # Only update cars with update flag on + and ( + force + or vin not in self._last_update_time + or ( + cur_time - self._last_update_time[vin] + >= self._calculate_next_interval(vin) + ) + ) + ): + tasks.append(_get_and_process_car_data(vin)) + else: + _LOGGER.debug( + ( + "%s: Skipping update with state %s. Polling: %s. " + "Last update: %s ago. Last parked: %s ago. " + "Last wake_up %s ago. " + ), + vin[-5:], + car_state, + self.__update.get(vin), + cur_time - self._last_update_time[vin], + cur_time - self.get_last_park_time(vin=vin), + cur_time - self.get_last_wake_up_time(vin=vin), + ) + if self._include_energysites and self._energysite_list and not car_id: # do not update energy sites if car_id was a parameter. - for energysite in self.energysites.values(): - energysite_id = energysite.energysite_id - if energysite.resource_type == RESOURCE_TYPE_SOLAR: + for energysite in self._energysite_list: + energysite_id = energysite["energy_site_id"] + if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: tasks.append(_get_and_process_site_data(energysite_id)) - if energysite.resource_type == RESOURCE_TYPE_BATTERY: - battery_id = energysite.id + if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_BATTERY: + battery_id = energysite["id"] tasks.append( _get_and_process_battery_data(energysite_id, battery_id) ) @@ -1542,10 +1542,6 @@ def get_update_interval_vin(self, car_id: Text = None, vin: Text = None) -> int: return self._update_interval_vin.get(vin, self.update_interval) - def get_power_params(self, energysite_id: Text) -> Dict: - """Return cached copy of power_params for energysite_id.""" - return self.__energysite_data[energysite_id] - def _id_to_vin(self, car_id: Text) -> Optional[Text]: """Return vin for a car_id.""" return self.__id_vin_map.get(str(car_id)) diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index 91f97165..d013f5fb 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -1,5 +1,6 @@ -"""Tesla Energy energy site.""" +"""Tesla energy site.""" import logging +from typing import Callable from teslajsonpy.const import ( RESOURCE_TYPE, @@ -10,13 +11,12 @@ class EnergySite: - """Base class to represents a Tesla Energy site.""" + """Base class to represents a Tesla energy site.""" - def __init__(self, api, energysite, data) -> None: + def __init__(self, api: Callable, energysite: dict) -> None: """Initialize EnergySite.""" - self._api = api - self._energysite = energysite - self._data = data + self._api: Callable = api + self._energysite: dict = energysite @property def energysite_id(self) -> int: @@ -39,7 +39,7 @@ def has_solar(self) -> bool: return self._energysite.get("components").get("solar") @property - def id(self) -> int: + def id(self) -> str: """Return id (aka battery_id).""" return self._energysite.get("id") @@ -48,12 +48,6 @@ def resource_type(self) -> str: """Return energy site type.""" return self._energysite[RESOURCE_TYPE] - @property - def site_name(self) -> str: - """Return energy site name.""" - # "site_name" not a valid key if name never set in Tesla app - return self._energysite.get("site_name", DEFAULT_ENERGYSITE_NAME) - async def _send_command( self, name: str, *, path_vars: dict, wake_if_asleep: bool = False, **kwargs ) -> dict: @@ -73,26 +67,34 @@ class SolarSite(EnergySite): by :meth:`teslajsonpy.controller.generate_energysite_objects`. """ - def __init__(self, api, energysite, data) -> None: + def __init__( + self, api: Callable, energysite: dict, site_config: dict, site_data: dict + ) -> None: """Initialize SolarSite.""" - super().__init__(api, energysite, data) + super().__init__(api, energysite) + self._site_config: dict = site_config + self._site_data: dict = site_data @property def grid_power(self) -> float: """Return grid power in Watts.""" - # Add check to see if site has power metering? - return self._data["grid_power"] + return self._site_data.get("grid_power") @property def load_power(self) -> float: """Return load power in Watts.""" - # Add check to see if site has power metering? - return self._data["load_power"] + return self._site_data.get("load_power") + + @property + def site_name(self) -> str: + """Return energy site name.""" + # "site_name" not a valid key if name never set in Tesla app + return self._site_config.get("site_name", DEFAULT_ENERGYSITE_NAME) @property def solar_power(self) -> float: """Return solar power in Watts.""" - return self._data["solar_power"] + return self._site_data.get("solar_power") @property def solar_type(self) -> str: @@ -107,50 +109,69 @@ class PowerwallSite(EnergySite): by :meth:`teslajsonpy.controller.generate_energysite_objects`. """ - def __init__(self, api, energysite, data) -> None: + def __init__( + self, api: Callable, energysite: dict, battery_data: dict, battery_summary: dict + ) -> None: """Initialize PowerwallSite.""" - super().__init__(api, energysite, data) + super().__init__(api, energysite) + self._battery_data: dict = battery_data + self._battery_summary: dict = battery_summary @property def backup_reserve_percent(self) -> int: """Return backup reserve percentage.""" - return self._data["backup_reserve_percent"] + return self._battery_data.get("backup").get("backup_reserve_percent") @property def battery_power(self) -> float: """Return battery power in Watts.""" - return self._data["battery_power"] + if self._battery_data.get("power_reading"): + return self._battery_data["power_reading"][0]["battery_power"] @property def energy_left(self) -> float: """Return battery energy left in Watt hours.""" - return self._data["energy_left"] + return self._battery_summary.get("energy_left") @property def grid_power(self) -> float: - # Grid and load power are the same in SolarSite because of how we store - # the data. It comes from two different endpoints but we stored in self._data - return self._data["grid_power"] + """Return grid power in Watts.""" + if self._battery_data.get("power_reading"): + return self._battery_data["power_reading"][0]["grid_power"] @property def grid_status(self) -> str: """Return grid status.""" - return self._data["grid_status"] + return self._battery_data.get("grid_status") @property def load_power(self) -> float: """Return load power in Watts.""" - return self._data["load_power"] + if self._battery_data.get("power_reading"): + return self._battery_data["power_reading"][0]["load_power"] @property def operation_mode(self) -> str: """Return operation mode.""" - return self._data["operation"] + return self._battery_data.get("operation") @property def percentage_charged(self) -> float: """Return battery percentage charged.""" - return self._data["percentage_charged"] + # percentage_charged sometimes incorrectly reports 0 + return self._battery_summary.get("percentage_charged") + + @property + def site_name(self) -> str: + """Return energy site name.""" + # "site_name" not a valid key if name never set in Tesla app + return self._battery_data.get("site_name", DEFAULT_ENERGYSITE_NAME) + + @property + def solar_power(self) -> float: + """Return solar power in Watts.""" + if self._battery_data.get("power_reading"): + return self._battery_data["power_reading"][0]["solar_power"] async def set_operation_mode(self, real_mode: str) -> None: """Set operation mode of Powerwall. @@ -163,7 +184,7 @@ async def set_operation_mode(self, real_mode: str) -> None: default_real_mode=real_mode, ) if data and data["response"]["code"] == 201: - self._data["operation"] = real_mode + self._battery_data.update({"operation": real_mode}) async def set_reserve_percent(self, value: int) -> None: """Set reserve percentage of Powerwall. @@ -176,40 +197,52 @@ async def set_reserve_percent(self, value: int) -> None: backup_reserve_percent=int(value), ) if data and data["response"]["code"] == 201: - self._data["backup_reserve_percent"] = value + self._battery_data["backup"].update({"backup_reserve_percent": value}) -class SolarPowerwallSite(PowerwallSite, SolarSite): +class SolarPowerwallSite(PowerwallSite): """Represents a Tesla Energy Solar site with Powerwall(s). This class shouldn't be instantiated directly; it will be instantiated by :meth:`teslajsonpy.controller.generate_energysite_objects`. """ - def __init__(self, api, energysite, data) -> None: + def __init__( + self, api: Callable, energysite: dict, battery_data: dict, battery_summary: dict + ) -> None: """Initialize SolarPowerwallSite.""" - super().__init__(api, energysite, data) + super().__init__(api, energysite, battery_data, battery_summary) @property def export_rule(self) -> str: """Return energy export rule setting.""" - return self._data["customer_preferred_export_rule"] + return self._battery_data["components"].get("customer_preferred_export_rule") @property def grid_charging(self) -> bool: """Return grid charging.""" - return not self._data["disallow_charge_from_grid_with_solar_installed"] + # Key is missing from battery_data when False + return not self._battery_data["components"].get( + "disallow_charge_from_grid_with_solar_installed", False + ) + + @property + def solar_type(self) -> str: + """Return type of solar (e.g. pv_panels or roof).""" + return self._battery_data["components"].get("solar_type") async def set_grid_charging(self, value: bool) -> None: """Set grid charging setting of Powerwall.""" - param = not value + value = not value await self._send_command( "ENERGY_SITE_IMPORT_EXPORT_CONFIG", path_vars={"site_id": self.energysite_id}, - disallow_charge_from_grid_with_solar_installed=param, + disallow_charge_from_grid_with_solar_installed=value, ) # This endpoint returns an empty response instead of a result code - self._data["disallow_charge_from_grid_with_solar_installed"] = param + self._battery_data["components"].update( + {"disallow_charge_from_grid_with_solar_installed": value} + ) async def set_export_rule(self, setting: str) -> None: """Set energy export setting of Powerwall. @@ -224,5 +257,6 @@ async def set_export_rule(self, setting: str) -> None: customer_preferred_export_rule=setting, ) # This endpoint returns an empty response instead of a result code - # Add check to only set if not a bad response? - self._data["customer_preferred_export_rule"] = setting + self._battery_data["components"].update( + {"customer_preferred_export_rule": setting} + ) diff --git a/tests/tesla_mock.py b/tests/tesla_mock.py index d50f619b..b657ba9c 100644 --- a/tests/tesla_mock.py +++ b/tests/tesla_mock.py @@ -1,5 +1,4 @@ """Tesla mock.""" - import copy from teslajsonpy.controller import Controller @@ -40,33 +39,13 @@ def __init__(self, monkeypatch) -> None: self._monkeypatch.setattr( Controller, "get_site_config", self.mock_get_site_config ) - # self._monkeypatch.setattr( - # Controller, - # "_get_and_process_car_data", - # self.mock_get_and_process_car_data, - # ) - # self._monkeypatch.setattr( - # Controller, - # "_get_and_process_site_data", - # self.mock_get_and_process_site_data, - # ) - # self._monkeypatch.setattr( - # Controller, - # "_get_and_process_battery_data", - # self.mock_get_and_process_battery_data, - # ) - # self._monkeypatch.setattr( - # Controller, "get_last_update_time", self.mock_get_last_update_time - # ) self._monkeypatch.setattr(Controller, "update", self.mock_update) - self._monkeypatch.setattr( - Controller, "get_power_params", self.mock_get_power_params - ) self._energysites = copy.deepcopy(ENERGYSITES) self._product_list = copy.deepcopy(PRODUCT_LIST) self._vehicle_data = copy.deepcopy(VEHICLE_DATA) self._site_data = copy.deepcopy(SITE_DATA) self._battery_data = copy.deepcopy(BATTERY_DATA) + self._battery_summary = copy.deepcopy(BATTERY_SUMMARY) self._drive_state = copy.deepcopy(VEHICLE_DATA["drive_state"]) self._climate_state = copy.deepcopy(VEHICLE_DATA["climate_state"]) self._charge_state = copy.deepcopy(VEHICLE_DATA["charge_state"]) @@ -81,11 +60,6 @@ def mock_api(self, *args, **kwargs): """Mock controller's api method.""" return self.controller_api() - def mock_connect(self, *args, **kwargs): - # pylint: disable=unused-argument - """Mock controller's connect method.""" - return self.controller_connect() - def mock_command(self, *args, **kwargs): # pylint: disable=unused-argument """Mock controller's command method.""" @@ -101,11 +75,6 @@ def mock_get_climate_params(self, *args, **kwargs): """Mock controller's get_climate_params method.""" return self.controller_get_climate_params() - def mock_get_power_params(self, *args, **kwargs): - # pylint: disable=unused-argument - """Mock controller's get_power_params method.""" - return self.controller_get_power_params() - def mock_get_power_unknown_grid_params(self, *args, **kwargs): # pylint: disable=unused-argument """Mock controller's get_power_unknown_grid_params method.""" @@ -136,21 +105,6 @@ def mock_get_site_config(self, *args, **kwargs): """Mock controller's get_site_config method.""" return self.controller_get_site_config() - def mock_get_and_process_car_data(self, *args, **kwargs): - # pylint: disable=unused-argument - """Mock controller's _get_and_process_car_data method.""" - return self.controller_get_and_process_car_data() - - def mock_get_and_process_site_data(self, *args, **kwargs): - # pylint: disable=unused-argument - """Mock controller's _get_and_process_site_data method.""" - return self.controller_get_and_process_site_data() - - def mock_get_and_process_battery_data(self, *args, **kwargs): - # pylint: disable=unused-argument - """Mock controller's _get_and_process_battery_data method.""" - return self.controller_get_and_process_battery_data() - def mock_get_last_update_time(self, *args, **kwargs): # pylint: disable=unused-argument """Mock controller's get_last_update_time method.""" @@ -161,11 +115,6 @@ def mock_update(self, *args, **kwargs): """Mock controller's update method.""" return self.controller_update() - @staticmethod - def controller_connect(): - """Monkeypatch for controller.connect().""" - return ("abc123", "cba321") - @staticmethod async def controller_api(): """Monkeypatch for controller.command().""" @@ -184,10 +133,6 @@ def controller_get_climate_params(self): """Monkeypatch for controller.get_climate_params().""" return self._climate_state - def controller_get_power_params(self): - """Monkeypatch for controller.get_power_params().""" - return self._site_data - def controller_get_power_unknown_grid_params(self): """Monkeypatch for controller.get_power_params() with grid unknown.""" return self._site_data_unknown_grid @@ -212,18 +157,6 @@ async def controller_get_site_config(self): """Monkeypatch for controller.get_site_config().""" return self._site_config - async def controller_get_and_process_car_data(self): - """Monkeypatch for controller.update._get_and_process_car_data().""" - return self._vehicle_data - - async def controller_get_and_process_site_data(self): - """Monkeypatch for controller.update._get_and_process_site_data().""" - return self._site_data - - async def controller_get_and_process_battery_data(self): - """Monkeypatch for controller.update._get_and_process_battery_data().""" - return self._battery_data - @staticmethod async def controller_update(): """Monkeypatch for controller.update().""" @@ -254,10 +187,26 @@ def data_request_energysites(self): """Similate the result of combined product list & site config request.""" return self._energysites + def data_request_site_config(self): + """Get site_data.""" + return self._site_config + + def data_request_site_data(self): + """Get site_data.""" + return self._site_data + def data_request_site_data_unknown_grid(self): """Similate the result of site state with unknown grid data request.""" return self._site_data_unknown_grid + def data_request_battery_data(self): + """Get battery_data.""" + return self._battery_data + + def data_request_battery_summary(self): + """Get battery_summary.""" + return self._battery_summary + @staticmethod def command_ok(): """Simulate an OK result for a command.""" @@ -754,3 +703,12 @@ def command_ok(): ], "battery_count": 1, } + +BATTERY_SUMMARY = { + "site_name": "My Battery Home", + "id": "XXX", + "energy_left": 13610.736842105263, + "total_pack_energy": 14056, + "percentage_charged": 96.8322199922116, + "battery_power": 400, +} diff --git a/tests/unit_tests/test_energy.py b/tests/unit_tests/test_energy.py index 5399d8e1..d054ef36 100644 --- a/tests/unit_tests/test_energy.py +++ b/tests/unit_tests/test_energy.py @@ -1,12 +1,18 @@ """Test energy sites.""" - import pytest from teslajsonpy.const import DEFAULT_ENERGYSITE_NAME from teslajsonpy.controller import Controller -from teslajsonpy.energy import EnergySite +from teslajsonpy.energy import SolarSite -from tests.tesla_mock import TeslaMock, ENERGYSITES, SITE_CONFIG +from tests.tesla_mock import ( + BATTERY_DATA, + BATTERY_SUMMARY, + ENERGYSITES, + SITE_CONFIG, + SITE_DATA, + TeslaMock, +) @pytest.mark.asyncio @@ -28,22 +34,17 @@ async def test_energysite_setup(monkeypatch): @pytest.mark.asyncio async def test_solar_site(monkeypatch): """Test SolarSite class.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + # Add site_data since we're not mocking Controller.update() + _controller._site_data = {12345: _mock.data_request_site_data()} _controller.generate_energysite_objects() _solar_site = _controller.energysites[12345] assert _solar_site._api is not None assert _solar_site._energysite is not None - assert _solar_site._data == { - "solar_power": 0, - "load_power": 0, - "grid_power": 0, - "battery_power": 0, - "percentage_charged": 0, - } assert _solar_site.energysite_id == ENERGYSITES[0]["energy_site_id"] assert _solar_site.has_battery == ENERGYSITES[0]["components"]["battery"] @@ -53,31 +54,27 @@ async def test_solar_site(monkeypatch): assert _solar_site.resource_type == ENERGYSITES[0]["resource_type"] assert _solar_site.site_name == SITE_CONFIG["site_name"] - assert _solar_site.grid_power == 0 - assert _solar_site.load_power == 0 - assert _solar_site.solar_power == 0 + assert _solar_site.grid_power == SITE_DATA["grid_power"] + assert _solar_site.load_power == SITE_DATA["load_power"] + assert _solar_site.solar_power == SITE_DATA["solar_power"] assert _solar_site.solar_type == ENERGYSITES[0]["components"]["solar_type"] @pytest.mark.asyncio async def test_powerwall_site(monkeypatch): """Test PowerwallSite class.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + # Add battery_data and battery_summary since we're not mocking Controller.update() + _controller._battery_data = {67890: _mock.data_request_battery_data()} + _controller._battery_summary = {67890: _mock.data_request_battery_summary()} _controller.generate_energysite_objects() _solar_powerwall_site = _controller.energysites[67890] assert _solar_powerwall_site._api is not None assert _solar_powerwall_site._energysite is not None - assert _solar_powerwall_site._data == { - "solar_power": 0, - "load_power": 0, - "grid_power": 0, - "battery_power": 0, - "percentage_charged": 0, - } assert _solar_powerwall_site.energysite_id == ENERGYSITES[1]["energy_site_id"] assert ( @@ -89,12 +86,26 @@ async def test_powerwall_site(monkeypatch): assert _solar_powerwall_site.has_solar == ENERGYSITES[1]["components"]["solar"] assert _solar_powerwall_site.resource_type == ENERGYSITES[1]["resource_type"] assert _solar_powerwall_site.site_name == ENERGYSITES[1]["site_name"] - - assert _solar_powerwall_site.percentage_charged == 0 - assert _solar_powerwall_site.battery_power == 0 - assert _solar_powerwall_site.grid_power == 0 - assert _solar_powerwall_site.load_power == 0 - assert _solar_powerwall_site.solar_power == 0 + assert ( + _solar_powerwall_site.percentage_charged + == BATTERY_SUMMARY["percentage_charged"] + ) + assert ( + _solar_powerwall_site.battery_power + == BATTERY_DATA["power_reading"][0]["battery_power"] + ) + assert ( + _solar_powerwall_site.grid_power + == BATTERY_DATA["power_reading"][0]["grid_power"] + ) + assert ( + _solar_powerwall_site.load_power + == BATTERY_DATA["power_reading"][0]["load_power"] + ) + assert ( + _solar_powerwall_site.solar_power + == BATTERY_DATA["power_reading"][0]["solar_power"] + ) assert ( _solar_powerwall_site.solar_type == ENERGYSITES[1]["components"]["solar_type"] ) @@ -106,8 +117,9 @@ async def test_energysite_with_no_name(monkeypatch): _mock = TeslaMock(monkeypatch) _api = Controller(None) _energysite = _mock.data_request_energysites()[0] - _energysite_data = _mock.controller_get_power_params() - _sensor = EnergySite(_api, _energysite, _energysite_data) + _site_config = {12345: _mock.data_request_site_config()} + _site_data = {12345: _mock.data_request_site_data()} + _sensor = SolarSite(_api, _energysite, _site_config, _site_data) assert _sensor.site_name == DEFAULT_ENERGYSITE_NAME @@ -115,9 +127,11 @@ async def test_energysite_with_no_name(monkeypatch): @pytest.mark.asyncio async def test_set_operation_mode(monkeypatch): """Test set operation mode.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._battery_data = {67890: _mock.data_request_battery_data()} + _controller._battery_summary = {67890: _mock.data_request_battery_summary()} _controller.generate_energysite_objects() _energysite = _controller.energysites[67890] @@ -127,9 +141,11 @@ async def test_set_operation_mode(monkeypatch): @pytest.mark.asyncio async def test_set_reserve_percent(monkeypatch): """Test set reserve percent.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._battery_data = {67890: _mock.data_request_battery_data()} + _controller._battery_summary = {67890: _mock.data_request_battery_summary()} _controller.generate_energysite_objects() _energysite = _controller.energysites[67890] @@ -139,9 +155,11 @@ async def test_set_reserve_percent(monkeypatch): @pytest.mark.asyncio async def test_set_grid_charging(monkeypatch): """Test set grid charging.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._battery_data = {67890: _mock.data_request_battery_data()} + _controller._battery_summary = {67890: _mock.data_request_battery_summary()} _controller.generate_energysite_objects() _energysite = _controller.energysites[67890] @@ -151,9 +169,11 @@ async def test_set_grid_charging(monkeypatch): @pytest.mark.asyncio async def test_set_export_rule(monkeypatch): """Test set export rule.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._battery_data = {67890: _mock.data_request_battery_data()} + _controller._battery_summary = {67890: _mock.data_request_battery_summary()} _controller.generate_energysite_objects() _energysite = _controller.energysites[67890] From ecc4678139d06bc8136a99b849eb8c31196abd1a Mon Sep 17 00:00:00 2001 From: shred86 Date: Sat, 3 Sep 2022 18:51:04 -0700 Subject: [PATCH 52/84] Remove asyncio.Lock for energy data --- teslajsonpy/controller.py | 95 +++++++++++++++++---------------------- 1 file changed, 41 insertions(+), 54 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 6d09db43..eb78eefd 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -429,7 +429,6 @@ async def connect( for energysite in self._energysite_list: energysite_id = energysite.get("energy_site_id") - battery_id = energysite.get("id") if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: # site_name comes from site_config for non-Powerwall sites @@ -445,9 +444,6 @@ async def connect( # Default to True and check during updates self._grid_status_unknown = {energysite_id: True} - self.__lock[energysite_id] = asyncio.Lock() - self.__lock[battery_id] = asyncio.Lock() - if not test_login: try: await self.update(wake_if_asleep=wake_if_asleep) @@ -781,70 +777,61 @@ async def _get_and_process_car_data(vin: Text) -> None: ) async def _get_and_process_site_data(energysite_id: int) -> None: - async with self.__lock[energysite_id]: - _LOGGER.debug("Updating SITE_DATA for energysite: %s", energysite_id) + _LOGGER.debug("Updating SITE_DATA for energysite: %s", energysite_id) + try: + data = await self.api("SITE_DATA", path_vars={"site_id": energysite_id}) + except TeslaException: + data = None + + if data and data["response"]: + response = data["response"] + # Some setups always report grid_status of "Unknown" regardless + # of the actual grid status. Others only report grid_status "Unknown" + # when the actual grid status is unknown. These setups also sometimes + # report an incorrect solar_power value of 0. + if ( + "grid_status" not in response + or response.get("grid_status") != "Unknown" + ): + self._grid_status_unknown[energysite_id] = False - try: - data = await self.api( - "SITE_DATA", path_vars={"site_id": energysite_id} + if not self._grid_status_unknown[energysite_id] and ( + response.get("grid_status") == "Unknown" + and response.get("solar_power") == 0 + ): + _LOGGER.debug( + "Ignoring possible spurious energy site solar power read." ) - except TeslaException: - data = None - - if data and data["response"]: - response = data["response"] - # Some setups always report grid_status of "Unknown" regardless - # of the actual grid status. Others only report grid_status "Unknown" - # when the actual grid status is unknown. These setups also sometimes - # report an incorrect solar_power value of 0. - if ( - "grid_status" not in response - or response.get("grid_status") != "Unknown" - ): - self._grid_status_unknown[energysite_id] = False - - if not self._grid_status_unknown[energysite_id] and ( - response.get("grid_status") == "Unknown" - and response.get("solar_power") == 0 - ): - _LOGGER.debug( - "Ignoring possible spurious energy site solar power read." - ) - del response["solar_power"] + del response["solar_power"] - self._site_data[energysite_id].update(response) + self._site_data[energysite_id].update(response) async def _get_and_process_battery_data( energysite_id: int, battery_id: str ) -> None: - async with self.__lock[battery_id]: - _LOGGER.debug("Updating BATTERY_DATA for energysite: %s", energysite_id) - - try: - data = await self.api( - "BATTERY_DATA", path_vars={"battery_id": battery_id} - ) - except TeslaException: - data = None + _LOGGER.debug("Updating BATTERY_DATA for energysite: %s", energysite_id) + try: + data = await self.api( + "BATTERY_DATA", path_vars={"battery_id": battery_id} + ) + except TeslaException: + data = None - self._battery_data[energysite_id].update(data) + self._battery_data[energysite_id].update(data) async def _get_and_process_battery_summary( energysite_id: int, battery_id: str ) -> None: - async with self.__lock[battery_id]: - _LOGGER.debug( - "Updating BATTERY_SUMMARY for energysite: %s", energysite_id - ) + _LOGGER.debug("Updating BATTERY_SUMMARY for energysite: %s", energysite_id) - try: - data = await self.api( - "BATTERY_SUMMARY", path_vars={"battery_id": battery_id} - ) - except TeslaException: - data = None + try: + data = await self.api( + "BATTERY_SUMMARY", path_vars={"battery_id": battery_id} + ) + except TeslaException: + data = None - self._battery_summary[energysite_id].update(data) + self._battery_summary[energysite_id].update(data) async with self.__update_lock: if self._include_vehicles: From e09b1f6c30ec666ffc82b3b201cd25441eb03779 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 4 Sep 2022 07:08:38 -0700 Subject: [PATCH 53/84] Fix getting battery data and summary --- teslajsonpy/controller.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index eb78eefd..853a80f7 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -817,7 +817,8 @@ async def _get_and_process_battery_data( except TeslaException: data = None - self._battery_data[energysite_id].update(data) + if data and data["response"]: + self._battery_data[energysite_id].update(data["response"]) async def _get_and_process_battery_summary( energysite_id: int, battery_id: str @@ -831,7 +832,8 @@ async def _get_and_process_battery_summary( except TeslaException: data = None - self._battery_summary[energysite_id].update(data) + if data and data["response"]: + self._battery_summary[energysite_id].update(data["response"]) async with self.__update_lock: if self._include_vehicles: From 0cca9eacd3b63991a947f93eb6a734d4517cdc26 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 4 Sep 2022 07:12:22 -0700 Subject: [PATCH 54/84] Update for site data --- teslajsonpy/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 853a80f7..0b5ab10a 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -804,7 +804,7 @@ async def _get_and_process_site_data(energysite_id: int) -> None: ) del response["solar_power"] - self._site_data[energysite_id].update(response) + self._site_data[energysite_id].update(response) async def _get_and_process_battery_data( energysite_id: int, battery_id: str From e640938c7ec114ce0a1c6f8dfadd15f35ea7fced Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 4 Sep 2022 14:24:57 -0700 Subject: [PATCH 55/84] Use actual key for rear heated seats --- teslajsonpy/car.py | 46 ++++++++++++++++++++---------------- tests/tesla_mock.py | 16 +++++++++++-- tests/unit_tests/test_car.py | 4 +--- 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 9f785435..435d9ba4 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -295,9 +295,7 @@ def is_charge_port_door_open(self) -> bool: @property def is_climate_on(self) -> bool: """Return climate is on.""" - return self._controller.get_climate_params(vin=self.vin).get( - "is_climate_on", False - ) + return self._controller.get_climate_params(vin=self.vin).get("is_climate_on") @property def is_frunk_locked(self) -> int: @@ -321,7 +319,6 @@ def is_locked(self) -> bool: @property def is_steering_wheel_heater_on(self) -> bool: """Return steering wheel heater.""" - # Not seeing this in the JSON response for 2015 Model S 85D on 28 Aug 2022 return self._controller.get_climate_params(vin=self.vin).get( "steering_wheel_heater" ) @@ -399,15 +396,13 @@ def outside_temp(self) -> float: return self._controller.get_climate_params(vin=self.vin).get("outside_temp") @property - def rear_heated_seats(self) -> bool: - """Return if car has rear (second row) heated seats.""" - # Assuming if rear left doesn't have it, there's no rear seat heating - if self._controller.get_climate_params(vin=self.vin).get( - "seat_heater_rear_left" - ): - return True - else: - return False + def rear_seat_heaters(self) -> int: + """Return if car has rear (second row) heated seats. + + Returns + int: 0 (no rear heated seats), int: ? (rear heated seats) + """ + return self._controller.get_config_params(vin=self.vin).get("rear_seat_heaters") @property def sentry_mode(self) -> bool: @@ -650,21 +645,32 @@ async def set_heated_steering_wheel(self, value: bool) -> None: params = {"steering_wheel_heater": value} self._controller.update_climate_params(vin=self.vin, params=params) - async def set_hvac_mode(self, on_off: str) -> None: - """Send command to set HVAC mode.""" - # Better name for on_off? - if on_off == "off": - await self._send_command( + async def set_hvac_mode(self, value: str) -> None: + """Send command to set HVAC mode. + + Args + "off" + "on" + """ + if value == "off": + data = await self._send_command( "CLIMATE_OFF", path_vars={"vehicle_id": self.id}, wake_if_asleep=True, ) - elif on_off == "on": - await self._send_command( + if data and data["response"]["result"] is True: + params = {"is_climate_on": False} + self._controller.update_climate_params(vin=self.vin, params=params) + + elif value == "on": + data = await self._send_command( "CLIMATE_ON", path_vars={"vehicle_id": self.id}, wake_if_asleep=True, ) + if data and data["response"]["result"] is True: + params = {"is_climate_on": True} + self._controller.update_climate_params(vin=self.vin, params=params) async def set_max_defrost(self, state: int) -> None: """Send command to set max defrost. diff --git a/tests/tesla_mock.py b/tests/tesla_mock.py index b657ba9c..4370a3a1 100644 --- a/tests/tesla_mock.py +++ b/tests/tesla_mock.py @@ -24,6 +24,9 @@ def __init__(self, monkeypatch) -> None: self._monkeypatch.setattr( Controller, "get_climate_params", self.mock_get_climate_params ) + self._monkeypatch.setattr( + Controller, "get_config_params", self.mock_get_config_params + ) self._monkeypatch.setattr( Controller, "get_drive_params", self.mock_get_drive_params ) @@ -75,6 +78,11 @@ def mock_get_climate_params(self, *args, **kwargs): """Mock controller's get_climate_params method.""" return self.controller_get_climate_params() + def mock_get_config_params(self, *args, **kwargs): + # pylint: disable=unused-argument + """Mock controller's get_config_params method.""" + return self.controller_get_config_params() + def mock_get_power_unknown_grid_params(self, *args, **kwargs): # pylint: disable=unused-argument """Mock controller's get_power_unknown_grid_params method.""" @@ -133,6 +141,10 @@ def controller_get_climate_params(self): """Monkeypatch for controller.get_climate_params().""" return self._climate_state + def controller_get_config_params(self): + """Monkeypatch for controller.get_climate_params().""" + return self._vehicle_config + def controller_get_power_unknown_grid_params(self): """Monkeypatch for controller.get_power_params() with grid unknown.""" return self._site_data_unknown_grid @@ -184,7 +196,7 @@ def data_request_vehicle_state(self): return self._vehicle_state def data_request_energysites(self): - """Similate the result of combined product list & site config request.""" + """Simulate the result of combined product list & site config request.""" return self._energysites def data_request_site_config(self): @@ -196,7 +208,7 @@ def data_request_site_data(self): return self._site_data def data_request_site_data_unknown_grid(self): - """Similate the result of site state with unknown grid data request.""" + """Simulate the result of site state with unknown grid data request.""" return self._site_data_unknown_grid def data_request_battery_data(self): diff --git a/tests/unit_tests/test_car.py b/tests/unit_tests/test_car.py index 2b99f440..0fac07d9 100644 --- a/tests/unit_tests/test_car.py +++ b/tests/unit_tests/test_car.py @@ -184,9 +184,7 @@ async def test_car_properties(monkeypatch): assert _car.outside_temp == VEHICLE_DATA["climate_state"]["outside_temp"] - assert _car.rear_heated_seats == VEHICLE_DATA["climate_state"].get( - "seat_heater_rear_left", False - ) + assert _car.rear_seat_heaters == VEHICLE_DATA["vehicle_config"]["rear_seat_heaters"] assert _car.sentry_mode == VEHICLE_DATA["vehicle_state"].get("sentry_mode") From 3e62649c3fd48619f56684a6bcc77070886f9f91 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 4 Sep 2022 14:44:24 -0700 Subject: [PATCH 56/84] Use get method --- teslajsonpy/energy.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index d013f5fb..abb8bf13 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -216,20 +216,22 @@ def __init__( @property def export_rule(self) -> str: """Return energy export rule setting.""" - return self._battery_data["components"].get("customer_preferred_export_rule") + return self._battery_data.get("components").get( + "customer_preferred_export_rule" + ) @property def grid_charging(self) -> bool: """Return grid charging.""" # Key is missing from battery_data when False - return not self._battery_data["components"].get( + return not self._battery_data.get("components").get( "disallow_charge_from_grid_with_solar_installed", False ) @property def solar_type(self) -> str: """Return type of solar (e.g. pv_panels or roof).""" - return self._battery_data["components"].get("solar_type") + return self._battery_data.get("components").get("solar_type") async def set_grid_charging(self, value: bool) -> None: """Set grid charging setting of Powerwall.""" From e2425b26dd3a46ec88da04cd7a4e918c7b835772 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 4 Sep 2022 21:27:55 -0700 Subject: [PATCH 57/84] Allow setting amps below 5 --- teslajsonpy/car.py | 8 ++++++++ teslajsonpy/const.py | 2 +- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 435d9ba4..2bf7c7d7 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -586,6 +586,14 @@ async def set_charging_amps(self, value: float) -> None: charging_amps=int(value), wake_if_asleep=True, ) + # A second API call allows setting below 5 Amps + if value < 5: + data = await self._send_command( + "CHARGING_AMPS", + path_vars={"vehicle_id": self.id}, + charging_amps=int(value), + wake_if_asleep=True, + ) if data and data["response"]["result"] is True: params = {"charge_amps": int(value)} diff --git a/teslajsonpy/const.py b/teslajsonpy/const.py index 96f48bc1..682f1ede 100644 --- a/teslajsonpy/const.py +++ b/teslajsonpy/const.py @@ -20,7 +20,7 @@ BACKUP_RESERVE_MAX = 100 BACKUP_RESERVE_MIN = 0 -CHARGE_CURRENT_MIN = 5 +CHARGE_CURRENT_MIN = 0 DEFAULT_ENERGYSITE_NAME = "My Home" GRID_ACTIVE = "Active" PRODUCT_TYPE_ENERGY_SITES = "energy_sites" From 3eb6a8d6d1c4062684977cb465b993a070252b1e Mon Sep 17 00:00:00 2001 From: shred86 Date: Mon, 5 Sep 2022 08:06:50 -0700 Subject: [PATCH 58/84] Get site config for battery sites --- teslajsonpy/controller.py | 19 +++++++++---------- teslajsonpy/energy.py | 35 +++++++++++++++++++++++++---------- 2 files changed, 34 insertions(+), 20 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 0b5ab10a..e5c9bef8 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -430,16 +430,13 @@ async def connect( for energysite in self._energysite_list: energysite_id = energysite.get("energy_site_id") - if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: - # site_name comes from site_config for non-Powerwall sites - self._site_config[energysite_id] = await self.get_site_config( - energysite_id - ) - self._site_data[energysite_id] = {} - - if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_BATTERY: - self._battery_data[energysite_id] = {} - self._battery_summary[energysite_id] = {} + self._site_config[energysite_id] = await self.get_site_config( + energysite_id + ) + # These will get updated when self.update is called below + self._site_data[energysite_id] = {} + self._battery_data[energysite_id] = {} + self._battery_summary[energysite_id] = {} # For dealing with sites that always report "Unknown" # Default to True and check during updates self._grid_status_unknown = {energysite_id: True} @@ -572,6 +569,7 @@ def generate_energysite_objects(self) -> Dict[int, EnergySite]: self.energysites[energysite_id] = SolarPowerwallSite( self.api, energysite, + self._site_config[energysite_id], self._battery_data[energysite_id], self._battery_summary[energysite_id], ) @@ -583,6 +581,7 @@ def generate_energysite_objects(self) -> Dict[int, EnergySite]: self.energysites[energysite_id] = PowerwallSite( self.api, energysite, + self._site_config[energysite_id], self._battery_data[energysite_id], self._battery_summary[energysite_id], ) diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index abb8bf13..7da0a96a 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -13,10 +13,11 @@ class EnergySite: """Base class to represents a Tesla energy site.""" - def __init__(self, api: Callable, energysite: dict) -> None: + def __init__(self, api: Callable, energysite: dict, site_config: dict) -> None: """Initialize EnergySite.""" - self._api: Callable = api - self._energysite: dict = energysite + self._api = api + self._energysite = energysite + self._site_config = site_config @property def energysite_id(self) -> int: @@ -71,9 +72,8 @@ def __init__( self, api: Callable, energysite: dict, site_config: dict, site_data: dict ) -> None: """Initialize SolarSite.""" - super().__init__(api, energysite) - self._site_config: dict = site_config - self._site_data: dict = site_data + super().__init__(api, energysite, site_config) + self._site_data = site_data @property def grid_power(self) -> float: @@ -110,10 +110,15 @@ class PowerwallSite(EnergySite): """ def __init__( - self, api: Callable, energysite: dict, battery_data: dict, battery_summary: dict + self, + api: Callable, + energysite: dict, + site_config: dict, + battery_data: dict, + battery_summary: dict, ) -> None: """Initialize PowerwallSite.""" - super().__init__(api, energysite) + super().__init__(api, energysite, site_config) self._battery_data: dict = battery_data self._battery_summary: dict = battery_summary @@ -173,6 +178,11 @@ def solar_power(self) -> float: if self._battery_data.get("power_reading"): return self._battery_data["power_reading"][0]["solar_power"] + @property + def version(self) -> float: + """Return firmware version.""" + return self._site_config.get("version") + async def set_operation_mode(self, real_mode: str) -> None: """Set operation mode of Powerwall. @@ -208,10 +218,15 @@ class SolarPowerwallSite(PowerwallSite): """ def __init__( - self, api: Callable, energysite: dict, battery_data: dict, battery_summary: dict + self, + api: Callable, + energysite: dict, + site_config: dict, + battery_data: dict, + battery_summary: dict, ) -> None: """Initialize SolarPowerwallSite.""" - super().__init__(api, energysite, battery_data, battery_summary) + super().__init__(api, energysite, site_config, battery_data, battery_summary) @property def export_rule(self) -> str: From efd242186ccdb885af099a83f9213e26d562da69 Mon Sep 17 00:00:00 2001 From: shred86 Date: Mon, 5 Sep 2022 16:08:57 -0700 Subject: [PATCH 59/84] Fix for car online --- poetry.lock | 89 +++++++++++++++++++++------------------------- teslajsonpy/car.py | 6 ++-- 2 files changed, 43 insertions(+), 52 deletions(-) diff --git a/poetry.lock b/poetry.lock index db07f818..ad888c34 100644 --- a/poetry.lock +++ b/poetry.lock @@ -90,14 +90,6 @@ category = "main" optional = false python-versions = ">=3.5" -[[package]] -name = "atomicwrites" -version = "1.4.1" -description = "Atomic file writes." -category = "dev" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - [[package]] name = "attrs" version = "22.1.0" @@ -176,7 +168,7 @@ lxml = ["lxml"] [[package]] name = "black" -version = "22.6.0" +version = "22.8.0" description = "The uncompromising code formatter." category = "dev" optional = false @@ -518,11 +510,11 @@ pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" [[package]] name = "pathspec" -version = "0.9.0" +version = "0.10.1" description = "Utility library for gitignore style pattern matching of file paths." category = "dev" optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" +python-versions = ">=3.7" [[package]] name = "platformdirs" @@ -634,14 +626,13 @@ diagrams = ["railroad-diagrams", "jinja2"] [[package]] name = "pytest" -version = "7.1.2" +version = "7.1.3" description = "pytest: simple powerful testing with Python" category = "dev" optional = false python-versions = ">=3.7" [package.dependencies] -atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=19.2.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} @@ -742,11 +733,11 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "sniffio" -version = "1.2.0" +version = "1.3.0" description = "Sniff out which async library your code is running under" category = "main" optional = false -python-versions = ">=3.5" +python-versions = ">=3.7" [[package]] name = "snowballstemmer" @@ -1011,7 +1002,7 @@ socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.16.3" +version = "20.16.4" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1164,9 +1155,6 @@ asynctest = [ {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, {file = "asynctest-0.13.0.tar.gz", hash = "sha256:c27862842d15d83e6a34eb0b2866c323880eb3a75e4485b079ea11748fd77fac"}, ] -atomicwrites = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, -] attrs = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, @@ -1191,29 +1179,29 @@ beautifulsoup4 = [ {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, ] black = [ - {file = "black-22.6.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f586c26118bc6e714ec58c09df0157fe2d9ee195c764f630eb0d8e7ccce72e69"}, - {file = "black-22.6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b270a168d69edb8b7ed32c193ef10fd27844e5c60852039599f9184460ce0807"}, - {file = "black-22.6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6797f58943fceb1c461fb572edbe828d811e719c24e03375fd25170ada53825e"}, - {file = "black-22.6.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c85928b9d5f83b23cee7d0efcb310172412fbf7cb9d9ce963bd67fd141781def"}, - {file = "black-22.6.0-cp310-cp310-win_amd64.whl", hash = "sha256:f6fe02afde060bbeef044af7996f335fbe90b039ccf3f5eb8f16df8b20f77666"}, - {file = "black-22.6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cfaf3895a9634e882bf9d2363fed5af8888802d670f58b279b0bece00e9a872d"}, - {file = "black-22.6.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:94783f636bca89f11eb5d50437e8e17fbc6a929a628d82304c80fa9cd945f256"}, - {file = "black-22.6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:2ea29072e954a4d55a2ff58971b83365eba5d3d357352a07a7a4df0d95f51c78"}, - {file = "black-22.6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e439798f819d49ba1c0bd9664427a05aab79bfba777a6db94fd4e56fae0cb849"}, - {file = "black-22.6.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:187d96c5e713f441a5829e77120c269b6514418f4513a390b0499b0987f2ff1c"}, - {file = "black-22.6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:074458dc2f6e0d3dab7928d4417bb6957bb834434516f21514138437accdbe90"}, - {file = "black-22.6.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:a218d7e5856f91d20f04e931b6f16d15356db1c846ee55f01bac297a705ca24f"}, - {file = "black-22.6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:568ac3c465b1c8b34b61cd7a4e349e93f91abf0f9371eda1cf87194663ab684e"}, - {file = "black-22.6.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6c1734ab264b8f7929cef8ae5f900b85d579e6cbfde09d7387da8f04771b51c6"}, - {file = "black-22.6.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9a3ac16efe9ec7d7381ddebcc022119794872abce99475345c5a61aa18c45ad"}, - {file = "black-22.6.0-cp38-cp38-win_amd64.whl", hash = "sha256:b9fd45787ba8aa3f5e0a0a98920c1012c884622c6c920dbe98dbd05bc7c70fbf"}, - {file = "black-22.6.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7ba9be198ecca5031cd78745780d65a3f75a34b2ff9be5837045dce55db83d1c"}, - {file = "black-22.6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a3db5b6409b96d9bd543323b23ef32a1a2b06416d525d27e0f67e74f1446c8f2"}, - {file = "black-22.6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:560558527e52ce8afba936fcce93a7411ab40c7d5fe8c2463e279e843c0328ee"}, - {file = "black-22.6.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b154e6bbde1e79ea3260c4b40c0b7b3109ffcdf7bc4ebf8859169a6af72cd70b"}, - {file = "black-22.6.0-cp39-cp39-win_amd64.whl", hash = "sha256:4af5bc0e1f96be5ae9bd7aaec219c901a94d6caa2484c21983d043371c733fc4"}, - {file = "black-22.6.0-py3-none-any.whl", hash = "sha256:ac609cf8ef5e7115ddd07d85d988d074ed00e10fbc3445aee393e70164a2219c"}, - {file = "black-22.6.0.tar.gz", hash = "sha256:6c6d39e28aed379aec40da1c65434c77d75e65bb59a1e1c283de545fb4e7c6c9"}, + {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, + {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, + {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, + {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, + {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, + {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, + {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, + {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, + {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, + {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, + {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, + {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, + {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, + {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, + {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, + {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, + {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, + {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, + {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, + {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, + {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, + {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, + {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, ] certifi = [ {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, @@ -1504,8 +1492,8 @@ packaging = [ {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, ] pathspec = [ - {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"}, - {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"}, + {file = "pathspec-0.10.1-py3-none-any.whl", hash = "sha256:46846318467efc4556ccfd27816e004270a9eeeeb4d062ce5e6fc7a87c573f93"}, + {file = "pathspec-0.10.1.tar.gz", hash = "sha256:7ace6161b621d31e7902eb6b5ae148d12cfd23f4a249b9ffb6b9fee12084323d"}, ] platformdirs = [ {file = "platformdirs-2.5.2-py3-none-any.whl", hash = "sha256:027d8e83a2d7de06bbac4e5ef7e023c02b863d7ea5d079477e722bb41ab25788"}, @@ -1538,8 +1526,8 @@ pyparsing = [ {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, ] pytest = [ - {file = "pytest-7.1.2-py3-none-any.whl", hash = "sha256:13d0e3ccfc2b6e26be000cb6568c832ba67ba32e719443bfe725814d3c42433c"}, - {file = "pytest-7.1.2.tar.gz", hash = "sha256:a06a0425453864a270bc45e71f783330a7428defb4230fb5e6a731fde06ecd45"}, + {file = "pytest-7.1.3-py3-none-any.whl", hash = "sha256:1377bda3466d70b55e3f5cecfa55bb7cfcf219c7964629b967c37cf0bda818b7"}, + {file = "pytest-7.1.3.tar.gz", hash = "sha256:4f365fec2dff9c1162f834d9f18af1ba13062db0c708bf7b946f8a5c76180c39"}, ] pytest-asyncio = [ {file = "pytest-asyncio-0.19.0.tar.gz", hash = "sha256:ac4ebf3b6207259750bc32f4c1d8fcd7e79739edbc67ad0c58dd150b1d072fed"}, @@ -1598,8 +1586,8 @@ six = [ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, ] sniffio = [ - {file = "sniffio-1.2.0-py3-none-any.whl", hash = "sha256:471b71698eac1c2112a40ce2752bb2f4a4814c22a54a3eed3676bc0f5ca9f663"}, - {file = "sniffio-1.2.0.tar.gz", hash = "sha256:c4666eecec1d3f50960c6bdf61ab7bc350648da6c126e3cf6898d8cd4ddcd3de"}, + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, ] snowballstemmer = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, @@ -1697,7 +1685,10 @@ unidecode = [ {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, ] urllib3 = [] -virtualenv = [] +virtualenv = [ + {file = "virtualenv-20.16.4-py3-none-any.whl", hash = "sha256:035ed57acce4ac35c82c9d8802202b0e71adac011a511ff650cbcf9635006a22"}, + {file = "virtualenv-20.16.4.tar.gz", hash = "sha256:014f766e4134d0008dcaa1f95bafa0fb0f575795d07cae50b1bee514185d6782"}, +] wrapt = [ {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, {file = "wrapt-1.14.1-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:903500616422a40a98a5a3c4ff4ed9d0066f3b4c951fa286018ecdf0750194ef"}, diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 2bf7c7d7..d4585d19 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -324,11 +324,11 @@ def is_steering_wheel_heater_on(self) -> bool: ) @property - def is_trunk_locked(self) -> int: + def is_trunk_locked(self) -> bool: """Return car trunk is locked (closed). Returns - int: 0 (locked), 255 (unlocked) + bool: False (0), True (255) """ response = self._controller.get_state_params(vin=self.vin).get("rt") @@ -340,7 +340,7 @@ def is_trunk_locked(self) -> int: @property def is_on(self) -> bool: """Return car is on.""" - return self._controller.car_online[self.vin] + return self._controller.is_car_online(vin=self.vin) @property def longitude(self) -> float: From 6d9a1837d411f2305617ab3f87180deedf3480ba Mon Sep 17 00:00:00 2001 From: shred86 Date: Tue, 6 Sep 2022 21:29:34 -0700 Subject: [PATCH 60/84] Use vehicle_data and fixes --- teslajsonpy/car.py | 287 ++++++++++++++++------------------- teslajsonpy/controller.py | 7 +- tests/tesla_mock.py | 233 +++++++++++++++++++++++++++- tests/unit_tests/test_car.py | 70 ++++++--- 4 files changed, 412 insertions(+), 185 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index d4585d19..773644f4 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -6,15 +6,22 @@ _LOGGER = logging.getLogger(__name__) -SEAT_NAME_MAP = [ - "left", - "right", - "rear_left", - "rear_center", - "rear_right", - "third_row_left", - "third_row_right", -] +CLIMATE_KEEPER_ID_MAP = { + 0: "off", + 1: "on", + 2: "dog", + 3: "camp", +} + +SEAT_ID_MAP = { + 0: "left", + 1: "right", + 2: "rear_left", + 4: "rear_center", + 5: "rear_right", + 6: "third_row_left", + 7: "third_row_right", +} class TeslaCar: @@ -24,35 +31,36 @@ class TeslaCar: by :meth:`teslajsonpy.controller.generate_car_objects`. """ - def __init__(self, car, controller) -> None: + def __init__(self, car: dict, controller, vehicle_data: dict) -> None: """Initialize TeslaCar.""" self._car = car self._controller = controller + self._vehicle_data = vehicle_data @property def display_name(self) -> str: """Return display name.""" - return self._car.get("display_name") + return self._vehicle_data.get("display_name") @property def id(self) -> int: """Return id.""" - return self._car.get("id") + return self._vehicle_data.get("id") @property def state(self) -> str: """Return car state.""" - return self._car.get("state") + return self._vehicle_data.get("state") @property def vehicle_id(self) -> int: """Return car id.""" - return self._car.get("vehicle_id") + return self._vehicle_data.get("vehicle_id") @property def vin(self) -> str: """Return car vin.""" - return self._car.get("vin") + return self._vehicle_data.get("vin") @property def data_available(self) -> bool: @@ -62,129 +70,111 @@ def data_available(self) -> bool: @property def battery_level(self) -> float: """Return car battery level.""" - return self._controller.get_charging_params(vin=self.vin).get("battery_level") + return self._vehicle_data.get("charge_state").get("battery_level") @property def battery_range(self) -> float: """Return car battery range.""" - return self._controller.get_charging_params(vin=self.vin).get("battery_range") + return self._vehicle_data.get("charge_state").get("battery_range") @property def cabin_overheat_protection(self) -> str: """Return cabin overheat protection.""" - return self._controller.get_climate_params(vin=self.vin).get( - "cabin_overheat_protection" - ) + return self._vehicle_data.get("climate_state").get("cabin_overheat_protection") @property def car_type(self) -> str: """Return car type.""" - # This is actually listed in PRODUCT_LIST return f"Model {str(self.vin[3]).upper()}" @property def car_version(self) -> str: """Return installed car software version.""" - return self._controller.get_state_params(vin=self.vin).get("car_version") + return self._vehicle_data.get("vehicle_state").get("car_version") @property def charger_actual_current(self) -> int: """Return charger actual current.""" - return self._controller.get_charging_params(vin=self.vin).get( - "charger_actual_current" - ) + return self._vehicle_data.get("charge_state").get("charger_actual_current") @property def charge_current_request(self) -> int: """Return charge current request.""" - return self._controller.get_charging_params(vin=self.vin).get( - "charge_current_request" - ) + return self._vehicle_data.get("charge_state").get("charge_current_request") @property def charge_current_request_max(self) -> int: """Return charge current request max.""" - return self._controller.get_charging_params(vin=self.vin).get( - "charge_current_request_max" - ) + return self._vehicle_data.get("charge_state").get("charge_current_request_max") @property def charge_port_latch(self) -> str: """Return charger port latch state. - "Engaged" - Other states? + Returns + str: Engaged + Other states? """ - return self._controller.get_charging_params(vin=self.vin).get( - "charge_port_latch" - ) + return self._vehicle_data.get("charge_state").get("charge_port_latch") @property def charge_energy_added(self) -> float: """Return charge energy added.""" - return self._controller.get_charging_params(vin=self.vin).get( - "charge_energy_added" - ) + return self._vehicle_data.get("charge_state").get("charge_energy_added") @property def charge_limit_soc(self) -> int: """Return charge limit soc.""" - return self._controller.get_charging_params(vin=self.vin).get( - "charge_limit_soc" - ) + return self._vehicle_data.get("charge_state").get("charge_limit_soc") @property def charge_limit_soc_max(self) -> int: """Return charge limit soc max.""" - return self._controller.get_charging_params(vin=self.vin).get( - "charge_limit_soc_max" - ) + return self._vehicle_data.get("charge_state").get("charge_limit_soc_max") @property def charge_limit_soc_min(self) -> int: """Return charge limit soc min.""" - return self._controller.get_charging_params(vin=self.vin).get( - "charge_limit_soc_min" - ) + return self._vehicle_data.get("charge_state").get("charge_limit_soc_min") @property def charge_miles_added_ideal(self) -> float: """Return charge ideal miles added.""" - return self._controller.get_charging_params(vin=self.vin).get( - "charge_miles_added_ideal" - ) + return self._vehicle_data.get("charge_state").get("charge_miles_added_ideal") @property def charge_miles_added_rated(self) -> float: """Return charge rated miles added.""" - return self._controller.get_charging_params(vin=self.vin).get( - "charge_miles_added_rated" - ) + return self._vehicle_data.get("charge_state").get("charge_miles_added_rated") @property def charger_phases(self) -> int: """Return charger phase.""" - return self._controller.get_charging_params(vin=self.vin).get("charger_phases") + return self._vehicle_data.get("charge_state").get("charger_phases") @property def charger_power(self) -> int: """Return charger power.""" - return self._controller.get_charging_params(vin=self.vin).get("charger_power") + return self._vehicle_data.get("charge_state").get("charger_power") @property def charge_rate(self) -> str: """Return charge rate.""" - return self._controller.get_charging_params(vin=self.vin).get("charge_rate") + return self._vehicle_data.get("charge_state").get("charge_rate") @property def charging_state(self) -> str: - """Return charging state.""" - return self._controller.get_charging_params(vin=self.vin).get("charging_state") + """Return charging state. + + Returns + str: Charging, Stopped, Complete, others? + """ + return self._vehicle_data.get("charge_state").get("charging_state") @property def charger_voltage(self) -> int: """Return charger voltage.""" - return self._controller.get_charging_params(vin=self.vin).get("charger_voltage") + return self._vehicle_data.get("charge_state").get("charger_voltage") @property def climate_keeper_mode(self) -> str: @@ -195,16 +185,12 @@ def climate_keeper_mode(self) -> str: Not supported on all Tesla models. """ - return self._controller.get_climate_params(vin=self.vin).get( - "climate_keeper_mode", "" - ) + return self._vehicle_data.get("climate_state").get("climate_keeper_mode") @property def conn_charge_cable(self) -> str: """Return charge cable connection.""" - return self._controller.get_charging_params(vin=self.vin).get( - "conn_charge_cable" - ) + return self._vehicle_data.get("charge_state").get("conn_charge_cable") @property def defrost_mode(self) -> int: @@ -213,89 +199,73 @@ def defrost_mode(self) -> int: Returns int: 2 (on), 0 (off) """ - return self._controller.get_climate_params(vin=self.vin).get("defrost_mode", 0) + return self._vehicle_data.get("climate_state").get("defrost_mode", 0) @property def driver_temp_setting(self) -> float: """Return driver temperature setting.""" - return self._controller.get_climate_params(vin=self.vin).get( - "driver_temp_setting" - ) + return self._vehicle_data.get("climate_state").get("driver_temp_setting") @property def fast_charger_present(self) -> bool: """Return fast charger present.""" - return self._controller.get_charging_params(vin=self.vin).get( - "fast_charger_present" - ) + return self._vehicle_data.get("charge_state").get("fast_charger_present") @property def fast_charger_brand(self) -> str: """Return fast charger brand.""" - return self._controller.get_charging_params(vin=self.vin).get( - "fast_charger_brand" - ) + return self._vehicle_data.get("charge_state").get("fast_charger_brand") @property def fast_charger_type(self) -> str: """Return fast charger type.""" - return self._controller.get_charging_params(vin=self.vin).get( - "fast_charger_type" - ) + return self._vehicle_data.get("charge_state").get("fast_charger_type") @property def gui_distance_units(self) -> str: """Return gui distance units.""" # Why set default to mi/hr? - return self._controller.get_gui_params(vin=self.vin).get( - "gui_distance_units", "mi/hr" - ) + return self._vehicle_data.get("gui_settings").get("gui_distance_units") @property def gui_range_display(self) -> str: """Return range display.""" - return self._controller.get_gui_params(vin=self.vin).get("gui_range_display") + return self._vehicle_data.get("gui_settings").get("gui_range_display") @property def heading(self) -> int: """Return heading.""" - return self._controller.get_drive_params(vin=self.vin).get("heading") + return self._vehicle_data.get("drive_state").get("heading") @property def homelink_device_count(self) -> int: """Return Homelink device count.""" - return self._controller.get_state_params(vin=self.vin).get( - "homelink_device_count" - ) + return self._vehicle_data.get("vehicle_state").get("homelink_device_count") @property def homelink_nearby(self) -> bool: """Return Homelink nearby.""" - return self._controller.get_state_params(vin=self.vin).get("homelink_nearby") + return self._vehicle_data.get("vehicle_state").get("homelink_nearby") @property def ideal_battery_range(self) -> float: """Return car ideal battery range.""" - return self._controller.get_charging_params(vin=self.vin).get( - "ideal_battery_range" - ) + return self._vehicle_data.get("charge_state").get("ideal_battery_range") @property def inside_temp(self) -> float: """Return inside temperature.""" - return self._controller.get_climate_params(vin=self.vin).get("inside_temp") + return self._vehicle_data.get("climate_state").get("inside_temp") @property def is_charge_port_door_open(self) -> bool: """Return charger port door open.""" - return self._controller.get_charging_params(vin=self.vin).get( - "charge_port_door_open" - ) + return self._vehicle_data.get("charge_state").get("charge_port_door_open") @property def is_climate_on(self) -> bool: """Return climate is on.""" - return self._controller.get_climate_params(vin=self.vin).get("is_climate_on") + return self._vehicle_data.get("climate_state").get("is_climate_on") @property def is_frunk_locked(self) -> int: @@ -304,7 +274,7 @@ def is_frunk_locked(self) -> int: Returns int: 0 (locked), 255 (unlocked) """ - response = self._controller.get_state_params(vin=self.vin).get("ft") + response = self._vehicle_data.get("vehicle_state").get("ft") if response == 0: return True @@ -314,14 +284,12 @@ def is_frunk_locked(self) -> int: @property def is_locked(self) -> bool: """Return car is locked.""" - return self._controller.get_state_params(vin=self.vin).get("locked") + return self._vehicle_data.get("vehicle_state").get("locked") @property def is_steering_wheel_heater_on(self) -> bool: """Return steering wheel heater.""" - return self._controller.get_climate_params(vin=self.vin).get( - "steering_wheel_heater" - ) + return self._vehicle_data.get("climate_state").get("steering_wheel_heater") @property def is_trunk_locked(self) -> bool: @@ -330,7 +298,7 @@ def is_trunk_locked(self) -> bool: Returns bool: False (0), True (255) """ - response = self._controller.get_state_params(vin=self.vin).get("rt") + response = self._vehicle_data.get("vehicle_state").get("rt") if response == 0: return True @@ -345,55 +313,52 @@ def is_on(self) -> bool: @property def longitude(self) -> float: """Return longitude.""" - return self._controller.get_drive_params(vin=self.vin).get("longitude") + return self._vehicle_data.get("drive_state").get("longitude") @property def latitude(self) -> float: """Return latitude.""" - return self._controller.get_drive_params(vin=self.vin).get("latitude") + return self._vehicle_data.get("drive_state").get("latitude") @property def max_avail_temp(self) -> float: """Return max available temperature.""" - return self._controller.get_climate_params(vin=self.vin).get("max_avail_temp") + return self._vehicle_data.get("climate_state").get("max_avail_temp") @property def min_avail_temp(self) -> float: """Return min available temperature.""" - return self._controller.get_climate_params(vin=self.vin).get("min_avail_temp") + return self._vehicle_data.get("climate_state").get("min_avail_temp") @property def native_heading(self) -> int: """Return native heading.""" - # Not seeing this in the JSON response for 2015 Model S 85D on 28 Aug 2022 - return self._controller.get_drive_params(vin=self.vin).get("native_heading") + return self._vehicle_data.get("drive_state").get("native_heading") @property def native_location_supported(self) -> int: """Return native location supported.""" - return self._controller.get_drive_params(vin=self.vin).get( - "native_location_supported" - ) + return self._vehicle_data.get("drive_state").get("native_location_supported") @property def native_longitude(self) -> float: """Return native longitude.""" - return self._controller.get_drive_params(vin=self.vin).get("native_longitude") + return self._vehicle_data.get("drive_state").get("native_longitude") @property def native_latitude(self) -> float: """Return native latitude.""" - return self._controller.get_drive_params(vin=self.vin).get("native_latitude") + return self._vehicle_data.get("drive_state").get("native_latitude") @property def odometer(self) -> float: """Return odometer.""" - return self._controller.get_state_params(vin=self.vin).get("odometer") + return self._vehicle_data.get("vehicle_state").get("odometer") @property def outside_temp(self) -> float: """Return outside temperature.""" - return self._controller.get_climate_params(vin=self.vin).get("outside_temp") + return self._vehicle_data.get("climate_state").get("outside_temp") @property def rear_seat_heaters(self) -> int: @@ -402,56 +367,52 @@ def rear_seat_heaters(self) -> int: Returns int: 0 (no rear heated seats), int: ? (rear heated seats) """ - return self._controller.get_config_params(vin=self.vin).get("rear_seat_heaters") + return self._vehicle_data.get("vehicle_config").get("rear_seat_heaters") @property def sentry_mode(self) -> bool: """Return sentry mode.""" - return self._controller.get_state_params(vin=self.vin).get("sentry_mode") + return self._vehicle_data.get("vehicle_state").get("sentry_mode") @property def sentry_mode_available(self) -> bool: """Return sentry mode available.""" - return self._controller.get_state_params(vin=self.vin).get( - "sentry_mode_available" - ) + return self._vehicle_data.get("vehicle_state").get("sentry_mode_available") @property def shift_state(self) -> str: """Return shift state.""" - return self._controller.get_drive_params(vin=self.vin).get("shift_state") + return self._vehicle_data.get("drive_state").get("shift_state") @property def speed(self) -> float: """Return speed.""" - return self._controller.get_drive_params(vin=self.vin).get("speed") + return self._vehicle_data.get("drive_state").get("speed") @property def software_update(self) -> dict: """Return software update version information.""" - return self._controller.get_state_params(vin=self.vin).get( - "software_update", {} - ) + return self._vehicle_data.get("vehicle_state").get("software_update", {}) @property def steering_wheel_heater(self) -> bool: """Return steering wheel heater option.""" # Not seeing this in the JSON response for 2015 Model S 85D on 28 Aug 2022 - return self._controller.get_climate_params(vin=self.vin).get( - "steering_wheel_heater" - ) + return self._vehicle_data.get("climate_state").get("steering_wheel_heater") @property def third_row_seats(self) -> str: - """Return third row seats option.""" - return self._controller.get_state_params(vin=self.vin).get("third_row_seats") + """Return third row seats option. + + Returns + str: None + """ + return self._vehicle_data.get("vehicle_config").get("third_row_seats") @property def time_to_full_charge(self) -> float: """Return time to full charge.""" - return self._controller.get_charging_params(vin=self.vin).get( - "time_to_full_charge" - ) + return self._vehicle_data.get("charge_state").get("time_to_full_charge") async def _send_command( self, name: str, *, path_vars: dict, wake_if_asleep: bool = False, **kwargs @@ -489,7 +450,7 @@ async def change_charge_limit(self, value: float) -> None: if data and data["response"]["result"] is True: params = {"charge_limit_soc": int(value)} - self._controller.update_charging_params(vin=self.vin, params=params) + self._vehicle_data["charge_state"].update(params) async def charge_port_door_close(self) -> None: """Send command to close charge port door.""" @@ -501,7 +462,7 @@ async def charge_port_door_close(self) -> None: if data and data["response"]["result"] is True: params = {"charge_port_door_open": False} - self._controller.update_state_params(vin=self.vin, params=params) + self._vehicle_data["charge_state"].update(params) async def charge_port_door_open(self) -> None: """Send command to open charge port door.""" @@ -513,7 +474,7 @@ async def charge_port_door_open(self) -> None: if data and data["response"]["result"] is True: params = {"charge_port_door_open": True} - self._controller.update_state_params(vin=self.vin, params=params) + self._vehicle_data["charge_state"].update(params) async def flash_lights(self) -> None: """Send command to flash lights.""" @@ -542,7 +503,7 @@ async def lock(self) -> None: ) if data and data["response"]["result"] is True: params = {"locked": True} - self._controller.update_state_params(vin=self.vin, params=params) + self._vehicle_data["vehicle_state"].update(params) async def remote_seat_heater_request(self, level: int, seat_id: int) -> None: """Send command to change seat heat. @@ -561,13 +522,14 @@ async def remote_seat_heater_request(self, level: int, seat_id: int) -> None: wake_if_asleep=True, ) if data and data["response"]["result"] is True: - params = {f"seat_heater_{SEAT_NAME_MAP[seat_id]}": level} - self._controller.update_climate_params(vin=self.vin, params=params) + params = {f"seat_heater_{SEAT_ID_MAP[seat_id]}": level} + self._vehicle_data["climate_state"].update(params) def get_seat_heater_status(self, seat_id: int) -> int: """Return status of seat heater for a given seat.""" - seat_id = f"seat_heater_{SEAT_NAME_MAP[seat_id]}" - return self._controller.get_climate_params(vin=self.vin).get(seat_id) + seat_id = f"seat_heater_{SEAT_ID_MAP[seat_id]}" + + return self._vehicle_data.get("climate_state").get(seat_id) async def schedule_software_update(self, offset_sec: Optional[int] = 0) -> None: """Send command to install software update.""" @@ -597,7 +559,7 @@ async def set_charging_amps(self, value: float) -> None: if data and data["response"]["result"] is True: params = {"charge_amps": int(value)} - self._controller.update_charging_params(vin=self.vin, params=params) + self._vehicle_data["charge_state"].update(params) async def set_cabin_overheat_protection(self, option: str) -> None: """Send command to set cabin overheat protection. @@ -623,9 +585,9 @@ async def set_cabin_overheat_protection(self, option: str) -> None: fan_only=fan_only, wake_if_asleep=True, ) - if data and data["response"]["result"]: + if data and data["response"]["result"] is True: params = {"cabin_overheat_protection": option} - self._controller.update_climate_params(vin=self.vin, params=params) + self._vehicle_data["climate_state"].update(params) async def set_climate_keeper_mode(self, keeper_id: int) -> None: """Send command to set climate keeper mode. @@ -633,12 +595,15 @@ async def set_climate_keeper_mode(self, keeper_id: int) -> None: Args keeper_id: 1 (keep on), 2 (dog mode), 3 (camp mode) """ - await self._send_command( + data = await self._send_command( "SET_CLIMATE_KEEPER_MODE", path_vars={"vehicle_id": self.id}, climate_keeper_mode=keeper_id, wake_if_asleep=True, ) + if data and data["response"]["result"] is True: + params = {"climate_keeper_mode": CLIMATE_KEEPER_ID_MAP[keeper_id]} + self._vehicle_data["climate_state"].update(params) async def set_heated_steering_wheel(self, value: bool) -> None: """Send command to set heated steering wheel.""" @@ -651,7 +616,7 @@ async def set_heated_steering_wheel(self, value: bool) -> None: if data and data["response"]["result"] is True: params = {"steering_wheel_heater": value} - self._controller.update_climate_params(vin=self.vin, params=params) + self._vehicle_data["climate_state"].update(params) async def set_hvac_mode(self, value: str) -> None: """Send command to set HVAC mode. @@ -668,7 +633,7 @@ async def set_hvac_mode(self, value: str) -> None: ) if data and data["response"]["result"] is True: params = {"is_climate_on": False} - self._controller.update_climate_params(vin=self.vin, params=params) + self._vehicle_data["climate_state"].update(params) elif value == "on": data = await self._send_command( @@ -678,7 +643,7 @@ async def set_hvac_mode(self, value: str) -> None: ) if data and data["response"]["result"] is True: params = {"is_climate_on": True} - self._controller.update_climate_params(vin=self.vin, params=params) + self._vehicle_data["climate_state"].update(params) async def set_max_defrost(self, state: int) -> None: """Send command to set max defrost. @@ -686,12 +651,15 @@ async def set_max_defrost(self, state: int) -> None: Args state: 2 = on, 0 = off """ - await self._send_command( + data = await self._send_command( "MAX_DEFROST", path_vars={"vehicle_id": self.id}, on=state, wake_if_asleep=True, ) + if data and data["response"]["result"] is True: + params = {"defrost_mode": state} + self._vehicle_data["climate_state"].update(params) async def set_sentry_mode(self, value: bool) -> None: """Send command to set sentry mode.""" @@ -704,7 +672,7 @@ async def set_sentry_mode(self, value: bool) -> None: if data and data["response"]["result"] is True: params = {"sentry_mode": value} - self._controller.update_state_params(vin=self.vin, params=params) + self._vehicle_data["vehicle_state"].update(params) async def set_temperature(self, temp: float) -> None: """Send command to set temperature.""" @@ -717,8 +685,7 @@ async def set_temperature(self, temp: float) -> None: ) if data and data["response"]["result"] is True: params = {"driver_temp_setting": temp} - - self._controller.update_climate_params(vin=self.vin, params=params) + self._vehicle_data["climate_state"].update(params) async def start_charge(self) -> None: """Send command to start charge.""" @@ -730,7 +697,7 @@ async def start_charge(self) -> None: if data and data["response"]["result"] is True: params = {"charging_state": "Charging"} - self._controller.update_charging_params(vin=self.vin, params=params) + self._vehicle_data["charge_state"].update(params) async def stop_charge(self) -> None: """Send command to start charge.""" @@ -741,8 +708,8 @@ async def stop_charge(self) -> None: ) if data and data["response"]["result"] is True: - params = {"charging_state": None} - self._controller.update_charging_params(vin=self.vin, params=params) + params = {"charging_state": "Stopped"} + self._vehicle_data["charge_state"].update(params) async def wake_up(self) -> None: """Send command to wake up.""" @@ -763,10 +730,10 @@ async def toggle_trunk(self) -> None: if data and data["response"]["result"] is True: if self.is_trunk_locked: params = {"rt": 0} - self._controller.update_state_params(vin=self.vin, params=params) + self._vehicle_data["vehicle_state"].update(params) if not self.is_trunk_locked: params = {"rt": 255} - self._controller.update_state_params(vin=self.vin, params=params) + self._vehicle_data["vehicle_state"].update(params) async def toggle_frunk(self) -> None: """Actuate front trunk lock.""" @@ -779,10 +746,10 @@ async def toggle_frunk(self) -> None: if data and data["response"]["result"] is True: if self.is_frunk_locked: params = {"ft": 0} - self._controller.update_state_params(vin=self.vin, params=params) + self._vehicle_data["vehicle_state"].update(params) if not self.is_frunk_locked: params = {"ft": 255} - self._controller.update_state_params(vin=self.vin, params=params) + self._vehicle_data["vehicle_state"].update(params) async def trigger_homelink(self) -> None: """Send command to trigger homelink.""" @@ -818,4 +785,4 @@ async def unlock(self) -> None: ) if data and data["response"]["result"] is True: params = {"locked": False} - self._controller.update_state_params(vin=self.vin, params=params) + self._vehicle_data["vehicle_state"].update(params) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index e5c9bef8..422c1caf 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -344,6 +344,7 @@ def __init__( self._include_energysites: bool = True self._product_list: List[dict] = [] self._vehicle_list: List[dict] = [] + self._vehicle_data: List[dict] = {} self._energysite_list: List[dict] = [] self._site_config: Dict[int:dict] = {} self._site_data: Dict[int:dict] = {} @@ -419,6 +420,8 @@ async def connect( self.__driving[vin] = {} self.__gui[vin] = {} + self._vehicle_data[vin] = {} + if self._include_energysites: self._energysite_list = [ p @@ -545,7 +548,7 @@ def generate_car_objects(self) -> Dict[str, TeslaCar]: """Generate car objects.""" for car in self._vehicle_list: vin = car["vin"] - self.cars[vin] = TeslaCar(car, self) + self.cars[vin] = TeslaCar(car, self, self._vehicle_data[vin]) return self.cars @@ -775,6 +778,8 @@ async def _get_and_process_car_data(vin: Text) -> None: ) ) + self._vehicle_data[vin].update(response) + async def _get_and_process_site_data(energysite_id: int) -> None: _LOGGER.debug("Updating SITE_DATA for energysite: %s", energysite_id) try: diff --git a/tests/tesla_mock.py b/tests/tesla_mock.py index 4370a3a1..c9a2376c 100644 --- a/tests/tesla_mock.py +++ b/tests/tesla_mock.py @@ -46,6 +46,7 @@ def __init__(self, monkeypatch) -> None: self._energysites = copy.deepcopy(ENERGYSITES) self._product_list = copy.deepcopy(PRODUCT_LIST) self._vehicle_data = copy.deepcopy(VEHICLE_DATA) + self._vehicle_data_by_vin = copy.deepcopy(VEHICLE_DATA_BY_VIN) self._site_data = copy.deepcopy(SITE_DATA) self._battery_data = copy.deepcopy(BATTERY_DATA) self._battery_summary = copy.deepcopy(BATTERY_SUMMARY) @@ -183,6 +184,10 @@ def data_request_vehicle(self): """Simulate the result of vehicle data request.""" return self._vehicle_data + def data_request_vehicle_by_vin(self): + """Simulate the result of vehicle data request by VIN.""" + return self._vehicle_data_by_vin + def data_request_charge_state(self): """Simulate the result of charge state data request.""" return self._charge_state @@ -200,7 +205,7 @@ def data_request_energysites(self): return self._energysites def data_request_site_config(self): - """Get site_data.""" + """Get site_config.""" return self._site_config def data_request_site_data(self): @@ -533,6 +538,232 @@ def command_ok(): }, } +VEHICLE_DATA_BY_VIN = { + "5YJSA11111111111": { + "id": 12345678901234567, + "user_id": 123456, + "vehicle_id": 1234567890, + "vin": "5YJSA11111111111", + "display_name": "My Model S", + "option_codes": "AD15,MDL3,PBSB,RENA,BT37,ID3W,RF3G,S3PB,DRLH,DV2W,W39B,APF0,COUS,BC3B,CH07,PC30,FC3P,FG31,GLFR,HL31,HM31,IL31,LTPB,MR31,FM3B,RS3H,SA3P,STCP,SC04,SU3C,T3CA,TW00,TM00,UT3P,WR00,AU3P,APH3,AF00,ZCST,MI00,CDM0", + "color": None, + "access_type": "OWNER", + "tokens": ["redacted", "redacted"], + "state": "online", + "in_service": False, + "id_s": "12345678901234567", + "calendar_enabled": True, + "api_version": 36, + "backseat_token": None, + "backseat_token_updated_at": None, + "charge_state": { + "battery_heater_on": False, + "battery_level": 78, + "battery_range": 169.08, + "charge_amps": 32, + "charge_current_request": 32, + "charge_current_request_max": 32, + "charge_enable_request": True, + "charge_energy_added": 13.57, + "charge_limit_soc": 80, + "charge_limit_soc_max": 100, + "charge_limit_soc_min": 50, + "charge_limit_soc_std": 90, + "charge_miles_added_ideal": 59.0, + "charge_miles_added_rated": 47.0, + "charge_port_cold_weather_mode": None, + "charge_port_color": "FlashingGreen", + "charge_port_door_open": True, + "charge_port_latch": "Engaged", + "charge_rate": 23.2, + "charge_to_max_range": False, + "charger_actual_current": 32, + "charger_phases": 1, + "charger_pilot_current": 32, + "charger_power": 7, + "charger_voltage": 242, + "charging_state": "Charging", + "conn_charge_cable": "SAE", + "est_battery_range": 150.09, + "fast_charger_brand": "", + "fast_charger_present": False, + "fast_charger_type": "MCSingleWireCAN", + "ideal_battery_range": 213.19, + "managed_charging_active": False, + "managed_charging_start_time": None, + "managed_charging_user_canceled": False, + "max_range_charge_counter": 0, + "minutes_to_full_charge": 15, + "not_enough_power_to_heat": False, + "off_peak_charging_enabled": True, + "off_peak_charging_times": "weekdays", + "off_peak_hours_end_time": 360, + "preconditioning_enabled": False, + "preconditioning_times": "all_week", + "scheduled_charging_mode": "DepartBy", + "scheduled_charging_pending": False, + "scheduled_charging_start_time": None, + "scheduled_charging_start_time_app": 0, + "scheduled_departure_time": 1661515200, + "scheduled_departure_time_minutes": 300, + "supercharger_session_trip_planner": False, + "time_to_full_charge": 0.25, + "timestamp": 1661641175268, + "trip_charging": False, + "usable_battery_level": 78, + "user_charge_enable_request": None, + }, + "climate_state": { + "allow_cabin_overheat_protection": True, + "battery_heater": False, + "battery_heater_no_power": False, + "cabin_overheat_protection": "Off", + "climate_keeper_mode": "off", + "defrost_mode": 0, + "driver_temp_setting": 23.3, + "fan_status": 0, + "hvac_auto_request": "On", + "inside_temp": 35.5, + "is_auto_conditioning_on": False, + "is_climate_on": False, + "is_front_defroster_on": False, + "is_preconditioning": False, + "is_rear_defroster_on": False, + "left_temp_direction": -309, + "max_avail_temp": 28.0, + "min_avail_temp": 15.0, + "outside_temp": 32.5, + "passenger_temp_setting": 23.3, + "remote_heater_control_enabled": False, + "right_temp_direction": -309, + "seat_heater_left": 0, + "seat_heater_right": 0, + "side_mirror_heaters": False, + "supports_fan_only_cabin_overheat_protection": False, + "timestamp": 1661641175268, + "wiper_blade_heater": False, + }, + "drive_state": { + "gps_as_of": 1661641173, + "heading": 182, + "latitude": 33.111111, + "longitude": -88.111111, + "native_latitude": 33.111111, + "native_location_supported": 1, + "native_longitude": -88.111111, + "native_type": "wgs", + "power": -7, + "shift_state": None, + "speed": None, + "timestamp": 1661641175268, + }, + "gui_settings": { + "gui_24_hour_time": False, + "gui_charge_rate_units": "mi/hr", + "gui_distance_units": "mi/hr", + "gui_range_display": "Rated", + "gui_temperature_units": "F", + "show_range_units": True, + "timestamp": 1661641175268, + }, + "vehicle_config": { + "can_accept_navigation_requests": True, + "can_actuate_trunks": True, + "car_special_type": "base", + "car_type": "models", + "charge_port_type": "US", + "dashcam_clip_save_supported": False, + "default_charge_to_max": False, + "driver_assist": "MonoCam", + "ece_restrictions": False, + "efficiency_package": "Default", + "eu_vehicle": False, + "exterior_color": "White", + "front_drive_unit": "NoneOrSmall", + "has_air_suspension": False, + "has_ludicrous_mode": False, + "has_seat_cooling": False, + "headlamp_type": "Hid", + "interior_trim_type": "AllBlack", + "motorized_charge_port": True, + "plg": True, + "pws": False, + "rear_drive_unit": "Small", + "rear_seat_heaters": 0, + "rear_seat_type": 1, + "rhd": False, + "roof_color": "Colored", + "seat_type": 1, + "spoiler_type": "None", + "sun_roof_installed": 0, + "third_row_seats": "None", + "timestamp": 1661641175269, + "trim_badging": "85d", + "use_range_badging": False, + "utc_offset": -25200, + "wheel_type": "Base19", + }, + "vehicle_state": { + "api_version": 36, + "autopark_state_v2": "standby", + "autopark_style": "standard", + "calendar_supported": True, + "car_version": "2022.8.10.1 171f0fe61c20", + "center_display_state": 0, + "dashcam_clip_save_available": False, + "dashcam_state": "", + "df": 0, + "dr": 0, + "fd_window": 0, + "feature_bitmask": "5,0", + "fp_window": 0, + "ft": 0, + "homelink_device_count": 2, + "homelink_nearby": True, + "is_user_present": False, + "last_autopark_error": "no_error", + "locked": False, + "media_state": {"remote_control_enabled": True}, + "notifications_supported": True, + "odometer": 70915.596752, + "parsed_calendar_supported": True, + "pf": 0, + "pr": 0, + "rd_window": 0, + "remote_start": False, + "remote_start_enabled": True, + "remote_start_supported": True, + "rp_window": 0, + "rt": 0, + "santa_mode": 0, + "smart_summon_available": False, + "software_update": { + "download_perc": 0, + "expected_duration_sec": 2700, + "install_perc": 1, + "status": "", + "version": " ", + }, + "speed_limit_mode": { + "active": False, + "current_limit_mph": 85.0, + "max_limit_mph": 90, + "min_limit_mph": 50.0, + "pin_code_set": False, + }, + "summon_standby_mode_enabled": False, + "timestamp": 1661641175268, + "tpms_pressure_fl": None, + "tpms_pressure_fr": None, + "tpms_pressure_rl": None, + "tpms_pressure_rr": None, + "valet_mode": False, + "valet_pin_needed": True, + "vehicle_name": "My Model S", + }, + } +} + ENERGYSITES = PRODUCT_LIST[1:3] # Tesla solar with Tesla inverter (no Powerwalls) diff --git a/tests/unit_tests/test_car.py b/tests/unit_tests/test_car.py index 0fac07d9..7323eb54 100644 --- a/tests/unit_tests/test_car.py +++ b/tests/unit_tests/test_car.py @@ -14,9 +14,10 @@ @pytest.mark.asyncio async def test_car_properties(monkeypatch): """Test TeslaCar class properties.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -202,7 +203,9 @@ async def test_car_properties(monkeypatch): "steering_wheel_heater" ) - assert _car.third_row_seats == VEHICLE_DATA["vehicle_state"].get("third_row_seats") + assert _car.third_row_seats == str( + VEHICLE_DATA["vehicle_state"].get("third_row_seats") + ) assert ( _car.time_to_full_charge == VEHICLE_DATA["charge_state"]["time_to_full_charge"] @@ -212,9 +215,10 @@ async def test_car_properties(monkeypatch): @pytest.mark.asyncio async def test_change_charge_limit(monkeypatch): """Test change charge limit.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -224,9 +228,10 @@ async def test_change_charge_limit(monkeypatch): @pytest.mark.asyncio async def test_charge_port_door_open_close(monkeypatch): """Test charge port door open/close command.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -238,9 +243,10 @@ async def test_charge_port_door_open_close(monkeypatch): @pytest.mark.asyncio async def test_flash_lights(monkeypatch): """Test flash lights command.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -250,9 +256,10 @@ async def test_flash_lights(monkeypatch): @pytest.mark.asyncio async def test_honk_horn(monkeypatch): """Test honk horn command.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -262,9 +269,10 @@ async def test_honk_horn(monkeypatch): @pytest.mark.asyncio async def test_lock(monkeypatch): """Test lock command.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -274,9 +282,10 @@ async def test_lock(monkeypatch): @pytest.mark.asyncio async def test_remote_seat_heater_request(monkeypatch): """Test remote seat heater request.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -286,9 +295,10 @@ async def test_remote_seat_heater_request(monkeypatch): @pytest.mark.asyncio async def test_schedule_software_update(monkeypatch): """Test scheduling software update.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -298,9 +308,10 @@ async def test_schedule_software_update(monkeypatch): @pytest.mark.asyncio async def test_set_charging_amps(monkeypatch): """Test setting charging amps.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -310,9 +321,10 @@ async def test_set_charging_amps(monkeypatch): @pytest.mark.asyncio async def test_set_cabin_overheat_protection(monkeypatch): """Test setting heated steering wheel.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -322,9 +334,10 @@ async def test_set_cabin_overheat_protection(monkeypatch): @pytest.mark.asyncio async def test_set_climate_keeper_mode(monkeypatch): """Test setting climate keeper mode.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -334,9 +347,10 @@ async def test_set_climate_keeper_mode(monkeypatch): @pytest.mark.asyncio async def test_set_heated_steering_wheel(monkeypatch): """Test setting heated steering wheel.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -346,9 +360,10 @@ async def test_set_heated_steering_wheel(monkeypatch): @pytest.mark.asyncio async def test_set_hvac_mode(monkeypatch): """Test setting HVAC mode.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -358,9 +373,10 @@ async def test_set_hvac_mode(monkeypatch): @pytest.mark.asyncio async def test_set_max_defrost(monkeypatch): """Test wake up.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -370,9 +386,10 @@ async def test_set_max_defrost(monkeypatch): @pytest.mark.asyncio async def test_set_sentry_mode(monkeypatch): """Test wake up.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -382,9 +399,10 @@ async def test_set_sentry_mode(monkeypatch): @pytest.mark.asyncio async def test_set_temperature(monkeypatch): """Test wake up.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -394,9 +412,10 @@ async def test_set_temperature(monkeypatch): @pytest.mark.asyncio async def test_start_stop_charge(monkeypatch): """Test wake up.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -408,9 +427,10 @@ async def test_start_stop_charge(monkeypatch): @pytest.mark.asyncio async def test_wake_up(monkeypatch): """Test wake up.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -420,9 +440,10 @@ async def test_wake_up(monkeypatch): @pytest.mark.asyncio async def test_toggle_trunk(monkeypatch): """Test toggle trunk.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -432,9 +453,10 @@ async def test_toggle_trunk(monkeypatch): @pytest.mark.asyncio async def test_toggle_frunk(monkeypatch): """Test toggle frunk.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -444,9 +466,10 @@ async def test_toggle_frunk(monkeypatch): @pytest.mark.asyncio async def test_trigger_homelink(monkeypatch): """Test unlock.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -456,9 +479,10 @@ async def test_trigger_homelink(monkeypatch): @pytest.mark.asyncio async def test_unlock(monkeypatch): """Test unlock.""" - TeslaMock(monkeypatch) + _mock = TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() + _controller._vehicle_data = _mock.data_request_vehicle_by_vin() _controller.generate_car_objects() _car = _controller.cars[VIN] From 84db5b7d4a10a237ee8ea1b43888ba928185a324 Mon Sep 17 00:00:00 2001 From: shred86 Date: Wed, 7 Sep 2022 21:55:06 -0700 Subject: [PATCH 61/84] Update some controller car data --- teslajsonpy/car.py | 10 ++++++++++ teslajsonpy/controller.py | 1 - 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 773644f4..7a5e4f9a 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -634,6 +634,8 @@ async def set_hvac_mode(self, value: str) -> None: if data and data["response"]["result"] is True: params = {"is_climate_on": False} self._vehicle_data["climate_state"].update(params) + # Need to update controller car data for polling functions + self._controller.update_climate_params(vin=self.vin, params=params) elif value == "on": data = await self._send_command( @@ -644,6 +646,8 @@ async def set_hvac_mode(self, value: str) -> None: if data and data["response"]["result"] is True: params = {"is_climate_on": True} self._vehicle_data["climate_state"].update(params) + # Need to update controller car data for polling functions + self._controller.update_climate_params(vin=self.vin, params=params) async def set_max_defrost(self, state: int) -> None: """Send command to set max defrost. @@ -673,6 +677,8 @@ async def set_sentry_mode(self, value: bool) -> None: if data and data["response"]["result"] is True: params = {"sentry_mode": value} self._vehicle_data["vehicle_state"].update(params) + # Need to update controller car data for polling functions + self._controller.update_state_params(vin=self.vin, params=params) async def set_temperature(self, temp: float) -> None: """Send command to set temperature.""" @@ -698,6 +704,8 @@ async def start_charge(self) -> None: if data and data["response"]["result"] is True: params = {"charging_state": "Charging"} self._vehicle_data["charge_state"].update(params) + # Need to update controller car data for polling functions + self._controller.update_charging_params(vin=self.vin, params=params) async def stop_charge(self) -> None: """Send command to start charge.""" @@ -710,6 +718,8 @@ async def stop_charge(self) -> None: if data and data["response"]["result"] is True: params = {"charging_state": "Stopped"} self._vehicle_data["charge_state"].update(params) + # Need to update controller car data for polling functions + self._controller.update_charging_params(vin=self.vin, params=params) async def wake_up(self) -> None: """Send command to wake up.""" diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 422c1caf..49559959 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -974,7 +974,6 @@ def update_climate_params( self, car_id: Text = None, vin: Text = None, params: Dict = None ) -> None: """Set climate_params for car_id.""" - # Used to update params in self.__climate for TeslaCar.set_temperature params = params or {} if car_id and not vin: vin = self._id_to_vin(car_id) From 0a98cf07eff4e5afaee94f4251b75dbab8913623 Mon Sep 17 00:00:00 2001 From: shred86 Date: Thu, 8 Sep 2022 21:52:15 -0700 Subject: [PATCH 62/84] Use car object properties --- teslajsonpy/car.py | 37 +- teslajsonpy/controller.py | 517 +++++----------------- tests/tesla_mock.py | 36 +- tests/unit_tests/test_polling_interval.py | 12 +- 4 files changed, 153 insertions(+), 449 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 7a5e4f9a..06f932dc 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -65,7 +65,7 @@ def vin(self) -> str: @property def data_available(self) -> bool: """Return if data is available.""" - return self._controller.get_state_params(vin=self.vin) + return self._vehicle_data @property def battery_level(self) -> float: @@ -252,6 +252,11 @@ def ideal_battery_range(self) -> float: """Return car ideal battery range.""" return self._vehicle_data.get("charge_state").get("ideal_battery_range") + @property + def in_service(self) -> bool: + """Return car in_service.""" + return self._vehicle_data.get("in_service") + @property def inside_temp(self) -> float: """Return inside temperature.""" @@ -281,6 +286,11 @@ def is_frunk_locked(self) -> int: if response == 255: return False + @property + def is_in_gear(self) -> bool: + """Return car is gear (i.e. drive or reverse).""" + return self.shift_state in ["D", "R"] + @property def is_locked(self) -> bool: """Return car is locked.""" @@ -350,6 +360,11 @@ def native_latitude(self) -> float: """Return native latitude.""" return self._vehicle_data.get("drive_state").get("native_latitude") + @property + def native_type(self) -> float: + """Return native type.""" + return self._vehicle_data.get("drive_state").get("native_type") + @property def odometer(self) -> float: """Return odometer.""" @@ -360,6 +375,11 @@ def outside_temp(self) -> float: """Return outside temperature.""" return self._vehicle_data.get("climate_state").get("outside_temp") + @property + def power(self) -> int: + """Return power.""" + return self._vehicle_data.get("drive_state").get("power") + @property def rear_seat_heaters(self) -> int: """Return if car has rear (second row) heated seats. @@ -397,7 +417,6 @@ def software_update(self) -> dict: @property def steering_wheel_heater(self) -> bool: """Return steering wheel heater option.""" - # Not seeing this in the JSON response for 2015 Model S 85D on 28 Aug 2022 return self._vehicle_data.get("climate_state").get("steering_wheel_heater") @property @@ -634,8 +653,6 @@ async def set_hvac_mode(self, value: str) -> None: if data and data["response"]["result"] is True: params = {"is_climate_on": False} self._vehicle_data["climate_state"].update(params) - # Need to update controller car data for polling functions - self._controller.update_climate_params(vin=self.vin, params=params) elif value == "on": data = await self._send_command( @@ -646,8 +663,6 @@ async def set_hvac_mode(self, value: str) -> None: if data and data["response"]["result"] is True: params = {"is_climate_on": True} self._vehicle_data["climate_state"].update(params) - # Need to update controller car data for polling functions - self._controller.update_climate_params(vin=self.vin, params=params) async def set_max_defrost(self, state: int) -> None: """Send command to set max defrost. @@ -677,8 +692,6 @@ async def set_sentry_mode(self, value: bool) -> None: if data and data["response"]["result"] is True: params = {"sentry_mode": value} self._vehicle_data["vehicle_state"].update(params) - # Need to update controller car data for polling functions - self._controller.update_state_params(vin=self.vin, params=params) async def set_temperature(self, temp: float) -> None: """Send command to set temperature.""" @@ -704,8 +717,6 @@ async def start_charge(self) -> None: if data and data["response"]["result"] is True: params = {"charging_state": "Charging"} self._vehicle_data["charge_state"].update(params) - # Need to update controller car data for polling functions - self._controller.update_charging_params(vin=self.vin, params=params) async def stop_charge(self) -> None: """Send command to start charge.""" @@ -718,8 +729,6 @@ async def stop_charge(self) -> None: if data and data["response"]["result"] is True: params = {"charging_state": "Stopped"} self._vehicle_data["charge_state"].update(params) - # Need to update controller car data for polling functions - self._controller.update_charging_params(vin=self.vin, params=params) async def wake_up(self) -> None: """Send command to wake up.""" @@ -786,6 +795,10 @@ async def trigger_homelink(self) -> None: if result is False: raise HomelinkError(f"Error calling trigger_homelink: {reason}") + async def update_car_state(self, state: dict) -> None: + """Update the car state.""" + self._vehicle_data.update(state) + async def unlock(self) -> None: """Send unlock command.""" data = await self._send_command( diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 49559959..99046ec1 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -314,12 +314,7 @@ def __init__( self._update_interval: int = update_interval self._update_interval_vin = {} self.__update = {} - self.__climate = {} - self.__charging = {} - self.__state = {} - self.__config = {} - self.__driving = {} - self.__gui = {} + self.__driving = {} # for websocket timestamp only self._last_update_time = {} # succesful update attempts by car self._last_wake_up_attempt = {} # attempts to wake_up car self._last_wake_up_time = {} # succesful wake_ups by car @@ -328,7 +323,6 @@ def __init__( self.__update_lock = None # controls access to update function self.__wakeup_conds = {} self.car_online = {} - self.car_state = {} self.__id_vin_map = {} self.__vin_id_map = {} self.__vin_vehicle_id_map = {} @@ -344,7 +338,7 @@ def __init__( self._include_energysites: bool = True self._product_list: List[dict] = [] self._vehicle_list: List[dict] = [] - self._vehicle_data: List[dict] = {} + self._vehicle_data: Dict[str:dict] = {} self._energysite_list: List[dict] = [] self._site_config: Dict[int:dict] = {} self._site_data: Dict[int:dict] = {} @@ -388,7 +382,7 @@ async def connect( self._product_list = await self.get_product_list() - if self._include_vehicles: + if self._include_vehicles or not test_login: self._vehicle_list = [ cars for cars in self._product_list if "vehicle_id" in cars ] @@ -408,21 +402,15 @@ async def connect( self._last_wake_up_time[vin] = 0 self.__update[vin] = True self.__update_state[vin] = "normal" - self.car_state[vin] = car self.set_car_online(vin=vin, online_status=car["state"] == "online") self.set_last_park_time( vin=vin, timestamp=self._last_attempted_update_time ) - self.__climate[vin] = {} - self.__charging[vin] = {} - self.__state[vin] = {} - self.__config[vin] = {} self.__driving[vin] = {} - self.__gui[vin] = {} - self._vehicle_data[vin] = {} + self._vehicle_data[vin] = await self.get_vehicle_data(vin) - if self._include_energysites: + if self._include_energysites or not test_login: self._energysite_list = [ p for p in self._product_list @@ -436,20 +424,23 @@ async def connect( self._site_config[energysite_id] = await self.get_site_config( energysite_id ) - # These will get updated when self.update is called below - self._site_data[energysite_id] = {} - self._battery_data[energysite_id] = {} - self._battery_summary[energysite_id] = {} + + if energysite.get(RESOURCE_TYPE) == RESOURCE_TYPE_SOLAR: + self._site_data[energysite_id] = await self.get_site_data( + energysite_id + ) + if energysite.get(RESOURCE_TYPE) == RESOURCE_TYPE_BATTERY: + battery_id = energysite.get("id") + self._battery_data[energysite_id] = await self.get_battery_data( + battery_id + ) + self._battery_summary[ + energysite_id + ] = await self.get_battery_summary(battery_id) # For dealing with sites that always report "Unknown" # Default to True and check during updates self._grid_status_unknown = {energysite_id: True} - if not test_login: - try: - await self.update(wake_if_asleep=wake_if_asleep) - except (TeslaException, RetryLimitError): - pass - return { "refresh_token": self.__connection.refresh_token, "access_token": self.__connection.access_token, @@ -544,6 +535,36 @@ async def get_site_config(self, energysite_id: int) -> dict: "response" ] + @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) + async def get_vehicle_data(self, vin: str) -> dict: + """Get vehicle data json from TeslaAPI for a given vin.""" + return ( + await self.api( + "VEHICLE_DATA", path_vars={"vehicle_id": self.__vin_id_map[vin]} + ) + )["response"] + + @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) + async def get_site_data(self, energysite_id: int) -> dict: + """Get site data json from TeslaAPI for a given energysite_id.""" + return (await self.api("SITE_DATA", path_vars={"site_id": energysite_id}))[ + "response" + ] + + @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) + async def get_battery_data(self, battery_id: str) -> dict: + """Get battery data json from TeslaAPI for a given battery_id.""" + return (await self.api("BATTERY_DATA", path_vars={"battery_id": battery_id}))[ + "response" + ] + + @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) + async def get_battery_summary(self, battery_id: str) -> dict: + """Get site config json from TeslaAPI for a given battery_id.""" + return ( + await self.api("BATTERY_SUMMARY", path_vars={"battery_id": battery_id}) + )["response"] + def generate_car_objects(self) -> Dict[str, TeslaCar]: """Generate car objects.""" for car in self._vehicle_list: @@ -605,11 +626,9 @@ async def _wake_up(self, car_id): self.set_car_online( car_id=car_id, online_status=result["response"]["state"] == "online" ) - self.car_state[car_vin] = result["response"] + await self.cars[car_vin].update_car_state(result["response"]) self._last_wake_up_attempt[car_vin] = cur_time - _LOGGER.debug( - "%s: Wakeup: %s", car_vin[-5:], self.car_state[car_vin]["state"] - ) + _LOGGER.debug("%s: Wakeup: %s", car_vin[-5:], self.cars[car_vin].state) return self.is_car_online(vin=car_vin) def _calculate_next_interval(self, vin: Text) -> int: @@ -617,24 +636,24 @@ def _calculate_next_interval(self, vin: Text) -> int: _LOGGER.debug( "%s: %s. Polling policy: %s. Update state: %s. Since last park: %s. Since last wake_up: %s. Idle interval: %s. shift_state: %s sentry: %s climate: %s, charging: %s ", vin[-5:], - self.car_state[vin].get("state"), + self.cars[vin].state, self.polling_policy, self.__update_state.get(vin), cur_time - self.get_last_park_time(vin=vin), cur_time - self.get_last_wake_up_time(vin=vin), IDLE_INTERVAL, - self.shift_state(vin=vin), - self.is_sentry_mode_on(vin=vin), - self.is_climate_on(vin=vin), - self.charging_state(vin=vin), + self.cars[vin].shift_state, + self.cars[vin].sentry_mode, + self.cars[vin].is_climate_on, + self.cars[vin].charging_state, ) if vin not in self.__update_state: self.__update_state[vin] = "normal" - if self.car_state[vin].get("state") == "asleep" or self.shift_state(vin=vin): + if self.cars[vin].state == "asleep" or self.cars[vin].shift_state: self.set_last_park_time( - vin=vin, timestamp=cur_time, shift_state=self.shift_state(vin=vin) + vin=vin, timestamp=cur_time, shift_state=self.cars[vin].shift_state ) - if self.is_in_gear(vin=vin): + if self.cars[vin].is_in_gear: driving_interval = min( DRIVING_INTERVAL, self.get_update_interval_vin(vin=vin) ) @@ -650,19 +669,19 @@ def _calculate_next_interval(self, vin: Text) -> int: _LOGGER.debug( "%s: %s; Polling policy set to '%s'. Scanning every %s seconds", vin[-5:], - self.car_state[vin].get("state"), + self.cars[vin].state, self.polling_policy, self.get_update_interval_vin(vin=vin), ) self.__update_state[vin] = "normal" return self.get_update_interval_vin(vin=vin) if self.polling_policy == "connected" and ( - self.is_sentry_mode_on(vin=vin) - or self.is_climate_on(vin=vin) + self.cars[vin].sentry_mode + or self.cars[vin].is_climate_on or ( - self.charging_state(vin=vin) - and self.charging_state(vin=vin) != "Disconnected" - and self.charging_state(vin=vin) != "" + self.cars[vin].charging_state + and self.cars[vin].charging_state != "Disconnected" + and self.cars[vin].charging_state != "" ) or cur_time - self.get_last_wake_up_time(vin=vin) <= IDLE_INTERVAL ): @@ -671,16 +690,16 @@ def _calculate_next_interval(self, vin: Text) -> int: "or last_wake_up_time < IDLE_INTERVAL " "Polling every %s seconds", vin[-5:], - self.car_state[vin].get("state"), + self.cars[vin].state, self.polling_policy, self.get_update_interval_vin(vin=vin), ) self.__update_state[vin] = "normal" return self.get_update_interval_vin(vin=vin) if (cur_time - self.get_last_park_time(vin=vin) > IDLE_INTERVAL) and not ( - self.is_sentry_mode_on(vin=vin) - or self.is_climate_on(vin=vin) - or self.charging_state(vin=vin) == "Charging" + self.cars[vin].sentry_mode + or self.cars[vin].is_climate_on + or self.cars[vin].charging_state == "Charging" ): sleep_interval = max(SLEEP_INTERVAL, self.get_update_interval_vin(vin=vin)) if self.__update_state[vin] != "trying_to_sleep": @@ -688,7 +707,7 @@ def _calculate_next_interval(self, vin: Text) -> int: _LOGGER.debug( "%s: %s; Polling policy set to '%s', trying to sleep; scan throttled to %s seconds and will ignore updates for %s seconds", vin[-5:], - self.car_state[vin].get("state"), + self.cars[vin].state, self.polling_policy, sleep_interval, sleep_interval + self._last_update_time[vin] - cur_time, @@ -699,7 +718,7 @@ def _calculate_next_interval(self, vin: Text) -> int: _LOGGER.debug( "%s: %s; Polling policy set to '%s', scanning every %s seconds", vin[-5:], - self.car_state[vin].get("state"), + self.cars[vin].state, self.polling_policy, self.get_update_interval_vin(vin=vin), ) @@ -734,26 +753,18 @@ async def update( """ - async def _get_and_process_car_data(vin: Text) -> None: + async def _get_and_process_car_data(vin: str) -> None: async with self.__lock[vin]: _LOGGER.debug("%s: Updating VEHICLE_DATA", vin[-5:]) try: - data = await self.api( - "VEHICLE_DATA", - path_vars={"vehicle_id": self.__vin_id_map[vin]}, - wake_if_asleep=wake_if_asleep, - ) + data = await self.get_vehicle_data(vin) except TeslaException: data = None - if data and data["response"]: - response = data["response"] - self.set_climate_params(vin=vin, params=response["climate_state"]) - self.set_charging_params(vin=vin, params=response["charge_state"]) - self.set_state_params(vin=vin, params=response["vehicle_state"]) - self.set_config_params(vin=vin, params=response["vehicle_config"]) + if data: + response = data if ( - self.shift_state(vin=vin) - and self.shift_state(vin=vin) + self.cars[vin].is_climate_on + and self.cars[vin].is_climate_on != response["drive_state"]["shift_state"] and ( response["drive_state"]["shift_state"] is None @@ -765,10 +776,8 @@ async def _get_and_process_car_data(vin: Text) -> None: timestamp=response["drive_state"]["timestamp"] / 1000, shift_state=response["drive_state"]["shift_state"], ) - self.__driving[vin] = response["drive_state"] - self.__gui[vin] = response["gui_settings"] self._last_update_time[vin] = round(time.time()) - if self.enable_websocket and self.is_in_gear(vin=vin): + if self.enable_websocket and self.cars[vin].is_in_gear: asyncio.create_task( self.__connection.websocket_connect( vin[-5:], @@ -783,12 +792,12 @@ async def _get_and_process_car_data(vin: Text) -> None: async def _get_and_process_site_data(energysite_id: int) -> None: _LOGGER.debug("Updating SITE_DATA for energysite: %s", energysite_id) try: - data = await self.api("SITE_DATA", path_vars={"site_id": energysite_id}) + data = await self.get_site_data(energysite_id) except TeslaException: data = None - if data and data["response"]: - response = data["response"] + if data: + response = data # Some setups always report grid_status of "Unknown" regardless # of the actual grid status. Others only report grid_status "Unknown" # when the actual grid status is unknown. These setups also sometimes @@ -815,14 +824,12 @@ async def _get_and_process_battery_data( ) -> None: _LOGGER.debug("Updating BATTERY_DATA for energysite: %s", energysite_id) try: - data = await self.api( - "BATTERY_DATA", path_vars={"battery_id": battery_id} - ) + data = await self.get_battery_data(battery_id) except TeslaException: data = None - if data and data["response"]: - self._battery_data[energysite_id].update(data["response"]) + if data: + self._battery_data[energysite_id].update(data) async def _get_and_process_battery_summary( energysite_id: int, battery_id: str @@ -830,14 +837,12 @@ async def _get_and_process_battery_summary( _LOGGER.debug("Updating BATTERY_SUMMARY for energysite: %s", energysite_id) try: - data = await self.api( - "BATTERY_SUMMARY", path_vars={"battery_id": battery_id} - ) + data = await self._battery_summary(battery_id) except TeslaException: data = None - if data and data["response"]: - self._battery_summary[energysite_id].update(data["response"]) + if data: + self._battery_summary[energysite_id].update(data) async with self.__update_lock: if self._include_vehicles: @@ -860,7 +865,7 @@ async def _get_and_process_battery_summary( self.set_car_online( vin=car["vin"], online_status=car["state"] == "online" ) - self.car_state[car["vin"]] = car + await self.cars[car["vin"]].update_car_state(car) self._last_attempted_update_time = cur_time # Only update online vehicles that haven't been updated recently @@ -875,17 +880,16 @@ async def _get_and_process_battery_summary( if ( (car_vin and car_vin != vin) or vin not in self.__lock - or (vin and self.car_state[vin].get("in_service")) + or (vin and self.cars[vin].in_service) ): continue async with self.__lock[vin]: - car_state = self.car_state[vin].get("state") if ( ( online or ( wake_if_asleep - and car_state in ["asleep", "offline"] + and self.cars[vin].state in ["asleep", "offline"] ) ) and ( # pylint: disable=too-many-boolean-expressions @@ -909,7 +913,7 @@ async def _get_and_process_battery_summary( "Last wake_up %s ago. " ), vin[-5:], - car_state, + self.cars[vin].state, self.__update.get(vin), cur_time - self._last_update_time[vin], cur_time - self.get_last_park_time(vin=vin), @@ -932,307 +936,6 @@ async def _get_and_process_battery_summary( return any(await asyncio.gather(*tasks)) - def get_climate_params(self, car_id: Text = None, vin: Text = None) -> Dict: - """Return cached copy of climate_params for car_id or all cars. - - Parameters - ---------- - car_id : string - Identifier for the car on the owner-api endpoint. It is the id - field for identifying the car across the owner-api endpoint. - https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id - vin : string - VIN number. - - If both car_id and vin is provided. VIN overrides car_id. - - Returns - ------- - dict - If car_id or vin exists, a dict with the climate parameters for a - single car. - Othewise, the entire dictionary with all cars. - - """ - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin and vin in self.__climate: - return self.__climate[vin] - return self.__climate - - def set_climate_params( - self, car_id: Text = None, vin: Text = None, params: Dict = None - ) -> None: - """Set climate_params for car_id.""" - params = params or {} - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin: - self.__climate[vin] = params - - def update_climate_params( - self, car_id: Text = None, vin: Text = None, params: Dict = None - ) -> None: - """Set climate_params for car_id.""" - params = params or {} - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin: - self.__climate[vin].update(params) - - def is_climate_on(self, car_id: Text = None, vin: Text = None) -> bool: - """Return true if climate is on.""" - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin and vin in self.__climate: - return self.get_climate_params(vin=vin).get("is_climate_on") - return False - - def get_charging_params(self, car_id: Text = None, vin: Text = None) -> Dict: - """Return cached copy of charging_params for car_id or all cars. - - Parameters - ---------- - car_id : string - Identifier for the car on the owner-api endpoint. It is the id - field for identifying the car across the owner-api endpoint. - https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id - vin : string - VIN number. - - If both car_id and vin is provided. VIN overrides car_id. - - Returns - ------- - dict - If car_id or vin exists, a dict with the charging parameters for a - single car. - Othewise, the entire dictionary with all cars. - - """ - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin and vin in self.__charging: - return self.__charging[vin] - return self.__charging - - def set_charging_params( - self, car_id: Text = None, vin: Text = None, params: Dict = None - ) -> None: - """Set charging_params for car_id.""" - params = params or {} - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin: - self.__charging[vin] = params - - def update_charging_params( - self, car_id: Text = None, vin: Text = None, params: Dict = None - ) -> None: - """Update charging_params for car_id.""" - params = params or {} - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin: - self.__charging[vin].update(params) - - def charging_state(self, car_id: Text = None, vin: Text = None) -> Text: - """Return charging state for a single vehicle.""" - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin and vin in self.__charging: - return self.get_charging_params(vin=vin).get("charging_state") - return None - - def get_state_params(self, car_id: Text = None, vin: Text = None) -> Dict: - """Return cached copy of state_params for car_id. or all cars. - - Parameters - ---------- - car_id : string - Identifier for the car on the owner-api endpoint. It is the id - field for identifying the car across the owner-api endpoint. - https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id - vin : string - VIN number. - - If both car_id and vin is provided. VIN overrides car_id. - - Returns - ------- - dict - If car_id or vin exists, a dict with the state parameters for a - single car. - Othewise, the entire dictionary with all cars. - - """ - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin and vin in self.__state: - return self.__state[vin] - return self.__state - - def set_state_params( - self, car_id: Text = None, vin: Text = None, params: Dict = None - ) -> None: - """Set state_params for car_id.""" - params = params or {} - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin: - self.__state[vin] = params - - def update_state_params( - self, car_id: Text = None, vin: Text = None, params: Dict = None - ) -> None: - """Update state_params for car_id.""" - params = params or {} - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin: - self.__state[vin].update(params) - - def is_sentry_mode_on(self, car_id: Text = None, vin: Text = None) -> bool: - """Return true if sentry_mode is on.""" - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin and vin in self.__state: - return self.get_state_params(vin=vin).get("sentry_mode") - return False - - def get_config_params(self, car_id: Text = None, vin: Text = None) -> Dict: - """Return cached copy of config_params for car_id or all cars. - - Parameters - ---------- - car_id : string - Identifier for the car on the owner-api endpoint. It is the id - field for identifying the car across the owner-api endpoint. - https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id - vin : string - VIN number. - - If both car_id and vin is provided. VIN overrides car_id. - - Returns - ------- - dict - If car_id or vin exists, a dict with the config parameters for a - single car. - Othewise, the entire dictionary with all cars. - - """ - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin and vin in self.__config: - return self.__config[vin] - return self.__config - - def set_config_params( - self, car_id: Text = None, vin: Text = None, params: Dict = None - ) -> None: - """Set config parameters for a car.""" - params = params or {} - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin: - self.__config[vin] = params - - def get_drive_params(self, car_id: Text = None, vin: Text = None) -> Dict: - """Return cached copy of drive_params for car_id or all cars. - - Parameters - ---------- - car_id : string - Identifier for the car on the owner-api endpoint. It is the id - field for identifying the car across the owner-api endpoint. - https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id - vin : string - VIN number. - - If both car_id and vin is provided. VIN overrides car_id. - - Returns - ------- - dict - If car_id or vin exists, a dict with the drive parameters for a - single car. - Othewise, the entire dictionary with all cars. - - """ - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin and vin in self.__driving: - return self.__driving[vin] - return self.__driving - - def set_drive_params( - self, car_id: Text = None, vin: Text = None, params: Dict = None - ) -> None: - """Set drive_params for car_id.""" - params = params or {} - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin: - self.__driving[vin] = params - - def shift_state(self, car_id: Text = None, vin: Text = None) -> Text: - """Return shift state for a single vehicle.""" - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin and vin in self.__driving: - return self.get_drive_params(vin=vin).get("shift_state") - return None - - def is_in_gear(self, car_id: Text = None, vin: Text = None) -> bool: - """Return true if car is in gear. False of car is parked or unknown.""" - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin and vin in self.__driving: - return self.shift_state(vin=vin) in ["D", "R"] - return False - - def get_gui_params(self, car_id: Text = None, vin: Text = None) -> Dict: - """Return cached copy of gui_params for car_id or all cars. - - Parameters - ---------- - car_id : string - Identifier for the car on the owner-api endpoint. It is the id - field for identifying the car across the owner-api endpoint. - https://tesla-api.timdorr.com/api-basics/vehicles#vehicle_id-vs-id - vin : string - VIN number. - - If both car_id and vin is provided. VIN overrides car_id. - - Returns - ------- - dict - If car_id or vin exists, a dict with the gui parameters for a - single car. - Othewise, the entire dictionary with all cars. - - """ - if car_id and not vin: - vin = self._id_to_vin(car_id) - if vin and vin in self.__gui: - return self.__gui[vin] - return self.__gui - - def set_gui_params( - self, car_id: Text = None, vin: Text = None, params: Dict = None - ) -> None: - """Set GUI params for car.""" - params = params or {} - print(car_id, vin) - if car_id and not vin: - vin = self._id_to_vin(car_id) - print(car_id, vin) - if vin: - self.__gui[vin] = params - print(self.__gui) - def get_updates(self, car_id: Text = None, vin: Text = None): """Get updates dictionary. @@ -1596,8 +1299,8 @@ def _process_websocket_message(self, data): _LOGGER.debug("Updating %s with websocket: %s", vin[-5:], update_json) self.__driving[vin]["timestamp"] = update_json["timestamp"] if ( - self.shift_state(vin=vin) - and self.shift_state(vin=vin) != update_json["shift_state"] + self.cars[vin].shift_state + and self.cars[vin].shift_state != update_json["shift_state"] and ( update_json["shift_state"] is None or update_json["shift_state"] == "P" @@ -1608,32 +1311,20 @@ def _process_websocket_message(self, data): timestamp=update_json["timestamp"] / 1000, shift_state=update_json["shift_state"], ) - self.__driving[vin]["shift_state"] = update_json["shift_state"] - self.__driving[vin]["speed"] = update_json["speed"] - self.__driving[vin]["power"] = update_json["power"] - self.__driving[vin]["latitude"] = update_json["est_corrected_lat"] - self.__driving[vin]["longitude"] = update_json["est_corrected_lng"] - self.__driving[vin]["heading"] = update_json["est_heading"] - self.__driving[vin]["native_latitude"] = update_json["native_latitude"] - self.__driving[vin]["native_longitude"] = update_json[ - "native_longitude" - ] - self.__driving[vin]["native_heading"] = update_json["native_heading"] - self.__driving[vin]["native_type"] = update_json["native_type"] - self.__driving[vin]["native_location_supported"] = update_json[ + self.cars[vin].shift_state = update_json["shift_state"] + self.cars[vin].speed = update_json["speed"] + self.cars[vin].power = update_json["power"] + self.cars[vin].latitude = update_json["est_corrected_lat"] + self.cars[vin].longitude = update_json["est_corrected_lng"] + self.cars[vin].heading = update_json["est_heading"] + self.cars[vin].native_latitude = update_json["native_latitude"] + self.cars[vin].native_longitude = update_json["native_longitude"] + self.cars[vin].native_heading = update_json["native_heading"] + self.cars[vin].native_type = update_json["native_type"] + self.cars[vin].native_location_supported = update_json[ "native_location_supported" ] - # old values - # self.__charging[vin]["timestamp"] = update_json["timestamp"] - # self.__state[vin]["timestamp"] = update_json["timestamp"] - # self.__state[vin]["odometer"] = update_json["odometer"] - # self.__charging[vin]["battery_level"] = update_json["soc"] - # self.__state[vin]["odometer"] = update_json["elevation"] - # no current elevation stored - # self.__charging[vin]["battery_range"] = update_json["range"] - # self.__charging[vin]["est_battery_range"] = update_json["est_range"] - # self.__driving[vin]["heading"] = update_json["heading"] - # est_heading appears more accurate + except ValueError as ex: _LOGGER.debug( "Websocket for %s malformed: %s\n%s", vin[-5:], values, ex diff --git a/tests/tesla_mock.py b/tests/tesla_mock.py index c9a2376c..4fd8a567 100644 --- a/tests/tesla_mock.py +++ b/tests/tesla_mock.py @@ -18,24 +18,24 @@ def __init__(self, monkeypatch) -> None: self._monkeypatch = monkeypatch self._monkeypatch.setattr(Controller, "api", self.mock_api) - self._monkeypatch.setattr( - Controller, "get_charging_params", self.mock_get_charging_params - ) - self._monkeypatch.setattr( - Controller, "get_climate_params", self.mock_get_climate_params - ) - self._monkeypatch.setattr( - Controller, "get_config_params", self.mock_get_config_params - ) - self._monkeypatch.setattr( - Controller, "get_drive_params", self.mock_get_drive_params - ) - self._monkeypatch.setattr( - Controller, "get_gui_params", self.mock_get_gui_params - ) - self._monkeypatch.setattr( - Controller, "get_state_params", self.mock_get_state_params - ) + # self._monkeypatch.setattr( + # Controller, "get_charging_params", self.mock_get_charging_params + # ) + # self._monkeypatch.setattr( + # Controller, "get_climate_params", self.mock_get_climate_params + # ) + # self._monkeypatch.setattr( + # Controller, "get_config_params", self.mock_get_config_params + # ) + # self._monkeypatch.setattr( + # Controller, "get_drive_params", self.mock_get_drive_params + # ) + # self._monkeypatch.setattr( + # Controller, "get_gui_params", self.mock_get_gui_params + # ) + # self._monkeypatch.setattr( + # Controller, "get_state_params", self.mock_get_state_params + # ) self._monkeypatch.setattr( Controller, "get_product_list", self.mock_get_product_list ) diff --git a/tests/unit_tests/test_polling_interval.py b/tests/unit_tests/test_polling_interval.py index 11a1e293..8779e23b 100644 --- a/tests/unit_tests/test_polling_interval.py +++ b/tests/unit_tests/test_polling_interval.py @@ -13,11 +13,11 @@ def test_update_interval(monkeypatch): """Test update_interval property""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) + _controller.set_id_vin(CAR_ID, VIN) # Test default update polling interval is set @@ -32,11 +32,11 @@ def test_update_interval(monkeypatch): def test_set_update_interval_vin(monkeypatch): """Test set_update_interval_vin().""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) + _controller.set_id_vin(CAR_ID, VIN) _controller.set_id_vin(CAR_ID2, VIN2) @@ -62,11 +62,11 @@ def test_set_update_interval_vin(monkeypatch): def test_get_update_interval_vin(monkeypatch): """Test get_update_interval_vin().""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) monkeypatch.setitem(_controller.car_online, VIN, True) - monkeypatch.setitem(_controller.car_state, VIN, _mock.data_request_vehicle()) + _controller.set_id_vin(CAR_ID, VIN) _controller.update_interval = UPDATE_INTERVAL From fd49d36c52ae95106c93ba9aa167f11591d20005 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sat, 10 Sep 2022 11:12:59 -0700 Subject: [PATCH 63/84] Additions for climate changes --- teslajsonpy/car.py | 54 ++++++++++++++++++++++++++++++++++++--- teslajsonpy/controller.py | 29 +++++++++++++-------- 2 files changed, 70 insertions(+), 13 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 06f932dc..09a09d39 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -37,6 +37,10 @@ def __init__(self, car: dict, controller, vehicle_data: dict) -> None: self._controller = controller self._vehicle_data = vehicle_data + self._previous_driver_temp = self.driver_temp_setting + self._previous_fan_status = self.fan_status + self._previous_passenger_temp = self.passenger_temp_setting + @property def display_name(self) -> str: """Return display name.""" @@ -206,6 +210,11 @@ def driver_temp_setting(self) -> float: """Return driver temperature setting.""" return self._vehicle_data.get("climate_state").get("driver_temp_setting") + @property + def fan_status(self) -> int: + """Return fan status setting.""" + return self._vehicle_data.get("climate_state").get("fan_status") + @property def fast_charger_present(self) -> bool: """Return fast charger present.""" @@ -375,6 +384,11 @@ def outside_temp(self) -> float: """Return outside temperature.""" return self._vehicle_data.get("climate_state").get("outside_temp") + @property + def passenger_temp_setting(self) -> float: + """Return passenger temperature setting.""" + return self._vehicle_data.get("climate_state").get("passenger_temp_setting") + @property def power(self) -> int: """Return power.""" @@ -621,7 +635,10 @@ async def set_climate_keeper_mode(self, keeper_id: int) -> None: wake_if_asleep=True, ) if data and data["response"]["result"] is True: - params = {"climate_keeper_mode": CLIMATE_KEEPER_ID_MAP[keeper_id]} + params = { + "climate_keeper_mode": CLIMATE_KEEPER_ID_MAP[keeper_id], + "is_climate_on": True, + } self._vehicle_data["climate_state"].update(params) async def set_heated_steering_wheel(self, value: bool) -> None: @@ -651,7 +668,15 @@ async def set_hvac_mode(self, value: str) -> None: wake_if_asleep=True, ) if data and data["response"]["result"] is True: - params = {"is_climate_on": False} + # Set additional values if turning HVAC off after defrost max + params = { + "defrost_mode": 0, + "driver_temp_setting": self._previous_driver_temp, + "is_climate_on": False, + "is_front_defroster_on": False, + "is_rear_defroster_on": False, + "passenger_temp_setting": self._previous_passenger_temp, + } self._vehicle_data["climate_state"].update(params) elif value == "on": @@ -677,7 +702,30 @@ async def set_max_defrost(self, state: int) -> None: wake_if_asleep=True, ) if data and data["response"]["result"] is True: - params = {"defrost_mode": state} + self._previous_driver_temp = self.driver_temp_setting + self._previous_passenger_temp = self.passenger_temp_setting + self._previous_fan_status = self.fan_status + + if state == 2: + params = { + "defrost_mode": state, + "driver_temp_setting": self.max_avail_temp, + "fan_status": 7, + "is_climate_on": True, + "is_front_defroster_on": True, + "is_rear_defroster_on": True, + "passenger_temp_setting": self.max_avail_temp, + } + if state == 0: + params = { + "defrost_mode": state, + "driver_temp_setting": self._previous_driver_temp, + "fan_status": self._previous_fan_status, + "is_climate_on": True, + "is_front_defroster_on": False, + "is_rear_defroster_on": False, + "passenger_temp_setting": self._previous_passenger_temp, + } self._vehicle_data["climate_state"].update(params) async def set_sentry_mode(self, value: bool) -> None: diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 99046ec1..10444cc7 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -361,7 +361,6 @@ async def connect( Args test_login (bool, optional): Whether to test credentials only. Defaults to False. - wake_if_asleep (bool, optional): Whether to wake up any sleeping cars to update state. Defaults to False. filtered_vins (list, optional): If not empty, filters the cars by the provided VINs. include_vehicles (bool, optional): Whether to include vehicles. Defaults to True. include_energysites(bool, optional): Whether to include energysites. Defaults to True. @@ -380,7 +379,7 @@ async def connect( self._include_vehicles = include_vehicles self._include_energysites = include_energysites - self._product_list = await self.get_product_list() + self._product_list = await self.get_product_list(wake_if_asleep=wake_if_asleep) if self._include_vehicles or not test_login: self._vehicle_list = [ @@ -408,7 +407,9 @@ async def connect( ) self.__driving[vin] = {} - self._vehicle_data[vin] = await self.get_vehicle_data(vin) + self._vehicle_data[vin] = await self.get_vehicle_data( + vin, wake_if_asleep=wake_if_asleep + ) if self._include_energysites or not test_login: self._energysite_list = [ @@ -519,14 +520,18 @@ def register_websocket_callback(self, callback) -> int: return len(self.__websocket_listeners) - 1 @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) - async def get_product_list(self) -> list: + async def get_product_list(self, wake_if_asleep: bool = False) -> list: """Get product list from Tesla.""" - return (await self.api("PRODUCT_LIST"))["response"] + return (await self.api("PRODUCT_LIST", wake_if_asleep=wake_if_asleep))[ + "response" + ] @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) - async def get_vehicles(self) -> list: + async def get_vehicles(self, wake_if_asleep: bool = False) -> list: """Get vehicles json from TeslaAPI.""" - return (await self.api("VEHICLE_LIST"))["response"] + return (await self.api("VEHICLE_LIST", wake_if_asleep=wake_if_asleep))[ + "response" + ] @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) async def get_site_config(self, energysite_id: int) -> dict: @@ -536,11 +541,13 @@ async def get_site_config(self, energysite_id: int) -> dict: ] @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) - async def get_vehicle_data(self, vin: str) -> dict: + async def get_vehicle_data(self, vin: str, wake_if_asleep: bool = False) -> dict: """Get vehicle data json from TeslaAPI for a given vin.""" return ( await self.api( - "VEHICLE_DATA", path_vars={"vehicle_id": self.__vin_id_map[vin]} + "VEHICLE_DATA", + path_vars={"vehicle_id": self.__vin_id_map[vin]}, + wake_if_asleep=wake_if_asleep, ) )["response"] @@ -757,7 +764,9 @@ async def _get_and_process_car_data(vin: str) -> None: async with self.__lock[vin]: _LOGGER.debug("%s: Updating VEHICLE_DATA", vin[-5:]) try: - data = await self.get_vehicle_data(vin) + data = await self.get_vehicle_data( + vin, wake_if_asleep=wake_if_asleep + ) except TeslaException: data = None if data: From ec685f1f6ef9ec1d7067a1040110f5af6c64c53f Mon Sep 17 00:00:00 2001 From: shred86 Date: Sat, 10 Sep 2022 15:14:54 -0700 Subject: [PATCH 64/84] Use prev charging state if None --- teslajsonpy/car.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 09a09d39..f6b6fe3d 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -37,6 +37,7 @@ def __init__(self, car: dict, controller, vehicle_data: dict) -> None: self._controller = controller self._vehicle_data = vehicle_data + self._previous_charging_state = self.charging_state self._previous_driver_temp = self.driver_temp_setting self._previous_fan_status = self.fan_status self._previous_passenger_temp = self.passenger_temp_setting @@ -171,9 +172,15 @@ def charging_state(self) -> str: """Return charging state. Returns - str: Charging, Stopped, Complete, others? + str: Charging, Stopped, Complete, Disconnected, NoPower """ - return self._vehicle_data.get("charge_state").get("charging_state") + current_charging_state = self._vehicle_data.get("charge_state").get( + "charging_state" + ) + # Tesla API returns None when car is sleeping + if current_charging_state: + return current_charging_state + return self._previous_charging_state @property def charger_voltage(self) -> int: From 3c544740b07ac376b3ba777e62330e095aed420e Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 11 Sep 2022 16:18:31 -0700 Subject: [PATCH 65/84] Minor clean up --- teslajsonpy/car.py | 5 ++--- teslajsonpy/controller.py | 45 ++++++++++++++++++++++++--------------- teslajsonpy/energy.py | 10 +++++++++ 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index f6b6fe3d..4c74a018 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -70,7 +70,7 @@ def vin(self) -> str: @property def data_available(self) -> bool: """Return if data is available.""" - return self._vehicle_data + return self._vehicle_data != {} @property def battery_level(self) -> float: @@ -177,7 +177,7 @@ def charging_state(self) -> str: current_charging_state = self._vehicle_data.get("charge_state").get( "charging_state" ) - # Tesla API returns None when car is sleeping + # Tesla API returns None when car is sleeping; use previous reported state if current_charging_state: return current_charging_state return self._previous_charging_state @@ -240,7 +240,6 @@ def fast_charger_type(self) -> str: @property def gui_distance_units(self) -> str: """Return gui distance units.""" - # Why set default to mi/hr? return self._vehicle_data.get("gui_settings").get("gui_distance_units") @property diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 10444cc7..3ed12384 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -624,6 +624,7 @@ async def _wake_up(self, car_id): car_id = self._update_id(car_id) async with self.__wakeup_conds[car_vin]: cur_time = round(time.time()) + if not self.is_car_online(vin=car_vin) or ( self._last_wake_up_attempt[car_vin] < self._last_attempted_update_time ): @@ -636,6 +637,7 @@ async def _wake_up(self, car_id): await self.cars[car_vin].update_car_state(result["response"]) self._last_wake_up_attempt[car_vin] = cur_time _LOGGER.debug("%s: Wakeup: %s", car_vin[-5:], self.cars[car_vin].state) + return self.is_car_online(vin=car_vin) def _calculate_next_interval(self, vin: Text) -> int: @@ -656,10 +658,12 @@ def _calculate_next_interval(self, vin: Text) -> int: ) if vin not in self.__update_state: self.__update_state[vin] = "normal" + if self.cars[vin].state == "asleep" or self.cars[vin].shift_state: self.set_last_park_time( vin=vin, timestamp=cur_time, shift_state=self.cars[vin].shift_state ) + if self.cars[vin].is_in_gear: driving_interval = min( DRIVING_INTERVAL, self.get_update_interval_vin(vin=vin) @@ -672,6 +676,7 @@ def _calculate_next_interval(self, vin: Text) -> int: driving_interval, ) return driving_interval + if self.polling_policy == "always": _LOGGER.debug( "%s: %s; Polling policy set to '%s'. Scanning every %s seconds", @@ -682,6 +687,7 @@ def _calculate_next_interval(self, vin: Text) -> int: ) self.__update_state[vin] = "normal" return self.get_update_interval_vin(vin=vin) + if self.polling_policy == "connected" and ( self.cars[vin].sentry_mode or self.cars[vin].is_climate_on @@ -703,6 +709,7 @@ def _calculate_next_interval(self, vin: Text) -> int: ) self.__update_state[vin] = "normal" return self.get_update_interval_vin(vin=vin) + if (cur_time - self.get_last_park_time(vin=vin) > IDLE_INTERVAL) and not ( self.cars[vin].sentry_mode or self.cars[vin].is_climate_on @@ -720,6 +727,7 @@ def _calculate_next_interval(self, vin: Text) -> int: sleep_interval + self._last_update_time[vin] - cur_time, ) return sleep_interval + if self.__update_state[vin] != "normal": self.__update_state[vin] = "normal" _LOGGER.debug( @@ -729,6 +737,7 @@ def _calculate_next_interval(self, vin: Text) -> int: self.polling_policy, self.get_update_interval_vin(vin=vin), ) + return self.get_update_interval_vin(vin=vin) async def update( @@ -764,13 +773,13 @@ async def _get_and_process_car_data(vin: str) -> None: async with self.__lock[vin]: _LOGGER.debug("%s: Updating VEHICLE_DATA", vin[-5:]) try: - data = await self.get_vehicle_data( + response = await self.get_vehicle_data( vin, wake_if_asleep=wake_if_asleep ) except TeslaException: - data = None - if data: - response = data + response = None + + if response: if ( self.cars[vin].is_climate_on and self.cars[vin].is_climate_on @@ -786,6 +795,7 @@ async def _get_and_process_car_data(vin: str) -> None: shift_state=response["drive_state"]["shift_state"], ) self._last_update_time[vin] = round(time.time()) + if self.enable_websocket and self.cars[vin].is_in_gear: asyncio.create_task( self.__connection.websocket_connect( @@ -801,12 +811,11 @@ async def _get_and_process_car_data(vin: str) -> None: async def _get_and_process_site_data(energysite_id: int) -> None: _LOGGER.debug("Updating SITE_DATA for energysite: %s", energysite_id) try: - data = await self.get_site_data(energysite_id) + response = await self.get_site_data(energysite_id) except TeslaException: - data = None + response = None - if data: - response = data + if response: # Some setups always report grid_status of "Unknown" regardless # of the actual grid status. Others only report grid_status "Unknown" # when the actual grid status is unknown. These setups also sometimes @@ -833,25 +842,24 @@ async def _get_and_process_battery_data( ) -> None: _LOGGER.debug("Updating BATTERY_DATA for energysite: %s", energysite_id) try: - data = await self.get_battery_data(battery_id) + response = await self.get_battery_data(battery_id) except TeslaException: - data = None + response = None - if data: - self._battery_data[energysite_id].update(data) + if response: + self._battery_data[energysite_id].update(response) async def _get_and_process_battery_summary( energysite_id: int, battery_id: str ) -> None: _LOGGER.debug("Updating BATTERY_SUMMARY for energysite: %s", energysite_id) - try: - data = await self._battery_summary(battery_id) + response = await self._battery_summary(battery_id) except TeslaException: - data = None + response = None - if data: - self._battery_summary[energysite_id].update(data) + if response: + self._battery_summary[energysite_id].update(response) async with self.__update_lock: if self._include_vehicles: @@ -892,6 +900,7 @@ async def _get_and_process_battery_summary( or (vin and self.cars[vin].in_service) ): continue + async with self.__lock[vin]: if ( ( @@ -932,8 +941,10 @@ async def _get_and_process_battery_summary( # do not update energy sites if car_id was a parameter. for energysite in self._energysite_list: energysite_id = energysite["energy_site_id"] + if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: tasks.append(_get_and_process_site_data(energysite_id)) + if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_BATTERY: battery_id = energysite["id"] tasks.append( diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index 7da0a96a..1ecce8c3 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -75,6 +75,11 @@ def __init__( super().__init__(api, energysite, site_config) self._site_data = site_data + @property + def data_available(self) -> bool: + """Return if data is available.""" + return self._site_data != {} + @property def grid_power(self) -> float: """Return grid power in Watts.""" @@ -133,6 +138,11 @@ def battery_power(self) -> float: if self._battery_data.get("power_reading"): return self._battery_data["power_reading"][0]["battery_power"] + @property + def data_available(self) -> bool: + """Return if data is available.""" + return self._battery_data != {} + @property def energy_left(self) -> float: """Return battery energy left in Watt hours.""" From 3367d8d719cb9cc8161e4d5255cb5787a302f403 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 11 Sep 2022 21:53:58 -0700 Subject: [PATCH 66/84] Fix updating battery summary --- teslajsonpy/controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 3ed12384..25187649 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -854,7 +854,7 @@ async def _get_and_process_battery_summary( ) -> None: _LOGGER.debug("Updating BATTERY_SUMMARY for energysite: %s", energysite_id) try: - response = await self._battery_summary(battery_id) + response = await self.get_battery_summary(battery_id) except TeslaException: response = None From 619c62b0a81985c18615100342cc0821199ad535 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 18 Sep 2022 12:41:08 -0700 Subject: [PATCH 67/84] Move api requests to object creation --- teslajsonpy/car.py | 4 - teslajsonpy/controller.py | 171 ++++++++++++++++++-------------------- 2 files changed, 81 insertions(+), 94 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 4c74a018..4b088ec5 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -849,10 +849,6 @@ async def trigger_homelink(self) -> None: if result is False: raise HomelinkError(f"Error calling trigger_homelink: {reason}") - async def update_car_state(self, state: dict) -> None: - """Update the car state.""" - self._vehicle_data.update(state) - async def unlock(self) -> None: """Send unlock command.""" data = await self._send_command( diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 25187649..f241928f 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -351,8 +351,6 @@ def __init__( async def connect( self, test_login: bool = False, - wake_if_asleep: bool = False, - filtered_vins: Optional[List[Text]] = None, include_vehicles: bool = True, include_energysites: bool = True, mfa_code: Text = "", @@ -361,7 +359,6 @@ async def connect( Args test_login (bool, optional): Whether to test credentials only. Defaults to False. - filtered_vins (list, optional): If not empty, filters the cars by the provided VINs. include_vehicles (bool, optional): Whether to include vehicles. Defaults to True. include_energysites(bool, optional): Whether to include energysites. Defaults to True. mfa_code (Text, optional): MFA code to use for connection @@ -379,68 +376,21 @@ async def connect( self._include_vehicles = include_vehicles self._include_energysites = include_energysites - self._product_list = await self.get_product_list(wake_if_asleep=wake_if_asleep) - - if self._include_vehicles or not test_login: - self._vehicle_list = [ - cars for cars in self._product_list if "vehicle_id" in cars - ] - - for car in self._vehicle_list: - vin = car["vin"] - if filtered_vins and vin not in filtered_vins: - _LOGGER.debug("Skipping car with VIN: %s", vin) - continue - - self.set_id_vin(car_id=car["id"], vin=vin) - self.set_vehicle_id_vin(vehicle_id=car["vehicle_id"], vin=vin) - self.__lock[vin] = asyncio.Lock() - self.__wakeup_conds[vin] = asyncio.Lock() - self._last_update_time[vin] = 0 - self._last_wake_up_attempt[vin] = 0 - self._last_wake_up_time[vin] = 0 - self.__update[vin] = True - self.__update_state[vin] = "normal" - self.set_car_online(vin=vin, online_status=car["state"] == "online") - self.set_last_park_time( - vin=vin, timestamp=self._last_attempted_update_time - ) - self.__driving[vin] = {} - - self._vehicle_data[vin] = await self.get_vehicle_data( - vin, wake_if_asleep=wake_if_asleep - ) - - if self._include_energysites or not test_login: - self._energysite_list = [ - p - for p in self._product_list - if p.get(RESOURCE_TYPE) == RESOURCE_TYPE_SOLAR - or p.get(RESOURCE_TYPE) == RESOURCE_TYPE_BATTERY - ] - - for energysite in self._energysite_list: - energysite_id = energysite.get("energy_site_id") + if not test_login: + self._product_list = await self.get_product_list() - self._site_config[energysite_id] = await self.get_site_config( - energysite_id - ) + if self._include_vehicles: + self._vehicle_list = [ + cars for cars in self._product_list if "vehicle_id" in cars + ] - if energysite.get(RESOURCE_TYPE) == RESOURCE_TYPE_SOLAR: - self._site_data[energysite_id] = await self.get_site_data( - energysite_id - ) - if energysite.get(RESOURCE_TYPE) == RESOURCE_TYPE_BATTERY: - battery_id = energysite.get("id") - self._battery_data[energysite_id] = await self.get_battery_data( - battery_id - ) - self._battery_summary[ - energysite_id - ] = await self.get_battery_summary(battery_id) - # For dealing with sites that always report "Unknown" - # Default to True and check during updates - self._grid_status_unknown = {energysite_id: True} + if self._include_energysites: + self._energysite_list = [ + p + for p in self._product_list + if p.get(RESOURCE_TYPE) == RESOURCE_TYPE_SOLAR + or p.get(RESOURCE_TYPE) == RESOURCE_TYPE_BATTERY + ] return { "refresh_token": self.__connection.refresh_token, @@ -572,51 +522,92 @@ async def get_battery_summary(self, battery_id: str) -> dict: await self.api("BATTERY_SUMMARY", path_vars={"battery_id": battery_id}) )["response"] - def generate_car_objects(self) -> Dict[str, TeslaCar]: - """Generate car objects.""" + async def generate_car_objects( + self, + wake_if_asleep: bool = False, + filtered_vins: Optional[List[Text]] = None, + ) -> Dict[str, TeslaCar]: + """Generate car objects. + + Args + wake_if_asleep (bool, optional): Wake up vehicles if asleep. + filtered_vins (list, optional): If not empty, filters the cars by the provided VINs. + """ for car in self._vehicle_list: vin = car["vin"] + if filtered_vins and vin not in filtered_vins: + _LOGGER.debug("Skipping car with VIN: %s", vin) + continue + + self.set_id_vin(car_id=car["id"], vin=vin) + self.set_vehicle_id_vin(vehicle_id=car["vehicle_id"], vin=vin) + self.__lock[vin] = asyncio.Lock() + self.__wakeup_conds[vin] = asyncio.Lock() + self._last_update_time[vin] = 0 + self._last_wake_up_attempt[vin] = 0 + self._last_wake_up_time[vin] = 0 + self.__update[vin] = True + self.__update_state[vin] = "normal" + self.set_car_online(vin=vin, online_status=car["state"] == "online") + self.set_last_park_time(vin=vin, timestamp=self._last_attempted_update_time) + self.__driving[vin] = {} + self._vehicle_data[vin] = {} # Prevent KeyError for _wake_up method + + self._vehicle_data[vin] = await self.get_vehicle_data( + vin, wake_if_asleep=wake_if_asleep + ) self.cars[vin] = TeslaCar(car, self, self._vehicle_data[vin]) return self.cars - def generate_energysite_objects(self) -> Dict[int, EnergySite]: + async def generate_energysite_objects(self) -> Dict[int, EnergySite]: """Generate energy site objects.""" for energysite in self._energysite_list: energysite_id = energysite["energy_site_id"] + + self._site_config[energysite_id] = await self.get_site_config(energysite_id) + # For dealing with sites that always report "Unknown" + # Default to True and check during updates + self._grid_status_unknown = {energysite_id: True} # Solar only systems (no Powerwalls) are listed as "solar" if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_SOLAR: + self._site_data[energysite_id] = await self.get_site_data(energysite_id) + self.energysites[energysite_id] = SolarSite( self.api, energysite, self._site_config[energysite_id], self._site_data[energysite_id], ) - # Solar with Powerwall are listed as "battery" - if ( - energysite[RESOURCE_TYPE] == RESOURCE_TYPE_BATTERY - and energysite["components"]["solar"] - ): - self.energysites[energysite_id] = SolarPowerwallSite( - self.api, - energysite, - self._site_config[energysite_id], - self._battery_data[energysite_id], - self._battery_summary[energysite_id], + # Powerwall systems listed as "battery" + if energysite[RESOURCE_TYPE] == RESOURCE_TYPE_BATTERY: + battery_id = energysite.get("id") + + self._battery_data[energysite_id] = await self.get_battery_data( + battery_id ) - # Assumed Powerwall only (no solar) is listed as "battery" - if ( - energysite[RESOURCE_TYPE] == RESOURCE_TYPE_BATTERY - and not energysite["components"]["solar"] - ): - self.energysites[energysite_id] = PowerwallSite( - self.api, - energysite, - self._site_config[energysite_id], - self._battery_data[energysite_id], - self._battery_summary[energysite_id], + + self._battery_summary[energysite_id] = await self.get_battery_summary( + battery_id ) + if energysite["components"]["solar"]: + self.energysites[energysite_id] = SolarPowerwallSite( + self.api, + energysite, + self._site_config[energysite_id], + self._battery_data[energysite_id], + self._battery_summary[energysite_id], + ) + else: + self.energysites[energysite_id] = PowerwallSite( + self.api, + energysite, + self._site_config[energysite_id], + self._battery_data[energysite_id], + self._battery_summary[energysite_id], + ) + return self.energysites async def _wake_up(self, car_id): @@ -634,7 +625,7 @@ async def _wake_up(self, car_id): self.set_car_online( car_id=car_id, online_status=result["response"]["state"] == "online" ) - await self.cars[car_vin].update_car_state(result["response"]) + self._vehicle_data[car_vin].update(result["response"]) self._last_wake_up_attempt[car_vin] = cur_time _LOGGER.debug("%s: Wakeup: %s", car_vin[-5:], self.cars[car_vin].state) @@ -882,7 +873,7 @@ async def _get_and_process_battery_summary( self.set_car_online( vin=car["vin"], online_status=car["state"] == "online" ) - await self.cars[car["vin"]].update_car_state(car) + self._vehicle_data[car["vin"]].update(car) self._last_attempted_update_time = cur_time # Only update online vehicles that haven't been updated recently From cf1998deb385eaf3d725b444648290c59956c672 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 18 Sep 2022 12:57:11 -0700 Subject: [PATCH 68/84] Minor clean up --- teslajsonpy/controller.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index f241928f..51f7f89f 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -373,18 +373,16 @@ async def connect( self._last_attempted_update_time = round(time.time()) self.__update_lock = asyncio.Lock() - self._include_vehicles = include_vehicles - self._include_energysites = include_energysites if not test_login: self._product_list = await self.get_product_list() - if self._include_vehicles: + if include_vehicles: self._vehicle_list = [ cars for cars in self._product_list if "vehicle_id" in cars ] - if self._include_energysites: + if include_energysites: self._energysite_list = [ p for p in self._product_list @@ -853,7 +851,7 @@ async def _get_and_process_battery_summary( self._battery_summary[energysite_id].update(response) async with self.__update_lock: - if self._include_vehicles: + if self._vehicle_list: cur_time = round(time.time()) # Update the online cars using get_vehicles() last_update = self._last_attempted_update_time @@ -928,7 +926,7 @@ async def _get_and_process_battery_summary( cur_time - self.get_last_park_time(vin=vin), cur_time - self.get_last_wake_up_time(vin=vin), ) - if self._include_energysites and self._energysite_list and not car_id: + if self._energysite_list and not car_id: # do not update energy sites if car_id was a parameter. for energysite in self._energysite_list: energysite_id = energysite["energy_site_id"] From 8f1762547e48c886ef47094177d36128e18d4cd1 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sun, 18 Sep 2022 13:31:07 -0700 Subject: [PATCH 69/84] Get data from vehicle_data dict directrly --- teslajsonpy/controller.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 51f7f89f..81570f6a 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -625,7 +625,11 @@ async def _wake_up(self, car_id): ) self._vehicle_data[car_vin].update(result["response"]) self._last_wake_up_attempt[car_vin] = cur_time - _LOGGER.debug("%s: Wakeup: %s", car_vin[-5:], self.cars[car_vin].state) + _LOGGER.debug( + "%s: Wakeup: %s", + car_vin[-5:], + self._vehicle_data[car_vin].get("state"), + ) return self.is_car_online(vin=car_vin) From 9a60e5b1f798d13a046b05956aff1bddcc884ef2 Mon Sep 17 00:00:00 2001 From: shred86 Date: Mon, 19 Sep 2022 20:25:51 -0700 Subject: [PATCH 70/84] Fixes for toggle trunk and frunk --- teslajsonpy/car.py | 46 +++++++++++++++++++++------------------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 4b088ec5..87f92b9f 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -45,27 +45,27 @@ def __init__(self, car: dict, controller, vehicle_data: dict) -> None: @property def display_name(self) -> str: """Return display name.""" - return self._vehicle_data.get("display_name") + return self._car.get("display_name") @property def id(self) -> int: """Return id.""" - return self._vehicle_data.get("id") + return self._car.get("id") @property def state(self) -> str: """Return car state.""" - return self._vehicle_data.get("state") + return self._car.get("state") @property def vehicle_id(self) -> int: """Return car id.""" - return self._vehicle_data.get("vehicle_id") + return self._car.get("vehicle_id") @property def vin(self) -> str: """Return car vin.""" - return self._vehicle_data.get("vin") + return self._car.get("vin") @property def data_available(self) -> bool: @@ -288,18 +288,15 @@ def is_climate_on(self) -> bool: return self._vehicle_data.get("climate_state").get("is_climate_on") @property - def is_frunk_locked(self) -> int: - """Return car frunk is locked (closed). + def is_frunk_closed(self) -> bool: + """Return car frunk is closed. Returns - int: 0 (locked), 255 (unlocked) + bool: True (0), False (255) """ response = self._vehicle_data.get("vehicle_state").get("ft") - if response == 0: - return True - if response == 255: - return False + return True if response == 0 else False @property def is_in_gear(self) -> bool: @@ -317,18 +314,15 @@ def is_steering_wheel_heater_on(self) -> bool: return self._vehicle_data.get("climate_state").get("steering_wheel_heater") @property - def is_trunk_locked(self) -> bool: - """Return car trunk is locked (closed). + def is_trunk_closed(self) -> bool: + """Return car trunk is closed. Returns - bool: False (0), True (255) + bool: True (0), False (255) """ response = self._vehicle_data.get("vehicle_state").get("rt") - if response == 0: - return True - if response == 255: - return False + return True if response == 0 else False @property def is_on(self) -> bool: @@ -793,7 +787,8 @@ async def wake_up(self) -> None: ) async def toggle_trunk(self) -> None: - """Actuate rear trunk lock.""" + """Actuate rear trunk.""" + prev_is_trunk_closed = self.is_trunk_closed data = await self._send_command( "ACTUATE_TRUNK", path_vars={"vehicle_id": self.id}, @@ -801,15 +796,16 @@ async def toggle_trunk(self) -> None: wake_if_asleep=True, ) if data and data["response"]["result"] is True: - if self.is_trunk_locked: + if not prev_is_trunk_closed: params = {"rt": 0} self._vehicle_data["vehicle_state"].update(params) - if not self.is_trunk_locked: + if prev_is_trunk_closed: params = {"rt": 255} self._vehicle_data["vehicle_state"].update(params) async def toggle_frunk(self) -> None: - """Actuate front trunk lock.""" + """Actuate front trunk.""" + prev_is_frunk_closed = self.is_frunk_closed data = await self._send_command( "ACTUATE_TRUNK", path_vars={"vehicle_id": self.id}, @@ -817,10 +813,10 @@ async def toggle_frunk(self) -> None: wake_if_asleep=True, ) if data and data["response"]["result"] is True: - if self.is_frunk_locked: + if not prev_is_frunk_closed: params = {"ft": 0} self._vehicle_data["vehicle_state"].update(params) - if not self.is_frunk_locked: + if prev_is_frunk_closed: params = {"ft": 255} self._vehicle_data["vehicle_state"].update(params) From f30a58c7697fdb1ec4f7736a057bd3107716f242 Mon Sep 17 00:00:00 2001 From: shred86 Date: Fri, 23 Sep 2022 21:39:58 -0700 Subject: [PATCH 71/84] Add powered lift gate property --- teslajsonpy/car.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 87f92b9f..9beedd6b 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -394,6 +394,11 @@ def power(self) -> int: """Return power.""" return self._vehicle_data.get("drive_state").get("power") + @property + def powered_lift_gate(self) -> bool: + """Return True if car has power lift gate.""" + return self._vehicle_data.get("vehicle_config").get("plg") + @property def rear_seat_heaters(self) -> int: """Return if car has rear (second row) heated seats. From 542d4e41dbaee3d0868b53a8aa6494e7e2293be1 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sat, 24 Sep 2022 10:21:24 -0700 Subject: [PATCH 72/84] Update tests --- teslajsonpy/controller.py | 7 ++ tests/tesla_mock.py | 49 +++++++++++++- tests/unit_tests/test_car.py | 114 +++++++++++++------------------- tests/unit_tests/test_energy.py | 39 ++++------- 4 files changed, 112 insertions(+), 97 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 81570f6a..271e81ab 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -132,6 +132,7 @@ def valid_result(result): is_wake_command = False is_wake_api = False is_energysite_command = False + if wrapped.__name__ == "api": car_id = kwargs.get("path_vars", {}).get("vehicle_id", "") is_wake_api = kwargs.get("name", "").lower() == "wake_up" @@ -139,7 +140,9 @@ def valid_result(result): car_id = args[0] if not kwargs.get("vehicle_id") else kwargs.get("vehicle_id") is_wake_command = len(args) >= 2 and args[1].lower() == "wake_up" is_energysite_command = kwargs.get("product_type") == PRODUCT_TYPE_ENERGY_SITES + result = None + if ( instance._id_to_vin(car_id) is None or (car_id and instance.is_car_online(car_id=car_id)) @@ -156,6 +159,7 @@ def valid_result(result): instance.set_car_online(car_id=car_id, online_status=False) # instance.car_online[instance._id_to_vin(car_id)] = False raise + if ( valid_result(result) or is_wake_command @@ -163,6 +167,7 @@ def valid_result(result): or instance._id_to_vin(car_id) is None ): return result + _LOGGER.debug( "%s: " "wake_up needed for %s -> %s " @@ -242,6 +247,7 @@ def valid_result(result): "Exception: %s\n%s(%s %s)", str(ex), wrapped.__name__, args, kwargs ) raise + if valid_result(result): _LOGGER.debug("Result: %s", result) if ( @@ -259,6 +265,7 @@ def valid_result(result): online_status=result.get("response").get("state") == "online", ) return result + raise TeslaException("could_not_wake_buses") diff --git a/tests/tesla_mock.py b/tests/tesla_mock.py index 4fd8a567..5fe5ff41 100644 --- a/tests/tesla_mock.py +++ b/tests/tesla_mock.py @@ -33,16 +33,23 @@ def __init__(self, monkeypatch) -> None: # self._monkeypatch.setattr( # Controller, "get_gui_params", self.mock_get_gui_params # ) - # self._monkeypatch.setattr( - # Controller, "get_state_params", self.mock_get_state_params - # ) self._monkeypatch.setattr( Controller, "get_product_list", self.mock_get_product_list ) self._monkeypatch.setattr( Controller, "get_site_config", self.mock_get_site_config ) + self._monkeypatch.setattr(Controller, "get_site_data", self.mock_get_site_data) + self._monkeypatch.setattr( + Controller, "get_battery_data", self.mock_get_battery_data + ) + self._monkeypatch.setattr( + Controller, "get_battery_summary", self.mock_get_battery_summary + ) self._monkeypatch.setattr(Controller, "update", self.mock_update) + self._monkeypatch.setattr( + Controller, "get_vehicle_data", self.mock_get_vehicle_data + ) self._energysites = copy.deepcopy(ENERGYSITES) self._product_list = copy.deepcopy(PRODUCT_LIST) self._vehicle_data = copy.deepcopy(VEHICLE_DATA) @@ -114,6 +121,26 @@ def mock_get_site_config(self, *args, **kwargs): """Mock controller's get_site_config method.""" return self.controller_get_site_config() + def mock_get_site_data(self, *args, **kwargs): + # pylint: disable=unused-argument + """Mock controller's get_site_data method.""" + return self.controller_get_site_data() + + def mock_get_battery_data(self, *args, **kwargs): + # pylint: disable=unused-argument + """Mock controller's get_battery_data method.""" + return self.controller_get_battery_data() + + def mock_get_battery_summary(self, *args, **kwargs): + # pylint: disable=unused-argument + """Mock controller's get_battery_summary method.""" + return self.controller_get_battery_summary() + + def mock_get_vehicle_data(self, *args, **kwargs): + # pylint: disable=unused-argument + """Mock controller's get_vehicle_data method.""" + return self.controller_get_vehicle_data() + def mock_get_last_update_time(self, *args, **kwargs): # pylint: disable=unused-argument """Mock controller's get_last_update_time method.""" @@ -170,6 +197,22 @@ async def controller_get_site_config(self): """Monkeypatch for controller.get_site_config().""" return self._site_config + async def controller_get_site_data(self): + """Monkeypatch for controller.get_site_data().""" + return self._site_data + + async def controller_get_battery_data(self): + """Monkeypatch for controller.get_battery_data().""" + return self._battery_data + + async def controller_get_battery_summary(self): + """Monkeypatch for controller.get_battery_summary().""" + return self._battery_summary + + async def controller_get_vehicle_data(self): + """Monkeypatch for controller.get_vehicle_data().""" + return self._vehicle_data + @staticmethod async def controller_update(): """Monkeypatch for controller.update().""" diff --git a/tests/unit_tests/test_car.py b/tests/unit_tests/test_car.py index 7323eb54..4030b6a3 100644 --- a/tests/unit_tests/test_car.py +++ b/tests/unit_tests/test_car.py @@ -14,11 +14,10 @@ @pytest.mark.asyncio async def test_car_properties(monkeypatch): """Test TeslaCar class properties.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] @@ -150,7 +149,7 @@ async def test_car_properties(monkeypatch): assert _car.is_climate_on == VEHICLE_DATA["climate_state"]["is_climate_on"] - assert _car.is_frunk_locked + assert _car.is_frunk_closed assert _car.is_locked == VEHICLE_DATA["vehicle_state"]["locked"] @@ -158,7 +157,7 @@ async def test_car_properties(monkeypatch): "steering_wheel_heater" ) - assert _car.is_trunk_locked + assert _car.is_trunk_closed assert _car.is_on @@ -215,11 +214,10 @@ async def test_car_properties(monkeypatch): @pytest.mark.asyncio async def test_change_charge_limit(monkeypatch): """Test change charge limit.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.change_charge_limit(70.0) is None @@ -228,11 +226,10 @@ async def test_change_charge_limit(monkeypatch): @pytest.mark.asyncio async def test_charge_port_door_open_close(monkeypatch): """Test charge port door open/close command.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.charge_port_door_open() is None @@ -243,11 +240,10 @@ async def test_charge_port_door_open_close(monkeypatch): @pytest.mark.asyncio async def test_flash_lights(monkeypatch): """Test flash lights command.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.flash_lights() is None @@ -256,11 +252,10 @@ async def test_flash_lights(monkeypatch): @pytest.mark.asyncio async def test_honk_horn(monkeypatch): """Test honk horn command.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.honk_horn() is None @@ -269,11 +264,10 @@ async def test_honk_horn(monkeypatch): @pytest.mark.asyncio async def test_lock(monkeypatch): """Test lock command.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.lock() is None @@ -282,11 +276,10 @@ async def test_lock(monkeypatch): @pytest.mark.asyncio async def test_remote_seat_heater_request(monkeypatch): """Test remote seat heater request.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.remote_seat_heater_request(3, 1) is None @@ -295,11 +288,10 @@ async def test_remote_seat_heater_request(monkeypatch): @pytest.mark.asyncio async def test_schedule_software_update(monkeypatch): """Test scheduling software update.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.schedule_software_update() is None @@ -308,11 +300,10 @@ async def test_schedule_software_update(monkeypatch): @pytest.mark.asyncio async def test_set_charging_amps(monkeypatch): """Test setting charging amps.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_charging_amps(32.0) is None @@ -321,11 +312,10 @@ async def test_set_charging_amps(monkeypatch): @pytest.mark.asyncio async def test_set_cabin_overheat_protection(monkeypatch): """Test setting heated steering wheel.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_cabin_overheat_protection("On") is None @@ -334,11 +324,10 @@ async def test_set_cabin_overheat_protection(monkeypatch): @pytest.mark.asyncio async def test_set_climate_keeper_mode(monkeypatch): """Test setting climate keeper mode.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_climate_keeper_mode(1) is None @@ -347,11 +336,10 @@ async def test_set_climate_keeper_mode(monkeypatch): @pytest.mark.asyncio async def test_set_heated_steering_wheel(monkeypatch): """Test setting heated steering wheel.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_heated_steering_wheel(True) is None @@ -360,11 +348,10 @@ async def test_set_heated_steering_wheel(monkeypatch): @pytest.mark.asyncio async def test_set_hvac_mode(monkeypatch): """Test setting HVAC mode.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_hvac_mode("on") is None @@ -373,11 +360,10 @@ async def test_set_hvac_mode(monkeypatch): @pytest.mark.asyncio async def test_set_max_defrost(monkeypatch): """Test wake up.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_max_defrost(2) is None @@ -386,11 +372,10 @@ async def test_set_max_defrost(monkeypatch): @pytest.mark.asyncio async def test_set_sentry_mode(monkeypatch): """Test wake up.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_sentry_mode(True) is None @@ -399,11 +384,10 @@ async def test_set_sentry_mode(monkeypatch): @pytest.mark.asyncio async def test_set_temperature(monkeypatch): """Test wake up.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.set_temperature(22.0) is None @@ -412,11 +396,10 @@ async def test_set_temperature(monkeypatch): @pytest.mark.asyncio async def test_start_stop_charge(monkeypatch): """Test wake up.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.start_charge() is None @@ -427,11 +410,10 @@ async def test_start_stop_charge(monkeypatch): @pytest.mark.asyncio async def test_wake_up(monkeypatch): """Test wake up.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.wake_up() is None @@ -440,11 +422,10 @@ async def test_wake_up(monkeypatch): @pytest.mark.asyncio async def test_toggle_trunk(monkeypatch): """Test toggle trunk.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.toggle_trunk() is None @@ -453,11 +434,10 @@ async def test_toggle_trunk(monkeypatch): @pytest.mark.asyncio async def test_toggle_frunk(monkeypatch): """Test toggle frunk.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.toggle_frunk() is None @@ -466,11 +446,10 @@ async def test_toggle_frunk(monkeypatch): @pytest.mark.asyncio async def test_trigger_homelink(monkeypatch): """Test unlock.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.trigger_homelink() is None @@ -479,11 +458,10 @@ async def test_trigger_homelink(monkeypatch): @pytest.mark.asyncio async def test_unlock(monkeypatch): """Test unlock.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._vehicle_data = _mock.data_request_vehicle_by_vin() - _controller.generate_car_objects() + await _controller.generate_car_objects() _car = _controller.cars[VIN] assert await _car.unlock() is None diff --git a/tests/unit_tests/test_energy.py b/tests/unit_tests/test_energy.py index d054ef36..674e2b53 100644 --- a/tests/unit_tests/test_energy.py +++ b/tests/unit_tests/test_energy.py @@ -21,7 +21,7 @@ async def test_energysite_setup(monkeypatch): TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller.generate_energysite_objects() + await _controller.generate_energysite_objects() solar_site = _controller.energysites[12345] powerwall_site = _controller.energysites[67890] @@ -34,12 +34,10 @@ async def test_energysite_setup(monkeypatch): @pytest.mark.asyncio async def test_solar_site(monkeypatch): """Test SolarSite class.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - # Add site_data since we're not mocking Controller.update() - _controller._site_data = {12345: _mock.data_request_site_data()} - _controller.generate_energysite_objects() + await _controller.generate_energysite_objects() _solar_site = _controller.energysites[12345] @@ -63,13 +61,10 @@ async def test_solar_site(monkeypatch): @pytest.mark.asyncio async def test_powerwall_site(monkeypatch): """Test PowerwallSite class.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - # Add battery_data and battery_summary since we're not mocking Controller.update() - _controller._battery_data = {67890: _mock.data_request_battery_data()} - _controller._battery_summary = {67890: _mock.data_request_battery_summary()} - _controller.generate_energysite_objects() + await _controller.generate_energysite_objects() _solar_powerwall_site = _controller.energysites[67890] @@ -127,12 +122,10 @@ async def test_energysite_with_no_name(monkeypatch): @pytest.mark.asyncio async def test_set_operation_mode(monkeypatch): """Test set operation mode.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._battery_data = {67890: _mock.data_request_battery_data()} - _controller._battery_summary = {67890: _mock.data_request_battery_summary()} - _controller.generate_energysite_objects() + await _controller.generate_energysite_objects() _energysite = _controller.energysites[67890] assert await _energysite.set_operation_mode("autonomous") is None @@ -141,12 +134,10 @@ async def test_set_operation_mode(monkeypatch): @pytest.mark.asyncio async def test_set_reserve_percent(monkeypatch): """Test set reserve percent.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._battery_data = {67890: _mock.data_request_battery_data()} - _controller._battery_summary = {67890: _mock.data_request_battery_summary()} - _controller.generate_energysite_objects() + await _controller.generate_energysite_objects() _energysite = _controller.energysites[67890] assert await _energysite.set_reserve_percent(10) is None @@ -155,12 +146,10 @@ async def test_set_reserve_percent(monkeypatch): @pytest.mark.asyncio async def test_set_grid_charging(monkeypatch): """Test set grid charging.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._battery_data = {67890: _mock.data_request_battery_data()} - _controller._battery_summary = {67890: _mock.data_request_battery_summary()} - _controller.generate_energysite_objects() + await _controller.generate_energysite_objects() _energysite = _controller.energysites[67890] assert await _energysite.set_grid_charging(True) is None @@ -169,12 +158,10 @@ async def test_set_grid_charging(monkeypatch): @pytest.mark.asyncio async def test_set_export_rule(monkeypatch): """Test set export rule.""" - _mock = TeslaMock(monkeypatch) + TeslaMock(monkeypatch) _controller = Controller(None) await _controller.connect() - _controller._battery_data = {67890: _mock.data_request_battery_data()} - _controller._battery_summary = {67890: _mock.data_request_battery_summary()} - _controller.generate_energysite_objects() + await _controller.generate_energysite_objects() _energysite = _controller.energysites[67890] assert await _energysite.set_export_rule("pv_only") is None From b9fc3c1bd89f0c8c73b9929b7f27cb9c6b6e8057 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sat, 24 Sep 2022 18:14:41 -0700 Subject: [PATCH 73/84] Return empty dict when car asleep --- teslajsonpy/car.py | 228 +++++++++++++++++++++++++------------- teslajsonpy/controller.py | 25 +++-- 2 files changed, 165 insertions(+), 88 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 9beedd6b..947df3fb 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -36,8 +36,13 @@ def __init__(self, car: dict, controller, vehicle_data: dict) -> None: self._car = car self._controller = controller self._vehicle_data = vehicle_data + self._charge_state = vehicle_data.get("charge_state") + self._climate_state = vehicle_data.get("climate_state") + self._drive_state = vehicle_data.get("drive_state") + self._gui_settings = vehicle_data.get("gui_settings") + self._vehicle_config = vehicle_data.get("vehicle_config") + self._vehicle_state = vehicle_data.get("vehicle_state") - self._previous_charging_state = self.charging_state self._previous_driver_temp = self.driver_temp_setting self._previous_fan_status = self.fan_status self._previous_passenger_temp = self.passenger_temp_setting @@ -69,23 +74,29 @@ def vin(self) -> str: @property def data_available(self) -> bool: - """Return if data is available.""" - return self._vehicle_data != {} + """Return if data from VEHICLE_DATA endpoint is available.""" + # self._vehicle_data gets updated with some data from VEHICLE_LIST endpoint + # Only return True if data specifically from VEHICLE_DATA endpoint is available + if self._vehicle_config: + return True @property def battery_level(self) -> float: """Return car battery level.""" - return self._vehicle_data.get("charge_state").get("battery_level") + if self._charge_state: + return self._charge_state.get("battery_level") @property def battery_range(self) -> float: """Return car battery range.""" - return self._vehicle_data.get("charge_state").get("battery_range") + if self._charge_state: + return self._charge_state.get("battery_range") @property def cabin_overheat_protection(self) -> str: """Return cabin overheat protection.""" - return self._vehicle_data.get("climate_state").get("cabin_overheat_protection") + if self._climate_state: + return self._climate_state.get("cabin_overheat_protection") @property def car_type(self) -> str: @@ -95,22 +106,26 @@ def car_type(self) -> str: @property def car_version(self) -> str: """Return installed car software version.""" - return self._vehicle_data.get("vehicle_state").get("car_version") + if self._vehicle_state: + return self._vehicle_state.get("car_version") @property def charger_actual_current(self) -> int: """Return charger actual current.""" - return self._vehicle_data.get("charge_state").get("charger_actual_current") + if self._charge_state: + return self._charge_state.get("charger_actual_current") @property def charge_current_request(self) -> int: """Return charge current request.""" - return self._vehicle_data.get("charge_state").get("charge_current_request") + if self._charge_state: + return self._charge_state.get("charge_current_request") @property def charge_current_request_max(self) -> int: """Return charge current request max.""" - return self._vehicle_data.get("charge_state").get("charge_current_request_max") + if self._charge_state: + return self._charge_state.get("charge_current_request_max") @property def charge_port_latch(self) -> str: @@ -120,52 +135,62 @@ def charge_port_latch(self) -> str: str: Engaged Other states? """ - return self._vehicle_data.get("charge_state").get("charge_port_latch") + if self._charge_state: + return self._charge_state.get("charge_port_latch") @property def charge_energy_added(self) -> float: """Return charge energy added.""" - return self._vehicle_data.get("charge_state").get("charge_energy_added") + if self._charge_state: + return self._charge_state.get("charge_energy_added") @property def charge_limit_soc(self) -> int: """Return charge limit soc.""" - return self._vehicle_data.get("charge_state").get("charge_limit_soc") + if self._charge_state: + return self._charge_state.get("charge_limit_soc") @property def charge_limit_soc_max(self) -> int: """Return charge limit soc max.""" - return self._vehicle_data.get("charge_state").get("charge_limit_soc_max") + if self._charge_state: + return self._charge_state.get("charge_limit_soc_max") @property def charge_limit_soc_min(self) -> int: """Return charge limit soc min.""" - return self._vehicle_data.get("charge_state").get("charge_limit_soc_min") + if self._charge_state: + return self._charge_state.get("charge_limit_soc_min") @property def charge_miles_added_ideal(self) -> float: """Return charge ideal miles added.""" - return self._vehicle_data.get("charge_state").get("charge_miles_added_ideal") + if self._charge_state: + return self._charge_state.get("charge_miles_added_ideal") @property def charge_miles_added_rated(self) -> float: """Return charge rated miles added.""" - return self._vehicle_data.get("charge_state").get("charge_miles_added_rated") + if self._charge_state: + return self._charge_state.get("charge_miles_added_rated") @property def charger_phases(self) -> int: """Return charger phase.""" - return self._vehicle_data.get("charge_state").get("charger_phases") + if self._charge_state: + return self._charge_state.get("charger_phases") @property def charger_power(self) -> int: """Return charger power.""" - return self._vehicle_data.get("charge_state").get("charger_power") + if self._charge_state: + return self._charge_state.get("charger_power") @property def charge_rate(self) -> str: """Return charge rate.""" - return self._vehicle_data.get("charge_state").get("charge_rate") + if self._charge_state: + return self._charge_state.get("charge_rate") @property def charging_state(self) -> str: @@ -174,18 +199,18 @@ def charging_state(self) -> str: Returns str: Charging, Stopped, Complete, Disconnected, NoPower """ - current_charging_state = self._vehicle_data.get("charge_state").get( - "charging_state" - ) - # Tesla API returns None when car is sleeping; use previous reported state - if current_charging_state: - return current_charging_state - return self._previous_charging_state + if self._charge_state: + charging_state = self._charge_state.get("charging_state") + states = ["Charging", "Stopped", "Complete", "Disconnected"] + if self._charge_state.get("charging_state") not in states: + _LOGGER.warn("Charging state is %s", charging_state) + return self._charge_state.get("charging_state") @property def charger_voltage(self) -> int: """Return charger voltage.""" - return self._vehicle_data.get("charge_state").get("charger_voltage") + if self._charge_state: + return self._charge_state.get("charger_voltage") @property def climate_keeper_mode(self) -> str: @@ -196,12 +221,14 @@ def climate_keeper_mode(self) -> str: Not supported on all Tesla models. """ - return self._vehicle_data.get("climate_state").get("climate_keeper_mode") + if self._climate_state: + return self._climate_state.get("climate_keeper_mode") @property def conn_charge_cable(self) -> str: """Return charge cable connection.""" - return self._vehicle_data.get("charge_state").get("conn_charge_cable") + if self._charge_state: + return self._charge_state.get("charger_voltage") @property def defrost_mode(self) -> int: @@ -210,82 +237,98 @@ def defrost_mode(self) -> int: Returns int: 2 (on), 0 (off) """ - return self._vehicle_data.get("climate_state").get("defrost_mode", 0) + if self._climate_state: + return self._climate_state.get("defrost_mode", 0) @property def driver_temp_setting(self) -> float: """Return driver temperature setting.""" - return self._vehicle_data.get("climate_state").get("driver_temp_setting") + if self._climate_state: + return self._climate_state.get("driver_temp_setting") @property def fan_status(self) -> int: """Return fan status setting.""" - return self._vehicle_data.get("climate_state").get("fan_status") + if self._climate_state: + return self._climate_state.get("fan_status") @property def fast_charger_present(self) -> bool: """Return fast charger present.""" - return self._vehicle_data.get("charge_state").get("fast_charger_present") + if self._charge_state: + return self._charge_state.get("fast_charger_present") @property def fast_charger_brand(self) -> str: """Return fast charger brand.""" - return self._vehicle_data.get("charge_state").get("fast_charger_brand") + if self._charge_state: + return self._charge_state.get("fast_charger_brand") @property def fast_charger_type(self) -> str: """Return fast charger type.""" - return self._vehicle_data.get("charge_state").get("fast_charger_type") + if self._charge_state: + return self._charge_state.get("fast_charger_type") @property def gui_distance_units(self) -> str: """Return gui distance units.""" - return self._vehicle_data.get("gui_settings").get("gui_distance_units") + if self._gui_settings: + return self._gui_settings.get("gui_distance_units") @property def gui_range_display(self) -> str: """Return range display.""" - return self._vehicle_data.get("gui_settings").get("gui_range_display") + if self._gui_settings: + return self._gui_settings.get("gui_range_display") @property def heading(self) -> int: """Return heading.""" - return self._vehicle_data.get("drive_state").get("heading") + if self._drive_state: + return self._drive_state.get("heading") @property def homelink_device_count(self) -> int: """Return Homelink device count.""" - return self._vehicle_data.get("vehicle_state").get("homelink_device_count") + if self._vehicle_state: + return self._vehicle_state.get("homelink_device_count") @property def homelink_nearby(self) -> bool: """Return Homelink nearby.""" - return self._vehicle_data.get("vehicle_state").get("homelink_nearby") + if self._vehicle_state: + return self._vehicle_state.get("homelink_nearby") @property def ideal_battery_range(self) -> float: """Return car ideal battery range.""" - return self._vehicle_data.get("charge_state").get("ideal_battery_range") + if self._charge_state: + return self._charge_state.get("ideal_battery_range") @property def in_service(self) -> bool: """Return car in_service.""" - return self._vehicle_data.get("in_service") + if self.data_available: + return self._vehicle_data.get("in_service") @property def inside_temp(self) -> float: """Return inside temperature.""" - return self._vehicle_data.get("climate_state").get("inside_temp") + if self._climate_state: + return self._climate_state.get("inside_temp") @property def is_charge_port_door_open(self) -> bool: """Return charger port door open.""" - return self._vehicle_data.get("charge_state").get("charge_port_door_open") + if self._charge_state: + return self._charge_state.get("charge_port_door_open") @property def is_climate_on(self) -> bool: """Return climate is on.""" - return self._vehicle_data.get("climate_state").get("is_climate_on") + if self._climate_state: + return self._climate_state.get("is_climate_on") @property def is_frunk_closed(self) -> bool: @@ -294,9 +337,9 @@ def is_frunk_closed(self) -> bool: Returns bool: True (0), False (255) """ - response = self._vehicle_data.get("vehicle_state").get("ft") - - return True if response == 0 else False + if self._vehicle_state: + response = self._vehicle_state.get("ft") + return True if response == 0 else False @property def is_in_gear(self) -> bool: @@ -306,23 +349,25 @@ def is_in_gear(self) -> bool: @property def is_locked(self) -> bool: """Return car is locked.""" - return self._vehicle_data.get("vehicle_state").get("locked") + if self._vehicle_state: + return self._vehicle_state.get("locked") @property def is_steering_wheel_heater_on(self) -> bool: """Return steering wheel heater.""" - return self._vehicle_data.get("climate_state").get("steering_wheel_heater") + if self._climate_state: + return self._climate_state.get("steering_wheel_heater") @property def is_trunk_closed(self) -> bool: """Return car trunk is closed. Returns - bool: True (0), False (255) + bool: True (0), False (1-255) """ - response = self._vehicle_data.get("vehicle_state").get("rt") - - return True if response == 0 else False + if self._vehicle_state: + response = self._vehicle_state.get("rt") + return True if response == 0 else False @property def is_on(self) -> bool: @@ -332,72 +377,86 @@ def is_on(self) -> bool: @property def longitude(self) -> float: """Return longitude.""" - return self._vehicle_data.get("drive_state").get("longitude") + if self._drive_state: + return self._drive_state.get("longitude") @property def latitude(self) -> float: """Return latitude.""" - return self._vehicle_data.get("drive_state").get("latitude") + if self._drive_state: + return self._drive_state.get("latitude") @property def max_avail_temp(self) -> float: """Return max available temperature.""" - return self._vehicle_data.get("climate_state").get("max_avail_temp") + if self._climate_state: + return self._climate_state.get("max_avail_temp") @property def min_avail_temp(self) -> float: """Return min available temperature.""" - return self._vehicle_data.get("climate_state").get("min_avail_temp") + if self._climate_state: + return self._climate_state.get("min_avail_temp") @property def native_heading(self) -> int: """Return native heading.""" - return self._vehicle_data.get("drive_state").get("native_heading") + if self._drive_state: + return self._drive_state.get("native_heading") @property def native_location_supported(self) -> int: """Return native location supported.""" - return self._vehicle_data.get("drive_state").get("native_location_supported") + if self._drive_state: + return self._drive_state.get("native_location_supported") @property def native_longitude(self) -> float: """Return native longitude.""" - return self._vehicle_data.get("drive_state").get("native_longitude") + if self._drive_state: + return self._drive_state.get("native_longitude") @property def native_latitude(self) -> float: """Return native latitude.""" - return self._vehicle_data.get("drive_state").get("native_latitude") + if self._drive_state: + return self._drive_state.get("native_latitude") @property def native_type(self) -> float: """Return native type.""" - return self._vehicle_data.get("drive_state").get("native_type") + if self._drive_state: + return self._drive_state.get("native_type") @property def odometer(self) -> float: """Return odometer.""" - return self._vehicle_data.get("vehicle_state").get("odometer") + if self._vehicle_state: + return self._vehicle_state.get("odometer") @property def outside_temp(self) -> float: """Return outside temperature.""" - return self._vehicle_data.get("climate_state").get("outside_temp") + if self._climate_state: + return self._climate_state.get("outside_temp") @property def passenger_temp_setting(self) -> float: """Return passenger temperature setting.""" - return self._vehicle_data.get("climate_state").get("passenger_temp_setting") + if self._climate_state: + return self._climate_state.get("passenger_temp_setting") @property def power(self) -> int: """Return power.""" - return self._vehicle_data.get("drive_state").get("power") + if self._drive_state: + return self._drive_state.get("power") @property def powered_lift_gate(self) -> bool: """Return True if car has power lift gate.""" - return self._vehicle_data.get("vehicle_config").get("plg") + if self._vehicle_config: + return self._vehicle_config.get("plg") @property def rear_seat_heaters(self) -> int: @@ -406,37 +465,44 @@ def rear_seat_heaters(self) -> int: Returns int: 0 (no rear heated seats), int: ? (rear heated seats) """ - return self._vehicle_data.get("vehicle_config").get("rear_seat_heaters") + if self._vehicle_config: + return self._vehicle_config.get("rear_seat_heaters") @property def sentry_mode(self) -> bool: """Return sentry mode.""" - return self._vehicle_data.get("vehicle_state").get("sentry_mode") + if self._vehicle_state: + return self._vehicle_state.get("sentry_mode") @property def sentry_mode_available(self) -> bool: """Return sentry mode available.""" - return self._vehicle_data.get("vehicle_state").get("sentry_mode_available") + if self._vehicle_state: + return self._vehicle_state.get("sentry_mode_available") @property def shift_state(self) -> str: """Return shift state.""" - return self._vehicle_data.get("drive_state").get("shift_state") + if self._drive_state: + return self._drive_state.get("shift_state") @property def speed(self) -> float: """Return speed.""" - return self._vehicle_data.get("drive_state").get("speed") + if self._drive_state: + return self._drive_state.get("speed") @property def software_update(self) -> dict: """Return software update version information.""" - return self._vehicle_data.get("vehicle_state").get("software_update", {}) + if self._vehicle_state: + return self._vehicle_state.get("software_update", {}) @property def steering_wheel_heater(self) -> bool: """Return steering wheel heater option.""" - return self._vehicle_data.get("climate_state").get("steering_wheel_heater") + if self._climate_state: + return self._climate_state.get("steering_wheel_heater") @property def third_row_seats(self) -> str: @@ -445,12 +511,14 @@ def third_row_seats(self) -> str: Returns str: None """ - return self._vehicle_data.get("vehicle_config").get("third_row_seats") + if self._vehicle_config: + return self._vehicle_config.get("third_row_seats") @property def time_to_full_charge(self) -> float: """Return time to full charge.""" - return self._vehicle_data.get("charge_state").get("time_to_full_charge") + if self._charge_state: + return self._charge_state.get("time_to_full_charge") async def _send_command( self, name: str, *, path_vars: dict, wake_if_asleep: bool = False, **kwargs @@ -566,8 +634,8 @@ async def remote_seat_heater_request(self, level: int, seat_id: int) -> None: def get_seat_heater_status(self, seat_id: int) -> int: """Return status of seat heater for a given seat.""" seat_id = f"seat_heater_{SEAT_ID_MAP[seat_id]}" - - return self._vehicle_data.get("climate_state").get(seat_id) + if self.data_available: + return self._vehicle_data.get("climate_state").get(seat_id) async def schedule_software_update(self, offset_sec: Optional[int] = 0) -> None: """Send command to install software update.""" diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 271e81ab..0868a352 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -498,13 +498,21 @@ async def get_site_config(self, energysite_id: int) -> dict: @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) async def get_vehicle_data(self, vin: str, wake_if_asleep: bool = False) -> dict: """Get vehicle data json from TeslaAPI for a given vin.""" - return ( - await self.api( - "VEHICLE_DATA", - path_vars={"vehicle_id": self.__vin_id_map[vin]}, - wake_if_asleep=wake_if_asleep, - ) - )["response"] + try: + response = ( + await self.api( + "VEHICLE_DATA", + path_vars={"vehicle_id": self.__vin_id_map[vin]}, + wake_if_asleep=wake_if_asleep, + ) + )["response"] + + except TeslaException as ex: + if ex.message == "VEHICLE_UNAVAILABLE": + _LOGGER.debug("Vehicle asleep - data unavailable.") + return {} + + return response @backoff.on_exception(min_expo, httpx.RequestError, max_time=10, logger=__name__) async def get_site_data(self, energysite_id: int) -> dict: @@ -768,6 +776,7 @@ async def update( RetryLimitError """ + tasks = [] async def _get_and_process_car_data(vin: str) -> None: async with self.__lock[vin]: @@ -891,7 +900,7 @@ async def _get_and_process_battery_summary( # to update. car_id = self._update_id(car_id) car_vin = self._id_to_vin(car_id) - tasks = [] + for vin, online in self.get_car_online().items(): # If specific car_id provided, only update match if ( From 108ce57740cef42f00d666d2291b9fa9d1c493d6 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sat, 24 Sep 2022 18:20:40 -0700 Subject: [PATCH 74/84] Fix conn charge cable property --- teslajsonpy/car.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 947df3fb..97e57734 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -228,7 +228,7 @@ def climate_keeper_mode(self) -> str: def conn_charge_cable(self) -> str: """Return charge cable connection.""" if self._charge_state: - return self._charge_state.get("charger_voltage") + return self._charge_state.get("conn_charge_cable") @property def defrost_mode(self) -> int: From 19b58144c78e93211835db3f59dfd9e422375da8 Mon Sep 17 00:00:00 2001 From: shred86 Date: Sat, 24 Sep 2022 18:58:47 -0700 Subject: [PATCH 75/84] Fix for software property --- teslajsonpy/car.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 97e57734..181189af 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -496,7 +496,7 @@ def speed(self) -> float: def software_update(self) -> dict: """Return software update version information.""" if self._vehicle_state: - return self._vehicle_state.get("software_update", {}) + return self._vehicle_state.get("software_update") @property def steering_wheel_heater(self) -> bool: From 3fb12dc57786831ee200480bf046a57956948f63 Mon Sep 17 00:00:00 2001 From: shred86 Date: Mon, 26 Sep 2022 07:01:39 -0700 Subject: [PATCH 76/84] Fix for car properties --- teslajsonpy/car.py | 225 +++++++++++++++++---------------------------- 1 file changed, 84 insertions(+), 141 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 181189af..f68bd0c5 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -36,12 +36,6 @@ def __init__(self, car: dict, controller, vehicle_data: dict) -> None: self._car = car self._controller = controller self._vehicle_data = vehicle_data - self._charge_state = vehicle_data.get("charge_state") - self._climate_state = vehicle_data.get("climate_state") - self._drive_state = vehicle_data.get("drive_state") - self._gui_settings = vehicle_data.get("gui_settings") - self._vehicle_config = vehicle_data.get("vehicle_config") - self._vehicle_state = vehicle_data.get("vehicle_state") self._previous_driver_temp = self.driver_temp_setting self._previous_fan_status = self.fan_status @@ -77,26 +71,25 @@ def data_available(self) -> bool: """Return if data from VEHICLE_DATA endpoint is available.""" # self._vehicle_data gets updated with some data from VEHICLE_LIST endpoint # Only return True if data specifically from VEHICLE_DATA endpoint is available - if self._vehicle_config: + if self._vehicle_data.get("vehicle_config", {}): return True @property def battery_level(self) -> float: """Return car battery level.""" - if self._charge_state: - return self._charge_state.get("battery_level") + return self._vehicle_data.get("charge_state", {}).get("battery_level") @property def battery_range(self) -> float: """Return car battery range.""" - if self._charge_state: - return self._charge_state.get("battery_range") + return self._vehicle_data.get("charge_state", {}).get("battery_range") @property def cabin_overheat_protection(self) -> str: """Return cabin overheat protection.""" - if self._climate_state: - return self._climate_state.get("cabin_overheat_protection") + return self._vehicle_data.get("climate_state", {}).get( + "cabin_overheat_protection" + ) @property def car_type(self) -> str: @@ -106,26 +99,24 @@ def car_type(self) -> str: @property def car_version(self) -> str: """Return installed car software version.""" - if self._vehicle_state: - return self._vehicle_state.get("car_version") + return self._vehicle_data.get("vehicle_state", {}).get("car_version") @property def charger_actual_current(self) -> int: """Return charger actual current.""" - if self._charge_state: - return self._charge_state.get("charger_actual_current") + return self._vehicle_data.get("charge_state", {}).get("charger_actual_current") @property def charge_current_request(self) -> int: """Return charge current request.""" - if self._charge_state: - return self._charge_state.get("charge_current_request") + return self._vehicle_data.get("charge_state", {}).get("charge_current_request") @property def charge_current_request_max(self) -> int: """Return charge current request max.""" - if self._charge_state: - return self._charge_state.get("charge_current_request_max") + return self._vehicle_data.get("charge_state", {}).get( + "charge_current_request_max" + ) @property def charge_port_latch(self) -> str: @@ -133,84 +124,78 @@ def charge_port_latch(self) -> str: Returns str: Engaged - Other states? + Other states? """ - if self._charge_state: - return self._charge_state.get("charge_port_latch") + return self._vehicle_data.get("charge_state", {}).get("charge_port_latch") @property def charge_energy_added(self) -> float: """Return charge energy added.""" - if self._charge_state: - return self._charge_state.get("charge_energy_added") + return self._vehicle_data.get("charge_state", {}).get("charge_energy_added") @property def charge_limit_soc(self) -> int: """Return charge limit soc.""" - if self._charge_state: - return self._charge_state.get("charge_limit_soc") + return self._vehicle_data.get("charge_state", {}).get("charge_limit_soc") @property def charge_limit_soc_max(self) -> int: """Return charge limit soc max.""" - if self._charge_state: - return self._charge_state.get("charge_limit_soc_max") + return self._vehicle_data.get("charge_state", {}).get("charge_limit_soc_max") @property def charge_limit_soc_min(self) -> int: """Return charge limit soc min.""" - if self._charge_state: - return self._charge_state.get("charge_limit_soc_min") + return self._vehicle_data.get("charge_state", {}).get("charge_limit_soc_min") @property def charge_miles_added_ideal(self) -> float: """Return charge ideal miles added.""" - if self._charge_state: - return self._charge_state.get("charge_miles_added_ideal") + return self._vehicle_data.get("charge_state", {}).get( + "charge_miles_added_ideal" + ) @property def charge_miles_added_rated(self) -> float: """Return charge rated miles added.""" - if self._charge_state: - return self._charge_state.get("charge_miles_added_rated") + return self._vehicle_data.get("charge_state", {}).get( + "charge_miles_added_rated" + ) @property def charger_phases(self) -> int: """Return charger phase.""" - if self._charge_state: - return self._charge_state.get("charger_phases") + return self._vehicle_data.get("charge_state", {}).get("charger_phases") @property def charger_power(self) -> int: """Return charger power.""" - if self._charge_state: - return self._charge_state.get("charger_power") + return self._vehicle_data.get("charge_state", {}).get("charger_power") @property def charge_rate(self) -> str: """Return charge rate.""" - if self._charge_state: - return self._charge_state.get("charge_rate") + return self._vehicle_data.get("charge_state", {}).get("charge_rate") @property def charging_state(self) -> str: """Return charging state. Returns - str: Charging, Stopped, Complete, Disconnected, NoPower + str: Charging, Stopped, Complete, Disconnected, NoPower """ - if self._charge_state: - charging_state = self._charge_state.get("charging_state") - states = ["Charging", "Stopped", "Complete", "Disconnected"] - if self._charge_state.get("charging_state") not in states: - _LOGGER.warn("Charging state is %s", charging_state) - return self._charge_state.get("charging_state") + charging_state = self._vehicle_data.get("charge_state", {}).get( + "charging_state" + ) + states = ["Charging", "Stopped", "Complete", "Disconnected"] + if charging_state not in states: + _LOGGER.warn("Charging state is %s", charging_state) + return self._vehicle_data.get("charge_state", {}).get("charging_state") @property def charger_voltage(self) -> int: """Return charger voltage.""" - if self._charge_state: - return self._charge_state.get("charger_voltage") + return self._vehicle_data.get("charge_state", {}).get("charger_voltage") @property def climate_keeper_mode(self) -> str: @@ -221,14 +206,12 @@ def climate_keeper_mode(self) -> str: Not supported on all Tesla models. """ - if self._climate_state: - return self._climate_state.get("climate_keeper_mode") + return self._vehicle_data.get("climate_state", {}).get("climate_keeper_mode") @property def conn_charge_cable(self) -> str: """Return charge cable connection.""" - if self._charge_state: - return self._charge_state.get("conn_charge_cable") + return self._vehicle_data.get("charge_state", {}).get("conn_charge_cable") @property def defrost_mode(self) -> int: @@ -237,74 +220,62 @@ def defrost_mode(self) -> int: Returns int: 2 (on), 0 (off) """ - if self._climate_state: - return self._climate_state.get("defrost_mode", 0) + return self._vehicle_data.get("climate_state", {}).get("defrost_mode", 0) @property def driver_temp_setting(self) -> float: """Return driver temperature setting.""" - if self._climate_state: - return self._climate_state.get("driver_temp_setting") + return self._vehicle_data.get("climate_state", {}).get("driver_temp_setting") @property def fan_status(self) -> int: """Return fan status setting.""" - if self._climate_state: - return self._climate_state.get("fan_status") + return self._vehicle_data.get("climate_state", {}).get("fan_status") @property def fast_charger_present(self) -> bool: """Return fast charger present.""" - if self._charge_state: - return self._charge_state.get("fast_charger_present") + return self._vehicle_data.get("charge_state", {}).get("fast_charger_present") @property def fast_charger_brand(self) -> str: """Return fast charger brand.""" - if self._charge_state: - return self._charge_state.get("fast_charger_brand") + return self._vehicle_data.get("charge_state", {}).get("fast_charger_brand") @property def fast_charger_type(self) -> str: """Return fast charger type.""" - if self._charge_state: - return self._charge_state.get("fast_charger_type") + return self._vehicle_data.get("charge_state", {}).get("fast_charger_type") @property def gui_distance_units(self) -> str: """Return gui distance units.""" - if self._gui_settings: - return self._gui_settings.get("gui_distance_units") + return self._vehicle_data.get("gui_settings", {}).get("gui_distance_units") @property def gui_range_display(self) -> str: """Return range display.""" - if self._gui_settings: - return self._gui_settings.get("gui_range_display") + return self._vehicle_data.get("gui_settings", {}).get("gui_range_display") @property def heading(self) -> int: """Return heading.""" - if self._drive_state: - return self._drive_state.get("heading") + return self._vehicle_data.get("drive_state", {}).get("heading") @property def homelink_device_count(self) -> int: """Return Homelink device count.""" - if self._vehicle_state: - return self._vehicle_state.get("homelink_device_count") + return self._vehicle_data.get("vehicle_state", {}).get("homelink_device_count") @property def homelink_nearby(self) -> bool: """Return Homelink nearby.""" - if self._vehicle_state: - return self._vehicle_state.get("homelink_nearby") + return self._vehicle_data.get("vehicle_state", {}).get("homelink_nearby") @property def ideal_battery_range(self) -> float: """Return car ideal battery range.""" - if self._charge_state: - return self._charge_state.get("ideal_battery_range") + return self._vehicle_data.get("charge_state", {}).get("ideal_battery_range") @property def in_service(self) -> bool: @@ -315,20 +286,17 @@ def in_service(self) -> bool: @property def inside_temp(self) -> float: """Return inside temperature.""" - if self._climate_state: - return self._climate_state.get("inside_temp") + return self._vehicle_data.get("climate_state", {}).get("inside_temp") @property def is_charge_port_door_open(self) -> bool: """Return charger port door open.""" - if self._charge_state: - return self._charge_state.get("charge_port_door_open") + return self._vehicle_data.get("charge_state", {}).get("charge_port_door_open") @property def is_climate_on(self) -> bool: """Return climate is on.""" - if self._climate_state: - return self._climate_state.get("is_climate_on") + return self._vehicle_data.get("climate_state", {}).get("is_climate_on") @property def is_frunk_closed(self) -> bool: @@ -337,9 +305,8 @@ def is_frunk_closed(self) -> bool: Returns bool: True (0), False (255) """ - if self._vehicle_state: - response = self._vehicle_state.get("ft") - return True if response == 0 else False + response = self._vehicle_data.get("vehicle_state", {}).get("ft") + return True if response == 0 else False @property def is_in_gear(self) -> bool: @@ -349,14 +316,12 @@ def is_in_gear(self) -> bool: @property def is_locked(self) -> bool: """Return car is locked.""" - if self._vehicle_state: - return self._vehicle_state.get("locked") + return self._vehicle_data.get("vehicle_state", {}).get("locked") @property def is_steering_wheel_heater_on(self) -> bool: """Return steering wheel heater.""" - if self._climate_state: - return self._climate_state.get("steering_wheel_heater") + return self._vehicle_data.get("climate_state", {}).get("steering_wheel_heater") @property def is_trunk_closed(self) -> bool: @@ -365,9 +330,8 @@ def is_trunk_closed(self) -> bool: Returns bool: True (0), False (1-255) """ - if self._vehicle_state: - response = self._vehicle_state.get("rt") - return True if response == 0 else False + response = self._vehicle_data.get("vehicle_state", {}).get("rt") + return True if response == 0 else False @property def is_on(self) -> bool: @@ -377,86 +341,74 @@ def is_on(self) -> bool: @property def longitude(self) -> float: """Return longitude.""" - if self._drive_state: - return self._drive_state.get("longitude") + return self._vehicle_data.get("drive_state", {}).get("longitude") @property def latitude(self) -> float: """Return latitude.""" - if self._drive_state: - return self._drive_state.get("latitude") + return self._vehicle_data.get("drive_state", {}).get("latitude") @property def max_avail_temp(self) -> float: """Return max available temperature.""" - if self._climate_state: - return self._climate_state.get("max_avail_temp") + return self._vehicle_data.get("climate_state", {}).get("max_avail_temp") @property def min_avail_temp(self) -> float: """Return min available temperature.""" - if self._climate_state: - return self._climate_state.get("min_avail_temp") + return self._vehicle_data.get("climate_state", {}).get("min_avail_temp") @property def native_heading(self) -> int: """Return native heading.""" - if self._drive_state: - return self._drive_state.get("native_heading") + return self._vehicle_data.get("drive_state", {}).get("native_heading") @property def native_location_supported(self) -> int: """Return native location supported.""" - if self._drive_state: - return self._drive_state.get("native_location_supported") + return self._vehicle_data.get("drive_state", {}).get( + "native_location_supported" + ) @property def native_longitude(self) -> float: """Return native longitude.""" - if self._drive_state: - return self._drive_state.get("native_longitude") + return self._vehicle_data.get("drive_state", {}).get("native_longitude") @property def native_latitude(self) -> float: """Return native latitude.""" - if self._drive_state: - return self._drive_state.get("native_latitude") + return self._vehicle_data.get("drive_state", {}).get("native_latitude") @property def native_type(self) -> float: """Return native type.""" - if self._drive_state: - return self._drive_state.get("native_type") + return self._vehicle_data.get("drive_state", {}).get("native_type") @property def odometer(self) -> float: """Return odometer.""" - if self._vehicle_state: - return self._vehicle_state.get("odometer") + return self._vehicle_data.get("vehicle_state", {}).get("odometer") @property def outside_temp(self) -> float: """Return outside temperature.""" - if self._climate_state: - return self._climate_state.get("outside_temp") + return self._vehicle_data.get("climate_state", {}).get("outside_temp") @property def passenger_temp_setting(self) -> float: """Return passenger temperature setting.""" - if self._climate_state: - return self._climate_state.get("passenger_temp_setting") + return self._vehicle_data.get("climate_state", {}).get("passenger_temp_setting") @property def power(self) -> int: """Return power.""" - if self._drive_state: - return self._drive_state.get("power") + return self._vehicle_data.get("drive_state", {}).get("power") @property def powered_lift_gate(self) -> bool: """Return True if car has power lift gate.""" - if self._vehicle_config: - return self._vehicle_config.get("plg") + return self._vehicle_data.get("vehicle_config", {}).get("plg") @property def rear_seat_heaters(self) -> int: @@ -465,44 +417,37 @@ def rear_seat_heaters(self) -> int: Returns int: 0 (no rear heated seats), int: ? (rear heated seats) """ - if self._vehicle_config: - return self._vehicle_config.get("rear_seat_heaters") + return self._vehicle_data.get("vehicle_config", {}).get("rear_seat_heaters") @property def sentry_mode(self) -> bool: """Return sentry mode.""" - if self._vehicle_state: - return self._vehicle_state.get("sentry_mode") + return self._vehicle_data.get("vehicle_state", {}).get("sentry_mode") @property def sentry_mode_available(self) -> bool: """Return sentry mode available.""" - if self._vehicle_state: - return self._vehicle_state.get("sentry_mode_available") + return self._vehicle_data.get("vehicle_state", {}).get("sentry_mode_available") @property def shift_state(self) -> str: """Return shift state.""" - if self._drive_state: - return self._drive_state.get("shift_state") + return self._vehicle_data.get("drive_state", {}).get("shift_state") @property def speed(self) -> float: """Return speed.""" - if self._drive_state: - return self._drive_state.get("speed") + return self._vehicle_data.get("drive_state", {}).get("speed") @property def software_update(self) -> dict: """Return software update version information.""" - if self._vehicle_state: - return self._vehicle_state.get("software_update") + return self._vehicle_data.get("vehicle_state", {}).get("software_update") @property def steering_wheel_heater(self) -> bool: """Return steering wheel heater option.""" - if self._climate_state: - return self._climate_state.get("steering_wheel_heater") + return self._vehicle_data.get("climate_state", {}).get("steering_wheel_heater") @property def third_row_seats(self) -> str: @@ -511,14 +456,12 @@ def third_row_seats(self) -> str: Returns str: None """ - if self._vehicle_config: - return self._vehicle_config.get("third_row_seats") + return self._vehicle_data.get("vehicle_config", {}).get("third_row_seats") @property def time_to_full_charge(self) -> float: """Return time to full charge.""" - if self._charge_state: - return self._charge_state.get("time_to_full_charge") + return self._vehicle_data.get("charge_state", {}).get("time_to_full_charge") async def _send_command( self, name: str, *, path_vars: dict, wake_if_asleep: bool = False, **kwargs From ee18d47c4cdbbc04926a93846b82882b4c1ecfad Mon Sep 17 00:00:00 2001 From: shred86 Date: Mon, 26 Sep 2022 08:31:31 -0700 Subject: [PATCH 77/84] Removing temporary logging --- teslajsonpy/car.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index f68bd0c5..26268f14 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -183,13 +183,8 @@ def charging_state(self) -> str: Returns str: Charging, Stopped, Complete, Disconnected, NoPower + None: When car is asleep """ - charging_state = self._vehicle_data.get("charge_state", {}).get( - "charging_state" - ) - states = ["Charging", "Stopped", "Complete", "Disconnected"] - if charging_state not in states: - _LOGGER.warn("Charging state is %s", charging_state) return self._vehicle_data.get("charge_state", {}).get("charging_state") @property From 133aa24efdb9005730735b063e71cb2e479e507a Mon Sep 17 00:00:00 2001 From: shred86 Date: Mon, 10 Oct 2022 16:28:55 -0700 Subject: [PATCH 78/84] Update poetry --- poetry.lock | 697 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 410 insertions(+), 287 deletions(-) diff --git a/poetry.lock b/poetry.lock index b69b4887..e9071bfc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,6 +1,6 @@ [[package]] name = "aiohttp" -version = "3.8.1" +version = "3.8.3" description = "Async http client/server framework (asyncio)" category = "main" optional = false @@ -18,7 +18,7 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} yarl = ">=1.0,<2.0" [package.extras] -speedups = ["aiodns", "brotli", "cchardet"] +speedups = ["Brotli", "aiodns", "cchardet"] [[package]] name = "aiosignal" @@ -53,8 +53,8 @@ sniffio = ">=1.1" typing-extensions = {version = "*", markers = "python_version < \"3.8\""} [package.extras] -doc = ["packaging", "sphinx-rtd-theme", "sphinx-autodoc-typehints (>=1.2.0)"] -test = ["coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "contextlib2", "uvloop (<0.15)", "mock (>=4)", "uvloop (>=0.15)"] +doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] trio = ["trio (>=0.16)"] [[package]] @@ -67,6 +67,7 @@ python-versions = ">=3.6.2" [package.dependencies] lazy-object-proxy = ">=1.4.0" +setuptools = ">=20.0" typed-ast = {version = ">=1.4.0,<2.0", markers = "implementation_name == \"cpython\" and python_version < \"3.8\""} typing-extensions = {version = ">=3.10", markers = "python_version < \"3.10\""} wrapt = ">=1.11,<2" @@ -99,10 +100,10 @@ optional = false python-versions = ">=3.5" [package.extras] -dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"] -docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"] -tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "zope.interface", "cloudpickle"] -tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "mypy (>=0.900,!=0.940)", "pytest-mypy-plugins", "cloudpickle"] +dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] +docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] +tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] +tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] [[package]] name = "authcaptureproxy" @@ -145,7 +146,7 @@ pytz = ">=2015.7" [[package]] name = "backoff" -version = "2.1.2" +version = "2.2.1" description = "Function decoration for backoff and retry" category = "main" optional = false @@ -168,11 +169,11 @@ lxml = ["lxml"] [[package]] name = "black" -version = "22.8.0" +version = "22.10.0" description = "The uncompromising code formatter." category = "dev" optional = false -python-versions = ">=3.6.2" +python-versions = ">=3.7" [package.dependencies] click = ">=8.0.0" @@ -191,7 +192,7 @@ uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" -version = "2022.6.15" +version = "2022.9.24" description = "Python package for providing Mozilla's CA Bundle." category = "main" optional = false @@ -206,7 +207,7 @@ optional = false python-versions = ">=3.6.0" [package.extras] -unicode_backport = ["unicodedata2"] +unicode-backport = ["unicodedata2"] [[package]] name = "click" @@ -230,7 +231,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" [[package]] name = "coverage" -version = "6.4.4" +version = "6.5.0" description = "Code coverage measurement for Python" category = "dev" optional = false @@ -344,14 +345,14 @@ rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} sniffio = "*" [package.extras] -brotli = ["brotlicffi", "brotli"] -cli = ["click (>=8.0.0,<9.0.0)", "rich (>=10,<13)", "pygments (>=2.0.0,<3.0.0)"] +brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] http2 = ["h2 (>=3,<5)"] socks = ["socksio (>=1.0.0,<2.0.0)"] [[package]] name = "idna" -version = "3.3" +version = "3.4" description = "Internationalized Domain Names in Applications (IDNA)" category = "main" optional = false @@ -367,7 +368,7 @@ python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" [[package]] name = "importlib-metadata" -version = "4.12.0" +version = "5.0.0" description = "Read metadata from Python packages" category = "main" optional = false @@ -378,9 +379,9 @@ typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} zipp = ">=0.5" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] perf = ["ipython"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"] +testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] [[package]] name = "iniconfig" @@ -399,10 +400,10 @@ optional = false python-versions = ">=3.6.1,<4.0" [package.extras] -pipfile_deprecated_finder = ["pipreqs", "requirementslib"] -requirements_deprecated_finder = ["pipreqs", "pip-api"] colors = ["colorama (>=0.4.3,<0.5.0)"] +pipfile-deprecated-finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] +requirements-deprecated-finder = ["pip-api", "pipreqs"] [[package]] name = "jinja2" @@ -472,11 +473,11 @@ python-versions = ">=3.7" [[package]] name = "mypy" -version = "0.971" +version = "0.982" description = "Optional static typing for Python" category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" [package.dependencies] mypy-extensions = ">=0.4.3" @@ -525,8 +526,8 @@ optional = false python-versions = ">=3.7" [package.extras] -docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)", "sphinx (>=4)"] -test = ["appdirs (==1.4.4)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)", "pytest (>=6)"] +docs = ["furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx (>=4)", "sphinx-autodoc-typehints (>=1.12)"] +test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] [[package]] name = "pluggy" @@ -540,8 +541,8 @@ python-versions = ">=3.6" importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} [package.extras] -testing = ["pytest-benchmark", "pytest"] -dev = ["tox", "pre-commit"] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] [[package]] name = "py" @@ -622,7 +623,7 @@ optional = false python-versions = ">=3.6.8" [package.extras] -diagrams = ["railroad-diagrams", "jinja2"] +diagrams = ["jinja2", "railroad-diagrams"] [[package]] name = "pytest" @@ -658,11 +659,11 @@ pytest = ">=6.1.0" typing-extensions = {version = ">=3.7.2", markers = "python_version < \"3.8\""} [package.extras] -testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)", "flaky (>=3.5.0)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] +testing = ["coverage (>=6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (>=0.931)", "pytest-trio (>=0.7.0)"] [[package]] name = "pytest-cov" -version = "3.0.0" +version = "4.0.0" description = "Pytest plugin for measuring coverage." category = "dev" optional = false @@ -673,11 +674,11 @@ coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] -testing = ["virtualenv", "pytest-xdist", "six", "process-tests", "hunter", "fields"] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] [[package]] name = "pytz" -version = "2022.2.1" +version = "2022.4" description = "World timezone definitions, modern and historical" category = "dev" optional = false @@ -707,7 +708,7 @@ urllib3 = ">=1.21.1,<1.27" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use_chardet_on_py3 = ["chardet (>=3.0.2,<6)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "rfc3986" @@ -723,6 +724,19 @@ idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} [package.extras] idna2008 = ["idna"] +[[package]] +name = "setuptools" +version = "65.4.1" +description = "Easily download, build, install, upgrade, and uninstall Python packages" +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] +testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "mock", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] +testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] + [[package]] name = "six" version = "1.16.0" @@ -757,7 +771,7 @@ python-versions = ">=3.6" [[package]] name = "sphinx" -version = "5.1.1" +version = "5.2.3" description = "Python documentation generator" category = "dev" optional = false @@ -765,16 +779,16 @@ python-versions = ">=3.6" [package.dependencies] alabaster = ">=0.7,<0.8" -babel = ">=1.3" -colorama = {version = ">=0.3.5", markers = "sys_platform == \"win32\""} +babel = ">=2.9" +colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} docutils = ">=0.14,<0.20" -imagesize = "*" -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} -Jinja2 = ">=2.3" -packaging = "*" -Pygments = ">=2.0" +imagesize = ">=1.3" +importlib-metadata = {version = ">=4.8", markers = "python_version < \"3.10\""} +Jinja2 = ">=3.0" +packaging = ">=21.0" +Pygments = ">=2.12" requests = ">=2.5.0" -snowballstemmer = ">=1.1" +snowballstemmer = ">=2.0" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" @@ -784,12 +798,12 @@ sphinxcontrib-serializinghtml = ">=1.1.5" [package.extras] docs = ["sphinxcontrib-websupport"] -lint = ["flake8 (>=3.5.0)", "flake8-comprehensions", "flake8-bugbear", "isort", "mypy (>=0.971)", "sphinx-lint", "docutils-stubs", "types-typed-ast", "types-requests"] -test = ["pytest (>=4.6)", "html5lib", "cython", "typed-ast"] +lint = ["docutils-stubs", "flake8 (>=3.5.0)", "flake8-bugbear", "flake8-comprehensions", "flake8-simplify", "isort", "mypy (>=0.981)", "sphinx-lint", "types-requests", "types-typed-ast"] +test = ["cython", "html5lib", "pytest (>=4.6)", "typed_ast"] [[package]] name = "sphinx-autoapi" -version = "1.9.0" +version = "2.0.0" description = "Sphinx API documentation generator" category = "dev" optional = false @@ -799,7 +813,7 @@ python-versions = ">=3.7" astroid = ">=2.7" Jinja2 = "*" PyYAML = "*" -sphinx = ">=3.0" +sphinx = ">=4.0" unidecode = "*" [package.extras] @@ -819,8 +833,8 @@ python-versions = ">=3.6" sphinx = ">=1.8" [package.extras] -rtd = ["sphinx-book-theme", "myst-nb", "ipython", "sphinx"] -code_style = ["pre-commit (==2.12.1)"] +code-style = ["pre-commit (==2.12.1)"] +rtd = ["ipython", "myst-nb", "sphinx", "sphinx-book-theme"] [[package]] name = "sphinx-rtd-theme" @@ -845,8 +859,8 @@ optional = false python-versions = ">=3.5" [package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] -lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-devhelp" @@ -857,8 +871,8 @@ optional = false python-versions = ">=3.5" [package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] -lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-htmlhelp" @@ -869,8 +883,8 @@ optional = false python-versions = ">=3.6" [package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] test = ["html5lib", "pytest"] -lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-jsmath" @@ -881,7 +895,7 @@ optional = false python-versions = ">=3.5" [package.extras] -test = ["mypy", "flake8", "pytest"] +test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" @@ -892,8 +906,8 @@ optional = false python-versions = ">=3.5" [package.extras] +lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] -lint = ["docutils-stubs", "mypy", "flake8"] [[package]] name = "sphinxcontrib-serializinghtml" @@ -904,17 +918,9 @@ optional = false python-versions = ">=3.5" [package.extras] -lint = ["flake8", "mypy", "docutils-stubs"] +lint = ["docutils-stubs", "flake8", "mypy"] test = ["pytest"] -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - [[package]] name = "tomli" version = "2.0.1" @@ -925,7 +931,7 @@ python-versions = ">=3.7" [[package]] name = "tox" -version = "3.25.1" +version = "3.26.0" description = "tox is a generic virtualenv management and test command line tool" category = "dev" optional = false @@ -939,12 +945,12 @@ packaging = ">=14" pluggy = ">=0.12.0" py = ">=1.4.17" six = ">=1.14.0" -toml = ">=0.9.4" +tomli = {version = ">=2.0.1", markers = "python_version >= \"3.7\" and python_version < \"3.11\""} virtualenv = ">=16.0.0,<20.0.0 || >20.0.0,<20.0.1 || >20.0.1,<20.0.2 || >20.0.2,<20.0.3 || >20.0.3,<20.0.4 || >20.0.4,<20.0.5 || >20.0.5,<20.0.6 || >20.0.6,<20.0.7 || >20.0.7" [package.extras] docs = ["pygments-github-lexers (>=0.0.5)", "sphinx (>=2.0.0)", "sphinxcontrib-autoprogram (>=0.1.5)", "towncrier (>=18.5.0)"] -testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)", "psutil (>=5.6.1)", "pathlib2 (>=2.3.3)"] +testing = ["flaky (>=3.4.0)", "freezegun (>=0.3.11)", "pathlib2 (>=2.3.3)", "psutil (>=5.6.1)", "pytest (>=4.0.0)", "pytest-cov (>=2.5.1)", "pytest-mock (>=1.10.0)", "pytest-randomly (>=1.0.0)"] [[package]] name = "typed-ast" @@ -966,14 +972,14 @@ python-versions = ">=3.6" click = ">=7.1.1,<9.0.0" [package.extras] -test = ["rich (>=10.11.0,<13.0.0)", "isort (>=5.0.6,<6.0.0)", "black (>=22.3.0,<23.0.0)", "mypy (==0.910)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "coverage (>=5.2,<6.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest (>=4.4.0,<5.4.0)", "shellingham (>=1.3.0,<2.0.0)"] -doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "mkdocs (>=1.1.2,<2.0.0)"] -dev = ["pre-commit (>=2.17.0,<3.0.0)", "flake8 (>=3.8.3,<4.0.0)", "autoflake (>=1.3.1,<2.0.0)"] -all = ["rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)", "colorama (>=0.4.3,<0.5.0)"] +all = ["colorama (>=0.4.3,<0.5.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] +dev = ["autoflake (>=1.3.1,<2.0.0)", "flake8 (>=3.8.3,<4.0.0)", "pre-commit (>=2.17.0,<3.0.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-material (>=8.1.4,<9.0.0)"] +test = ["black (>=22.3.0,<23.0.0)", "coverage (>=5.2,<6.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.910)", "pytest (>=4.4.0,<5.4.0)", "pytest-cov (>=2.10.0,<3.0.0)", "pytest-sugar (>=0.9.4,<0.10.0)", "pytest-xdist (>=1.32.0,<2.0.0)", "rich (>=10.11.0,<13.0.0)", "shellingham (>=1.3.0,<2.0.0)"] [[package]] name = "typing-extensions" -version = "4.3.0" +version = "4.4.0" description = "Backported and Experimental Type Hints for Python 3.7+" category = "main" optional = false @@ -981,7 +987,7 @@ python-versions = ">=3.7" [[package]] name = "unidecode" -version = "1.3.4" +version = "1.3.6" description = "ASCII transliterations of Unicode text" category = "dev" optional = false @@ -996,13 +1002,13 @@ optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*, <4" [package.extras] -brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"] -secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "urllib3-secure-extra", "ipaddress"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] +secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] [[package]] name = "virtualenv" -version = "20.16.4" +version = "20.16.5" description = "Virtual Python Environment builder" category = "dev" optional = false @@ -1041,15 +1047,15 @@ typing-extensions = {version = ">=3.7.4", markers = "python_version < \"3.8\""} [[package]] name = "zipp" -version = "3.8.1" +version = "3.9.0" description = "Backport of pathlib-compatible object wrapper for zip files" category = "main" optional = false python-versions = ">=3.7" [package.extras] -docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"] -testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.3)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)"] +docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] +testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] [metadata] lock-version = "1.1" @@ -1058,78 +1064,93 @@ content-hash = "3dd606e16b92391188ccfe2345783d1d0201e33175abf25980e6913cc8959906 [metadata.files] aiohttp = [ - {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1ed0b6477896559f17b9eaeb6d38e07f7f9ffe40b9f0f9627ae8b9926ae260a8"}, - {file = "aiohttp-3.8.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7dadf3c307b31e0e61689cbf9e06be7a867c563d5a63ce9dca578f956609abf8"}, - {file = "aiohttp-3.8.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a79004bb58748f31ae1cbe9fa891054baaa46fb106c2dc7af9f8e3304dc30316"}, - {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:12de6add4038df8f72fac606dff775791a60f113a725c960f2bab01d8b8e6b15"}, - {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6f0d5f33feb5f69ddd57a4a4bd3d56c719a141080b445cbf18f238973c5c9923"}, - {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eaba923151d9deea315be1f3e2b31cc39a6d1d2f682f942905951f4e40200922"}, - {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:099ebd2c37ac74cce10a3527d2b49af80243e2a4fa39e7bce41617fbc35fa3c1"}, - {file = "aiohttp-3.8.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:2e5d962cf7e1d426aa0e528a7e198658cdc8aa4fe87f781d039ad75dcd52c516"}, - {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fa0ffcace9b3aa34d205d8130f7873fcfefcb6a4dd3dd705b0dab69af6712642"}, - {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61bfc23df345d8c9716d03717c2ed5e27374e0fe6f659ea64edcd27b4b044cf7"}, - {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:31560d268ff62143e92423ef183680b9829b1b482c011713ae941997921eebc8"}, - {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:01d7bdb774a9acc838e6b8f1d114f45303841b89b95984cbb7d80ea41172a9e3"}, - {file = "aiohttp-3.8.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97ef77eb6b044134c0b3a96e16abcb05ecce892965a2124c566af0fd60f717e2"}, - {file = "aiohttp-3.8.1-cp310-cp310-win32.whl", hash = "sha256:c2aef4703f1f2ddc6df17519885dbfa3514929149d3ff900b73f45998f2532fa"}, - {file = "aiohttp-3.8.1-cp310-cp310-win_amd64.whl", hash = "sha256:713ac174a629d39b7c6a3aa757b337599798da4c1157114a314e4e391cd28e32"}, - {file = "aiohttp-3.8.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:473d93d4450880fe278696549f2e7aed8cd23708c3c1997981464475f32137db"}, - {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99b5eeae8e019e7aad8af8bb314fb908dd2e028b3cdaad87ec05095394cce632"}, - {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3af642b43ce56c24d063325dd2cf20ee012d2b9ba4c3c008755a301aaea720ad"}, - {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c3630c3ef435c0a7c549ba170a0633a56e92629aeed0e707fec832dee313fb7a"}, - {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4a4a4e30bf1edcad13fb0804300557aedd07a92cabc74382fdd0ba6ca2661091"}, - {file = "aiohttp-3.8.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:6f8b01295e26c68b3a1b90efb7a89029110d3a4139270b24fda961893216c440"}, - {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:a25fa703a527158aaf10dafd956f7d42ac6d30ec80e9a70846253dd13e2f067b"}, - {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:5bfde62d1d2641a1f5173b8c8c2d96ceb4854f54a44c23102e2ccc7e02f003ec"}, - {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:51467000f3647d519272392f484126aa716f747859794ac9924a7aafa86cd411"}, - {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:03a6d5349c9ee8f79ab3ff3694d6ce1cfc3ced1c9d36200cb8f08ba06bd3b782"}, - {file = "aiohttp-3.8.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:102e487eeb82afac440581e5d7f8f44560b36cf0bdd11abc51a46c1cd88914d4"}, - {file = "aiohttp-3.8.1-cp36-cp36m-win32.whl", hash = "sha256:4aed991a28ea3ce320dc8ce655875e1e00a11bdd29fe9444dd4f88c30d558602"}, - {file = "aiohttp-3.8.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b0e20cddbd676ab8a64c774fefa0ad787cc506afd844de95da56060348021e96"}, - {file = "aiohttp-3.8.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:37951ad2f4a6df6506750a23f7cbabad24c73c65f23f72e95897bb2cecbae676"}, - {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c23b1ad869653bc818e972b7a3a79852d0e494e9ab7e1a701a3decc49c20d51"}, - {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:15b09b06dae900777833fe7fc4b4aa426556ce95847a3e8d7548e2d19e34edb8"}, - {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:477c3ea0ba410b2b56b7efb072c36fa91b1e6fc331761798fa3f28bb224830dd"}, - {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:2f2f69dca064926e79997f45b2f34e202b320fd3782f17a91941f7eb85502ee2"}, - {file = "aiohttp-3.8.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ef9612483cb35171d51d9173647eed5d0069eaa2ee812793a75373447d487aa4"}, - {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6d69f36d445c45cda7b3b26afef2fc34ef5ac0cdc75584a87ef307ee3c8c6d00"}, - {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:55c3d1072704d27401c92339144d199d9de7b52627f724a949fc7d5fc56d8b93"}, - {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d00268fcb9f66fbcc7cd9fe423741d90c75ee029a1d15c09b22d23253c0a44"}, - {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:07b05cd3305e8a73112103c834e91cd27ce5b4bd07850c4b4dbd1877d3f45be7"}, - {file = "aiohttp-3.8.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:c34dc4958b232ef6188c4318cb7b2c2d80521c9a56c52449f8f93ab7bc2a8a1c"}, - {file = "aiohttp-3.8.1-cp37-cp37m-win32.whl", hash = "sha256:d2f9b69293c33aaa53d923032fe227feac867f81682f002ce33ffae978f0a9a9"}, - {file = "aiohttp-3.8.1-cp37-cp37m-win_amd64.whl", hash = "sha256:6ae828d3a003f03ae31915c31fa684b9890ea44c9c989056fea96e3d12a9fa17"}, - {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:0c7ebbbde809ff4e970824b2b6cb7e4222be6b95a296e46c03cf050878fc1785"}, - {file = "aiohttp-3.8.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:8b7ef7cbd4fec9a1e811a5de813311ed4f7ac7d93e0fda233c9b3e1428f7dd7b"}, - {file = "aiohttp-3.8.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c3d6a4d0619e09dcd61021debf7059955c2004fa29f48788a3dfaf9c9901a7cd"}, - {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:718626a174e7e467f0558954f94af117b7d4695d48eb980146016afa4b580b2e"}, - {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:589c72667a5febd36f1315aa6e5f56dd4aa4862df295cb51c769d16142ddd7cd"}, - {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2ed076098b171573161eb146afcb9129b5ff63308960aeca4b676d9d3c35e700"}, - {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:086f92daf51a032d062ec5f58af5ca6a44d082c35299c96376a41cbb33034675"}, - {file = "aiohttp-3.8.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:11691cf4dc5b94236ccc609b70fec991234e7ef8d4c02dd0c9668d1e486f5abf"}, - {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:31d1e1c0dbf19ebccbfd62eff461518dcb1e307b195e93bba60c965a4dcf1ba0"}, - {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:11a67c0d562e07067c4e86bffc1553f2cf5b664d6111c894671b2b8712f3aba5"}, - {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:bb01ba6b0d3f6c68b89fce7305080145d4877ad3acaed424bae4d4ee75faa950"}, - {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:44db35a9e15d6fe5c40d74952e803b1d96e964f683b5a78c3cc64eb177878155"}, - {file = "aiohttp-3.8.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:844a9b460871ee0a0b0b68a64890dae9c415e513db0f4a7e3cab41a0f2fedf33"}, - {file = "aiohttp-3.8.1-cp38-cp38-win32.whl", hash = "sha256:7d08744e9bae2ca9c382581f7dce1273fe3c9bae94ff572c3626e8da5b193c6a"}, - {file = "aiohttp-3.8.1-cp38-cp38-win_amd64.whl", hash = "sha256:04d48b8ce6ab3cf2097b1855e1505181bdd05586ca275f2505514a6e274e8e75"}, - {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:f5315a2eb0239185af1bddb1abf472d877fede3cc8d143c6cddad37678293237"}, - {file = "aiohttp-3.8.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a996d01ca39b8dfe77440f3cd600825d05841088fd6bc0144cc6c2ec14cc5f74"}, - {file = "aiohttp-3.8.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:13487abd2f761d4be7c8ff9080de2671e53fff69711d46de703c310c4c9317ca"}, - {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea302f34477fda3f85560a06d9ebdc7fa41e82420e892fc50b577e35fc6a50b2"}, - {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a2f635ce61a89c5732537a7896b6319a8fcfa23ba09bec36e1b1ac0ab31270d2"}, - {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e999f2d0e12eea01caeecb17b653f3713d758f6dcc770417cf29ef08d3931421"}, - {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0770e2806a30e744b4e21c9d73b7bee18a1cfa3c47991ee2e5a65b887c49d5cf"}, - {file = "aiohttp-3.8.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d15367ce87c8e9e09b0f989bfd72dc641bcd04ba091c68cd305312d00962addd"}, - {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6c7cefb4b0640703eb1069835c02486669312bf2f12b48a748e0a7756d0de33d"}, - {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:71927042ed6365a09a98a6377501af5c9f0a4d38083652bcd2281a06a5976724"}, - {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:28d490af82bc6b7ce53ff31337a18a10498303fe66f701ab65ef27e143c3b0ef"}, - {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:b6613280ccedf24354406caf785db748bebbddcf31408b20c0b48cb86af76866"}, - {file = "aiohttp-3.8.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:81e3d8c34c623ca4e36c46524a3530e99c0bc95ed068fd6e9b55cb721d408fb2"}, - {file = "aiohttp-3.8.1-cp39-cp39-win32.whl", hash = "sha256:7187a76598bdb895af0adbd2fb7474d7f6025d170bc0a1130242da817ce9e7d1"}, - {file = "aiohttp-3.8.1-cp39-cp39-win_amd64.whl", hash = "sha256:1c182cb873bc91b411e184dab7a2b664d4fea2743df0e4d57402f7f3fa644bac"}, - {file = "aiohttp-3.8.1.tar.gz", hash = "sha256:fc5471e1a54de15ef71c1bc6ebe80d4dc681ea600e68bfd1cbce40427f0b7578"}, + {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ba71c9b4dcbb16212f334126cc3d8beb6af377f6703d9dc2d9fb3874fd667ee9"}, + {file = "aiohttp-3.8.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d24b8bb40d5c61ef2d9b6a8f4528c2f17f1c5d2d31fed62ec860f6006142e83e"}, + {file = "aiohttp-3.8.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f88df3a83cf9df566f171adba39d5bd52814ac0b94778d2448652fc77f9eb491"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b97decbb3372d4b69e4d4c8117f44632551c692bb1361b356a02b97b69e18a62"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:309aa21c1d54b8ef0723181d430347d7452daaff93e8e2363db8e75c72c2fb2d"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ad5383a67514e8e76906a06741febd9126fc7c7ff0f599d6fcce3e82b80d026f"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:20acae4f268317bb975671e375493dbdbc67cddb5f6c71eebdb85b34444ac46b"}, + {file = "aiohttp-3.8.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:05a3c31c6d7cd08c149e50dc7aa2568317f5844acd745621983380597f027a18"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d6f76310355e9fae637c3162936e9504b4767d5c52ca268331e2756e54fd4ca5"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:256deb4b29fe5e47893fa32e1de2d73c3afe7407738bd3c63829874661d4822d"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:5c59fcd80b9049b49acd29bd3598cada4afc8d8d69bd4160cd613246912535d7"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:059a91e88f2c00fe40aed9031b3606c3f311414f86a90d696dd982e7aec48142"}, + {file = "aiohttp-3.8.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2feebbb6074cdbd1ac276dbd737b40e890a1361b3cc30b74ac2f5e24aab41f7b"}, + {file = "aiohttp-3.8.3-cp310-cp310-win32.whl", hash = "sha256:5bf651afd22d5f0c4be16cf39d0482ea494f5c88f03e75e5fef3a85177fecdeb"}, + {file = "aiohttp-3.8.3-cp310-cp310-win_amd64.whl", hash = "sha256:653acc3880459f82a65e27bd6526e47ddf19e643457d36a2250b85b41a564715"}, + {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:86fc24e58ecb32aee09f864cb11bb91bc4c1086615001647dbfc4dc8c32f4008"}, + {file = "aiohttp-3.8.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75e14eac916f024305db517e00a9252714fce0abcb10ad327fb6dcdc0d060f1d"}, + {file = "aiohttp-3.8.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d1fde0f44029e02d02d3993ad55ce93ead9bb9b15c6b7ccd580f90bd7e3de476"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ab94426ddb1ecc6a0b601d832d5d9d421820989b8caa929114811369673235c"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:89d2e02167fa95172c017732ed7725bc8523c598757f08d13c5acca308e1a061"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:02f9a2c72fc95d59b881cf38a4b2be9381b9527f9d328771e90f72ac76f31ad8"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c7149272fb5834fc186328e2c1fa01dda3e1fa940ce18fded6d412e8f2cf76d"}, + {file = "aiohttp-3.8.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:512bd5ab136b8dc0ffe3fdf2dfb0c4b4f49c8577f6cae55dca862cd37a4564e2"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:7018ecc5fe97027214556afbc7c502fbd718d0740e87eb1217b17efd05b3d276"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:88c70ed9da9963d5496d38320160e8eb7e5f1886f9290475a881db12f351ab5d"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:da22885266bbfb3f78218dc40205fed2671909fbd0720aedba39b4515c038091"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:e65bc19919c910127c06759a63747ebe14f386cda573d95bcc62b427ca1afc73"}, + {file = "aiohttp-3.8.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:08c78317e950e0762c2983f4dd58dc5e6c9ff75c8a0efeae299d363d439c8e34"}, + {file = "aiohttp-3.8.3-cp311-cp311-win32.whl", hash = "sha256:45d88b016c849d74ebc6f2b6e8bc17cabf26e7e40c0661ddd8fae4c00f015697"}, + {file = "aiohttp-3.8.3-cp311-cp311-win_amd64.whl", hash = "sha256:96372fc29471646b9b106ee918c8eeb4cca423fcbf9a34daa1b93767a88a2290"}, + {file = "aiohttp-3.8.3-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:c971bf3786b5fad82ce5ad570dc6ee420f5b12527157929e830f51c55dc8af77"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ff25f48fc8e623d95eca0670b8cc1469a83783c924a602e0fbd47363bb54aaca"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e381581b37db1db7597b62a2e6b8b57c3deec95d93b6d6407c5b61ddc98aca6d"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db19d60d846283ee275d0416e2a23493f4e6b6028825b51290ac05afc87a6f97"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:25892c92bee6d9449ffac82c2fe257f3a6f297792cdb18ad784737d61e7a9a85"}, + {file = "aiohttp-3.8.3-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:398701865e7a9565d49189f6c90868efaca21be65c725fc87fc305906be915da"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:4a4fbc769ea9b6bd97f4ad0b430a6807f92f0e5eb020f1e42ece59f3ecfc4585"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_i686.whl", hash = "sha256:b29bfd650ed8e148f9c515474a6ef0ba1090b7a8faeee26b74a8ff3b33617502"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_ppc64le.whl", hash = "sha256:1e56b9cafcd6531bab5d9b2e890bb4937f4165109fe98e2b98ef0dcfcb06ee9d"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_s390x.whl", hash = "sha256:ec40170327d4a404b0d91855d41bfe1fe4b699222b2b93e3d833a27330a87a6d"}, + {file = "aiohttp-3.8.3-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:2df5f139233060578d8c2c975128fb231a89ca0a462b35d4b5fcf7c501ebdbe1"}, + {file = "aiohttp-3.8.3-cp36-cp36m-win32.whl", hash = "sha256:f973157ffeab5459eefe7b97a804987876dd0a55570b8fa56b4e1954bf11329b"}, + {file = "aiohttp-3.8.3-cp36-cp36m-win_amd64.whl", hash = "sha256:437399385f2abcd634865705bdc180c8314124b98299d54fe1d4c8990f2f9494"}, + {file = "aiohttp-3.8.3-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:09e28f572b21642128ef31f4e8372adb6888846f32fecb288c8b0457597ba61a"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f3553510abdbec67c043ca85727396ceed1272eef029b050677046d3387be8d"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e168a7560b7c61342ae0412997b069753f27ac4862ec7867eff74f0fe4ea2ad9"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:db4c979b0b3e0fa7e9e69ecd11b2b3174c6963cebadeecfb7ad24532ffcdd11a"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e164e0a98e92d06da343d17d4e9c4da4654f4a4588a20d6c73548a29f176abe2"}, + {file = "aiohttp-3.8.3-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8a78079d9a39ca9ca99a8b0ac2fdc0c4d25fc80c8a8a82e5c8211509c523363"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:21b30885a63c3f4ff5b77a5d6caf008b037cb521a5f33eab445dc566f6d092cc"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:4b0f30372cef3fdc262f33d06e7b411cd59058ce9174ef159ad938c4a34a89da"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:8135fa153a20d82ffb64f70a1b5c2738684afa197839b34cc3e3c72fa88d302c"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:ad61a9639792fd790523ba072c0555cd6be5a0baf03a49a5dd8cfcf20d56df48"}, + {file = "aiohttp-3.8.3-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:978b046ca728073070e9abc074b6299ebf3501e8dee5e26efacb13cec2b2dea0"}, + {file = "aiohttp-3.8.3-cp37-cp37m-win32.whl", hash = "sha256:0d2c6d8c6872df4a6ec37d2ede71eff62395b9e337b4e18efd2177de883a5033"}, + {file = "aiohttp-3.8.3-cp37-cp37m-win_amd64.whl", hash = "sha256:21d69797eb951f155026651f7e9362877334508d39c2fc37bd04ff55b2007091"}, + {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:2ca9af5f8f5812d475c5259393f52d712f6d5f0d7fdad9acdb1107dd9e3cb7eb"}, + {file = "aiohttp-3.8.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d90043c1882067f1bd26196d5d2db9aa6d268def3293ed5fb317e13c9413ea4"}, + {file = "aiohttp-3.8.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:d737fc67b9a970f3234754974531dc9afeea11c70791dcb7db53b0cf81b79784"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ebf909ea0a3fc9596e40d55d8000702a85e27fd578ff41a5500f68f20fd32e6c"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5835f258ca9f7c455493a57ee707b76d2d9634d84d5d7f62e77be984ea80b849"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:da37dcfbf4b7f45d80ee386a5f81122501ec75672f475da34784196690762f4b"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87f44875f2804bc0511a69ce44a9595d5944837a62caecc8490bbdb0e18b1342"}, + {file = "aiohttp-3.8.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:527b3b87b24844ea7865284aabfab08eb0faf599b385b03c2aa91fc6edd6e4b6"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:d5ba88df9aa5e2f806650fcbeedbe4f6e8736e92fc0e73b0400538fd25a4dd96"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:e7b8813be97cab8cb52b1375f41f8e6804f6507fe4660152e8ca5c48f0436017"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:2dea10edfa1a54098703cb7acaa665c07b4e7568472a47f4e64e6319d3821ccf"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:713d22cd9643ba9025d33c4af43943c7a1eb8547729228de18d3e02e278472b6"}, + {file = "aiohttp-3.8.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2d252771fc85e0cf8da0b823157962d70639e63cb9b578b1dec9868dd1f4f937"}, + {file = "aiohttp-3.8.3-cp38-cp38-win32.whl", hash = "sha256:66bd5f950344fb2b3dbdd421aaa4e84f4411a1a13fca3aeb2bcbe667f80c9f76"}, + {file = "aiohttp-3.8.3-cp38-cp38-win_amd64.whl", hash = "sha256:84b14f36e85295fe69c6b9789b51a0903b774046d5f7df538176516c3e422446"}, + {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:16c121ba0b1ec2b44b73e3a8a171c4f999b33929cd2397124a8c7fcfc8cd9e06"}, + {file = "aiohttp-3.8.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:8d6aaa4e7155afaf994d7924eb290abbe81a6905b303d8cb61310a2aba1c68ba"}, + {file = "aiohttp-3.8.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:43046a319664a04b146f81b40e1545d4c8ac7b7dd04c47e40bf09f65f2437346"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:599418aaaf88a6d02a8c515e656f6faf3d10618d3dd95866eb4436520096c84b"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:92a2964319d359f494f16011e23434f6f8ef0434acd3cf154a6b7bec511e2fb7"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:73a4131962e6d91109bca6536416aa067cf6c4efb871975df734f8d2fd821b37"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:598adde339d2cf7d67beaccda3f2ce7c57b3b412702f29c946708f69cf8222aa"}, + {file = "aiohttp-3.8.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:75880ed07be39beff1881d81e4a907cafb802f306efd6d2d15f2b3c69935f6fb"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:a0239da9fbafd9ff82fd67c16704a7d1bccf0d107a300e790587ad05547681c8"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:4e3a23ec214e95c9fe85a58470b660efe6534b83e6cbe38b3ed52b053d7cb6ad"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:47841407cc89a4b80b0c52276f3cc8138bbbfba4b179ee3acbd7d77ae33f7ac4"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:54d107c89a3ebcd13228278d68f1436d3f33f2dd2af5415e3feaeb1156e1a62c"}, + {file = "aiohttp-3.8.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c37c5cce780349d4d51739ae682dec63573847a2a8dcb44381b174c3d9c8d403"}, + {file = "aiohttp-3.8.3-cp39-cp39-win32.whl", hash = "sha256:f178d2aadf0166be4df834c4953da2d7eef24719e8aec9a65289483eeea9d618"}, + {file = "aiohttp-3.8.3-cp39-cp39-win_amd64.whl", hash = "sha256:88e5be56c231981428f4f506c68b6a46fa25c4123a2e86d156c58a8369d31ab7"}, + {file = "aiohttp-3.8.3.tar.gz", hash = "sha256:3828fb41b7203176b82fe5d699e0d845435f2374750a44b480ea6b930f6be269"}, ] aiosignal = [ {file = "aiosignal-1.2.0-py3-none-any.whl", hash = "sha256:26e62109036cd181df6e6ad646f91f0dcfd05fe16d0cb924138ff2ab75d64e3a"}, @@ -1171,45 +1192,43 @@ babel = [ {file = "Babel-2.10.3.tar.gz", hash = "sha256:7614553711ee97490f732126dc077f8d0ae084ebc6a96e23db1482afabdb2c51"}, ] backoff = [ - {file = "backoff-2.1.2-py3-none-any.whl", hash = "sha256:b135e6d7c7513ba2bfd6895bc32bc8c66c6f3b0279b4c6cd866053cfd7d3126b"}, - {file = "backoff-2.1.2.tar.gz", hash = "sha256:407f1bc0f22723648a8880821b935ce5df8475cf04f7b6b5017ae264d30f6069"}, + {file = "backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8"}, + {file = "backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba"}, ] beautifulsoup4 = [ {file = "beautifulsoup4-4.11.1-py3-none-any.whl", hash = "sha256:58d5c3d29f5a36ffeb94f02f0d786cd53014cf9b3b3951d42e0080d8a9498d30"}, {file = "beautifulsoup4-4.11.1.tar.gz", hash = "sha256:ad9aa55b65ef2808eb405f46cf74df7fcb7044d5cbc26487f96eb2ef2e436693"}, ] black = [ - {file = "black-22.8.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ce957f1d6b78a8a231b18e0dd2d94a33d2ba738cd88a7fe64f53f659eea49fdd"}, - {file = "black-22.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5107ea36b2b61917956d018bd25129baf9ad1125e39324a9b18248d362156a27"}, - {file = "black-22.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e8166b7bfe5dcb56d325385bd1d1e0f635f24aae14b3ae437102dedc0c186747"}, - {file = "black-22.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd82842bb272297503cbec1a2600b6bfb338dae017186f8f215c8958f8acf869"}, - {file = "black-22.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d839150f61d09e7217f52917259831fe2b689f5c8e5e32611736351b89bb2a90"}, - {file = "black-22.8.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:a05da0430bd5ced89176db098567973be52ce175a55677436a271102d7eaa3fe"}, - {file = "black-22.8.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a098a69a02596e1f2a58a2a1c8d5a05d5a74461af552b371e82f9fa4ada8342"}, - {file = "black-22.8.0-cp36-cp36m-win_amd64.whl", hash = "sha256:5594efbdc35426e35a7defa1ea1a1cb97c7dbd34c0e49af7fb593a36bd45edab"}, - {file = "black-22.8.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a983526af1bea1e4cf6768e649990f28ee4f4137266921c2c3cee8116ae42ec3"}, - {file = "black-22.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b2c25f8dea5e8444bdc6788a2f543e1fb01494e144480bc17f806178378005e"}, - {file = "black-22.8.0-cp37-cp37m-win_amd64.whl", hash = "sha256:78dd85caaab7c3153054756b9fe8c611efa63d9e7aecfa33e533060cb14b6d16"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:cea1b2542d4e2c02c332e83150e41e3ca80dc0fb8de20df3c5e98e242156222c"}, - {file = "black-22.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5b879eb439094751185d1cfdca43023bc6786bd3c60372462b6f051efa6281a5"}, - {file = "black-22.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0a12e4e1353819af41df998b02c6742643cfef58282915f781d0e4dd7a200411"}, - {file = "black-22.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3a73f66b6d5ba7288cd5d6dad9b4c9b43f4e8a4b789a94bf5abfb878c663eb3"}, - {file = "black-22.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:e981e20ec152dfb3e77418fb616077937378b322d7b26aa1ff87717fb18b4875"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:8ce13ffed7e66dda0da3e0b2eb1bdfc83f5812f66e09aca2b0978593ed636b6c"}, - {file = "black-22.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:32a4b17f644fc288c6ee2bafdf5e3b045f4eff84693ac069d87b1a347d861497"}, - {file = "black-22.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0ad827325a3a634bae88ae7747db1a395d5ee02cf05d9aa7a9bd77dfb10e940c"}, - {file = "black-22.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:53198e28a1fb865e9fe97f88220da2e44df6da82b18833b588b1883b16bb5d41"}, - {file = "black-22.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:bc4d4123830a2d190e9cc42a2e43570f82ace35c3aeb26a512a2102bce5af7ec"}, - {file = "black-22.8.0-py3-none-any.whl", hash = "sha256:d2c21d439b2baf7aa80d6dd4e3659259be64c6f49dfd0f32091063db0e006db4"}, - {file = "black-22.8.0.tar.gz", hash = "sha256:792f7eb540ba9a17e8656538701d3eb1afcb134e3b45b71f20b25c77a8db7e6e"}, + {file = "black-22.10.0-1fixedarch-cp310-cp310-macosx_11_0_x86_64.whl", hash = "sha256:5cc42ca67989e9c3cf859e84c2bf014f6633db63d1cbdf8fdb666dcd9e77e3fa"}, + {file = "black-22.10.0-1fixedarch-cp311-cp311-macosx_11_0_x86_64.whl", hash = "sha256:5d8f74030e67087b219b032aa33a919fae8806d49c867846bfacde57f43972ef"}, + {file = "black-22.10.0-1fixedarch-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:197df8509263b0b8614e1df1756b1dd41be6738eed2ba9e9769f3880c2b9d7b6"}, + {file = "black-22.10.0-1fixedarch-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:2644b5d63633702bc2c5f3754b1b475378fbbfb481f62319388235d0cd104c2d"}, + {file = "black-22.10.0-1fixedarch-cp39-cp39-macosx_11_0_x86_64.whl", hash = "sha256:e41a86c6c650bcecc6633ee3180d80a025db041a8e2398dcc059b3afa8382cd4"}, + {file = "black-22.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2039230db3c6c639bd84efe3292ec7b06e9214a2992cd9beb293d639c6402edb"}, + {file = "black-22.10.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14ff67aec0a47c424bc99b71005202045dc09270da44a27848d534600ac64fc7"}, + {file = "black-22.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:819dc789f4498ecc91438a7de64427c73b45035e2e3680c92e18795a839ebb66"}, + {file = "black-22.10.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:5b9b29da4f564ba8787c119f37d174f2b69cdfdf9015b7d8c5c16121ddc054ae"}, + {file = "black-22.10.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8b49776299fece66bffaafe357d929ca9451450f5466e997a7285ab0fe28e3b"}, + {file = "black-22.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:21199526696b8f09c3997e2b4db8d0b108d801a348414264d2eb8eb2532e540d"}, + {file = "black-22.10.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1e464456d24e23d11fced2bc8c47ef66d471f845c7b7a42f3bd77bf3d1789650"}, + {file = "black-22.10.0-cp37-cp37m-win_amd64.whl", hash = "sha256:9311e99228ae10023300ecac05be5a296f60d2fd10fff31cf5c1fa4ca4b1988d"}, + {file = "black-22.10.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fba8a281e570adafb79f7755ac8721b6cf1bbf691186a287e990c7929c7692ff"}, + {file = "black-22.10.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:915ace4ff03fdfff953962fa672d44be269deb2eaf88499a0f8805221bc68c87"}, + {file = "black-22.10.0-cp38-cp38-win_amd64.whl", hash = "sha256:444ebfb4e441254e87bad00c661fe32df9969b2bf224373a448d8aca2132b395"}, + {file = "black-22.10.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:974308c58d057a651d182208a484ce80a26dac0caef2895836a92dd6ebd725e0"}, + {file = "black-22.10.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72ef3925f30e12a184889aac03d77d031056860ccae8a1e519f6cbb742736383"}, + {file = "black-22.10.0-cp39-cp39-win_amd64.whl", hash = "sha256:432247333090c8c5366e69627ccb363bc58514ae3e63f7fc75c54b1ea80fa7de"}, + {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, + {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, ] certifi = [ - {file = "certifi-2022.6.15-py3-none-any.whl", hash = "sha256:fe86415d55e84719d75f8b69414f6438ac3547d2078ab91b67e779ef69378412"}, - {file = "certifi-2022.6.15.tar.gz", hash = "sha256:84c85a9078b11105f04f3036a9482ae10e4621616db313fe045dd24743a0820d"}, + {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, + {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, ] charset-normalizer = [ - {file = "charset-normalizer-2.1.0.tar.gz", hash = "sha256:575e708016ff3a5e3681541cb9d79312c416835686d054a23accb873b254f413"}, - {file = "charset_normalizer-2.1.0-py3-none-any.whl", hash = "sha256:5189b6f22b01957427f35b6a08d9a0bc45b46d3788ef5a92e978433c7a35f8a5"}, + {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, + {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, ] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, @@ -1220,66 +1239,138 @@ colorama = [ {file = "colorama-0.4.5.tar.gz", hash = "sha256:e6c6b4334fc50988a639d9b98aa429a0b57da6e17b9a44f0451f930b6967b7a4"}, ] coverage = [ - {file = "coverage-6.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e7b4da9bafad21ea45a714d3ea6f3e1679099e420c8741c74905b92ee9bfa7cc"}, - {file = "coverage-6.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fde17bc42e0716c94bf19d92e4c9f5a00c5feb401f5bc01101fdf2a8b7cacf60"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbb0d89923c80dbd435b9cf8bba0ff55585a3cdb28cbec65f376c041472c60d"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67f9346aeebea54e845d29b487eb38ec95f2ecf3558a3cffb26ee3f0dcc3e760"}, - {file = "coverage-6.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42c499c14efd858b98c4e03595bf914089b98400d30789511577aa44607a1b74"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:c35cca192ba700979d20ac43024a82b9b32a60da2f983bec6c0f5b84aead635c"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9cc4f107009bca5a81caef2fca843dbec4215c05e917a59dec0c8db5cff1d2aa"}, - {file = "coverage-6.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f444627b3664b80d078c05fe6a850dd711beeb90d26731f11d492dcbadb6973"}, - {file = "coverage-6.4.4-cp310-cp310-win32.whl", hash = "sha256:66e6df3ac4659a435677d8cd40e8eb1ac7219345d27c41145991ee9bf4b806a0"}, - {file = "coverage-6.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:35ef1f8d8a7a275aa7410d2f2c60fa6443f4a64fae9be671ec0696a68525b875"}, - {file = "coverage-6.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c1328d0c2f194ffda30a45f11058c02410e679456276bfa0bbe0b0ee87225fac"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61b993f3998ee384935ee423c3d40894e93277f12482f6e777642a0141f55782"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d5dd4b8e9cd0deb60e6fcc7b0647cbc1da6c33b9e786f9c79721fd303994832f"}, - {file = "coverage-6.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7026f5afe0d1a933685d8f2169d7c2d2e624f6255fb584ca99ccca8c0e966fd7"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9c7b9b498eb0c0d48b4c2abc0e10c2d78912203f972e0e63e3c9dc21f15abdaa"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:ee2b2fb6eb4ace35805f434e0f6409444e1466a47f620d1d5763a22600f0f892"}, - {file = "coverage-6.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ab066f5ab67059d1f1000b5e1aa8bbd75b6ed1fc0014559aea41a9eb66fc2ce0"}, - {file = "coverage-6.4.4-cp311-cp311-win32.whl", hash = "sha256:9d6e1f3185cbfd3d91ac77ea065d85d5215d3dfa45b191d14ddfcd952fa53796"}, - {file = "coverage-6.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:e3d3c4cc38b2882f9a15bafd30aec079582b819bec1b8afdbde8f7797008108a"}, - {file = "coverage-6.4.4-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a095aa0a996ea08b10580908e88fbaf81ecf798e923bbe64fb98d1807db3d68a"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ef6f44409ab02e202b31a05dd6666797f9de2aa2b4b3534e9d450e42dea5e817"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b7101938584d67e6f45f0015b60e24a95bf8dea19836b1709a80342e01b472f"}, - {file = "coverage-6.4.4-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14a32ec68d721c3d714d9b105c7acf8e0f8a4f4734c811eda75ff3718570b5e3"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6a864733b22d3081749450466ac80698fe39c91cb6849b2ef8752fd7482011f3"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:08002f9251f51afdcc5e3adf5d5d66bb490ae893d9e21359b085f0e03390a820"}, - {file = "coverage-6.4.4-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:a3b2752de32c455f2521a51bd3ffb53c5b3ae92736afde67ce83477f5c1dd928"}, - {file = "coverage-6.4.4-cp37-cp37m-win32.whl", hash = "sha256:f855b39e4f75abd0dfbcf74a82e84ae3fc260d523fcb3532786bcbbcb158322c"}, - {file = "coverage-6.4.4-cp37-cp37m-win_amd64.whl", hash = "sha256:ee6ae6bbcac0786807295e9687169fba80cb0617852b2fa118a99667e8e6815d"}, - {file = "coverage-6.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:564cd0f5b5470094df06fab676c6d77547abfdcb09b6c29c8a97c41ad03b103c"}, - {file = "coverage-6.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cbbb0e4cd8ddcd5ef47641cfac97d8473ab6b132dd9a46bacb18872828031685"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6113e4df2fa73b80f77663445be6d567913fb3b82a86ceb64e44ae0e4b695de1"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8d032bfc562a52318ae05047a6eb801ff31ccee172dc0d2504614e911d8fa83e"}, - {file = "coverage-6.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e431e305a1f3126477abe9a184624a85308da8edf8486a863601d58419d26ffa"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cf2afe83a53f77aec067033199797832617890e15bed42f4a1a93ea24794ae3e"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:783bc7c4ee524039ca13b6d9b4186a67f8e63d91342c713e88c1865a38d0892a"}, - {file = "coverage-6.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ff934ced84054b9018665ca3967fc48e1ac99e811f6cc99ea65978e1d384454b"}, - {file = "coverage-6.4.4-cp38-cp38-win32.whl", hash = "sha256:e1fabd473566fce2cf18ea41171d92814e4ef1495e04471786cbc943b89a3781"}, - {file = "coverage-6.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:4179502f210ebed3ccfe2f78bf8e2d59e50b297b598b100d6c6e3341053066a2"}, - {file = "coverage-6.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:98c0b9e9b572893cdb0a00e66cf961a238f8d870d4e1dc8e679eb8bdc2eb1b86"}, - {file = "coverage-6.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc600f6ec19b273da1d85817eda339fb46ce9eef3e89f220055d8696e0a06908"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7a98d6bf6d4ca5c07a600c7b4e0c5350cd483c85c736c522b786be90ea5bac4f"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01778769097dbd705a24e221f42be885c544bb91251747a8a3efdec6eb4788f2"}, - {file = "coverage-6.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dfa0b97eb904255e2ab24166071b27408f1f69c8fbda58e9c0972804851e0558"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:fcbe3d9a53e013f8ab88734d7e517eb2cd06b7e689bedf22c0eb68db5e4a0a19"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:15e38d853ee224e92ccc9a851457fb1e1f12d7a5df5ae44544ce7863691c7a0d"}, - {file = "coverage-6.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6913dddee2deff8ab2512639c5168c3e80b3ebb0f818fed22048ee46f735351a"}, - {file = "coverage-6.4.4-cp39-cp39-win32.whl", hash = "sha256:354df19fefd03b9a13132fa6643527ef7905712109d9c1c1903f2133d3a4e145"}, - {file = "coverage-6.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:1238b08f3576201ebf41f7c20bf59baa0d05da941b123c6656e42cdb668e9827"}, - {file = "coverage-6.4.4-pp36.pp37.pp38-none-any.whl", hash = "sha256:f67cf9f406cf0d2f08a3515ce2db5b82625a7257f88aad87904674def6ddaec1"}, - {file = "coverage-6.4.4.tar.gz", hash = "sha256:e16c45b726acb780e1e6f88b286d3c10b3914ab03438f32117c4aa52d7f30d58"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"}, + {file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"}, + {file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"}, + {file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"}, + {file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"}, + {file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"}, + {file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"}, + {file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"}, + {file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"}, + {file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"}, + {file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"}, + {file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"}, + {file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"}, + {file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"}, + {file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"}, + {file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"}, + {file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"}, + {file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"}, + {file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"}, + {file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"}, + {file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"}, + {file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"}, + {file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"}, + {file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"}, + {file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"}, + {file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"}, + {file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"}, + {file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"}, ] dill = [ {file = "dill-0.3.5.1-py2.py3-none-any.whl", hash = "sha256:33501d03270bbe410c72639b350e941882a8b0fd55357580fbc873fba0c59302"}, {file = "dill-0.3.5.1.tar.gz", hash = "sha256:d75e41f3eff1eee599d738e76ba8f4ad98ea229db8b085318aa2b3333a208c86"}, ] -distlib = [] -docutils = [] -filelock = [] -flake8 = [] -frozenlist = [] +distlib = [ + {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, + {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, +] +docutils = [ + {file = "docutils-0.19-py3-none-any.whl", hash = "sha256:5e1de4d849fee02c63b040a4a3fd567f4ab104defd8a5511fbbc24a8a017efbc"}, + {file = "docutils-0.19.tar.gz", hash = "sha256:33995a6753c30b7f577febfc2c50411fec6aac7f7ffeb7c4cfe5991072dcf9e6"}, +] +filelock = [ + {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, + {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, +] +flake8 = [ + {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, + {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, +] +frozenlist = [ + {file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5f271c93f001748fc26ddea409241312a75e13466b06c94798d1a341cf0e6989"}, + {file = "frozenlist-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9c6ef8014b842f01f5d2b55315f1af5cbfde284eb184075c189fd657c2fd8204"}, + {file = "frozenlist-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:219a9676e2eae91cb5cc695a78b4cb43d8123e4160441d2b6ce8d2c70c60e2f3"}, + {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b47d64cdd973aede3dd71a9364742c542587db214e63b7529fbb487ed67cddd9"}, + {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2af6f7a4e93f5d08ee3f9152bce41a6015b5cf87546cb63872cc19b45476e98a"}, + {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a718b427ff781c4f4e975525edb092ee2cdef6a9e7bc49e15063b088961806f8"}, + {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c56c299602c70bc1bb5d1e75f7d8c007ca40c9d7aebaf6e4ba52925d88ef826d"}, + {file = "frozenlist-1.3.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:717470bfafbb9d9be624da7780c4296aa7935294bd43a075139c3d55659038ca"}, + {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:31b44f1feb3630146cffe56344704b730c33e042ffc78d21f2125a6a91168131"}, + {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c3b31180b82c519b8926e629bf9f19952c743e089c41380ddca5db556817b221"}, + {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:d82bed73544e91fb081ab93e3725e45dd8515c675c0e9926b4e1f420a93a6ab9"}, + {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:49459f193324fbd6413e8e03bd65789e5198a9fa3095e03f3620dee2f2dabff2"}, + {file = "frozenlist-1.3.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:94e680aeedc7fd3b892b6fa8395b7b7cc4b344046c065ed4e7a1e390084e8cb5"}, + {file = "frozenlist-1.3.1-cp310-cp310-win32.whl", hash = "sha256:fabb953ab913dadc1ff9dcc3a7a7d3dc6a92efab3a0373989b8063347f8705be"}, + {file = "frozenlist-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:eee0c5ecb58296580fc495ac99b003f64f82a74f9576a244d04978a7e97166db"}, + {file = "frozenlist-1.3.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:0bc75692fb3770cf2b5856a6c2c9de967ca744863c5e89595df64e252e4b3944"}, + {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:086ca1ac0a40e722d6833d4ce74f5bf1aba2c77cbfdc0cd83722ffea6da52a04"}, + {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1b51eb355e7f813bcda00276b0114c4172872dc5fb30e3fea059b9367c18fbcb"}, + {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:74140933d45271c1a1283f708c35187f94e1256079b3c43f0c2267f9db5845ff"}, + {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee4c5120ddf7d4dd1eaf079af3af7102b56d919fa13ad55600a4e0ebe532779b"}, + {file = "frozenlist-1.3.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:97d9e00f3ac7c18e685320601f91468ec06c58acc185d18bb8e511f196c8d4b2"}, + {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:6e19add867cebfb249b4e7beac382d33215d6d54476bb6be46b01f8cafb4878b"}, + {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:a027f8f723d07c3f21963caa7d585dcc9b089335565dabe9c814b5f70c52705a"}, + {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:61d7857950a3139bce035ad0b0945f839532987dfb4c06cfe160254f4d19df03"}, + {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:53b2b45052e7149ee8b96067793db8ecc1ae1111f2f96fe1f88ea5ad5fd92d10"}, + {file = "frozenlist-1.3.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bbb1a71b1784e68870800b1bc9f3313918edc63dbb8f29fbd2e767ce5821696c"}, + {file = "frozenlist-1.3.1-cp37-cp37m-win32.whl", hash = "sha256:ab6fa8c7871877810e1b4e9392c187a60611fbf0226a9e0b11b7b92f5ac72792"}, + {file = "frozenlist-1.3.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f89139662cc4e65a4813f4babb9ca9544e42bddb823d2ec434e18dad582543bc"}, + {file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:4c0c99e31491a1d92cde8648f2e7ccad0e9abb181f6ac3ddb9fc48b63301808e"}, + {file = "frozenlist-1.3.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:61e8cb51fba9f1f33887e22488bad1e28dd8325b72425f04517a4d285a04c519"}, + {file = "frozenlist-1.3.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc2f3e368ee5242a2cbe28323a866656006382872c40869b49b265add546703f"}, + {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:58fb94a01414cddcdc6839807db77ae8057d02ddafc94a42faee6004e46c9ba8"}, + {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:022178b277cb9277d7d3b3f2762d294f15e85cd2534047e68a118c2bb0058f3e"}, + {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:572ce381e9fe027ad5e055f143763637dcbac2542cfe27f1d688846baeef5170"}, + {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:19127f8dcbc157ccb14c30e6f00392f372ddb64a6ffa7106b26ff2196477ee9f"}, + {file = "frozenlist-1.3.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42719a8bd3792744c9b523674b752091a7962d0d2d117f0b417a3eba97d1164b"}, + {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2743bb63095ef306041c8f8ea22bd6e4d91adabf41887b1ad7886c4c1eb43d5f"}, + {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:fa47319a10e0a076709644a0efbcaab9e91902c8bd8ef74c6adb19d320f69b83"}, + {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:52137f0aea43e1993264a5180c467a08a3e372ca9d378244c2d86133f948b26b"}, + {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:f5abc8b4d0c5b556ed8cd41490b606fe99293175a82b98e652c3f2711b452988"}, + {file = "frozenlist-1.3.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:1e1cf7bc8cbbe6ce3881863671bac258b7d6bfc3706c600008925fb799a256e2"}, + {file = "frozenlist-1.3.1-cp38-cp38-win32.whl", hash = "sha256:0dde791b9b97f189874d654c55c24bf7b6782343e14909c84beebd28b7217845"}, + {file = "frozenlist-1.3.1-cp38-cp38-win_amd64.whl", hash = "sha256:9494122bf39da6422b0972c4579e248867b6b1b50c9b05df7e04a3f30b9a413d"}, + {file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:31bf9539284f39ff9398deabf5561c2b0da5bb475590b4e13dd8b268d7a3c5c1"}, + {file = "frozenlist-1.3.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e0c8c803f2f8db7217898d11657cb6042b9b0553a997c4a0601f48a691480fab"}, + {file = "frozenlist-1.3.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da5ba7b59d954f1f214d352308d1d86994d713b13edd4b24a556bcc43d2ddbc3"}, + {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:74e6b2b456f21fc93ce1aff2b9728049f1464428ee2c9752a4b4f61e98c4db96"}, + {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:526d5f20e954d103b1d47232e3839f3453c02077b74203e43407b962ab131e7b"}, + {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b499c6abe62a7a8d023e2c4b2834fce78a6115856ae95522f2f974139814538c"}, + {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab386503f53bbbc64d1ad4b6865bf001414930841a870fc97f1546d4d133f141"}, + {file = "frozenlist-1.3.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5f63c308f82a7954bf8263a6e6de0adc67c48a8b484fab18ff87f349af356efd"}, + {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:12607804084d2244a7bd4685c9d0dca5df17a6a926d4f1967aa7978b1028f89f"}, + {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:da1cdfa96425cbe51f8afa43e392366ed0b36ce398f08b60de6b97e3ed4affef"}, + {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:f810e764617b0748b49a731ffaa525d9bb36ff38332411704c2400125af859a6"}, + {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:35c3d79b81908579beb1fb4e7fcd802b7b4921f1b66055af2578ff7734711cfa"}, + {file = "frozenlist-1.3.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:c92deb5d9acce226a501b77307b3b60b264ca21862bd7d3e0c1f3594022f01bc"}, + {file = "frozenlist-1.3.1-cp39-cp39-win32.whl", hash = "sha256:5e77a8bd41e54b05e4fb2708dc6ce28ee70325f8c6f50f3df86a44ecb1d7a19b"}, + {file = "frozenlist-1.3.1-cp39-cp39-win_amd64.whl", hash = "sha256:625d8472c67f2d96f9a4302a947f92a7adbc1e20bedb6aff8dbc8ff039ca6189"}, + {file = "frozenlist-1.3.1.tar.gz", hash = "sha256:3a735e4211a04ccfa3f4833547acdf5d2f863bfeb01cfd3edaffbc251f15cec8"}, +] h11 = [ {file = "h11-0.12.0-py3-none-any.whl", hash = "sha256:36a3cb8c0a032f56e2da7084577878a035d3b61d104230d4bd49c0c6b555a9c6"}, {file = "h11-0.12.0.tar.gz", hash = "sha256:47222cb6067e4a307d535814917cd98fd0a57b6788ce715755fa2b6c28b56042"}, @@ -1293,14 +1384,17 @@ httpx = [ {file = "httpx-0.23.0.tar.gz", hash = "sha256:f28eac771ec9eb4866d3fb4ab65abd42d38c424739e80c08d8d20570de60b0ef"}, ] idna = [ - {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"}, - {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"}, + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, ] imagesize = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] -importlib-metadata = [] +importlib-metadata = [ + {file = "importlib_metadata-5.0.0-py3-none-any.whl", hash = "sha256:ddb0e35065e8938f867ed4928d0ae5bf2a53b7773871bfe6bcc7e4fcdc7dea43"}, + {file = "importlib_metadata-5.0.0.tar.gz", hash = "sha256:da31db32b304314d044d3c12c79bd59e307889b287ad12ff387b3500835fc2ab"}, +] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -1352,7 +1446,10 @@ lazy-object-proxy = [ {file = "lazy_object_proxy-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:677ea950bef409b47e51e733283544ac3d660b709cfce7b187f5ace137960d61"}, {file = "lazy_object_proxy-1.7.1-pp37.pp38-none-any.whl", hash = "sha256:d66906d5785da8e0be7360912e99c9188b70f52c422f9fc18223347235691a84"}, ] -m2r2 = [] +m2r2 = [ + {file = "m2r2-0.3.3-py3-none-any.whl", hash = "sha256:2ee32a5928c3598b67c70e6d22981ec936c03d5bfd2f64229e77678731952f16"}, + {file = "m2r2-0.3.3.tar.gz", hash = "sha256:f9b6e9efbc2b6987dbd43d2fd15a6d115ba837d8158ae73295542635b4086e75"}, +] markupsafe = [ {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, @@ -1395,7 +1492,10 @@ markupsafe = [ {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, ] -mccabe = [] +mccabe = [ + {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, + {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, +] mistune = [ {file = "mistune-0.8.4-py2.py3-none-any.whl", hash = "sha256:88a1051873018da288eee8538d476dffe1262495144b33ecb586c4ab266bb8d4"}, {file = "mistune-0.8.4.tar.gz", hash = "sha256:59a3429db53c50b5c6bcc8a07f8848cb00d7dc8bdb431a4ab41920d201d4756e"}, @@ -1462,29 +1562,30 @@ multidict = [ {file = "multidict-6.0.2.tar.gz", hash = "sha256:5ff3bd75f38e4c43f1f470f2df7a4d430b821c4ce22be384e1459cb57d6bb013"}, ] mypy = [ - {file = "mypy-0.971-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:f2899a3cbd394da157194f913a931edfd4be5f274a88041c9dc2d9cdcb1c315c"}, - {file = "mypy-0.971-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:98e02d56ebe93981c41211c05adb630d1d26c14195d04d95e49cd97dbc046dc5"}, - {file = "mypy-0.971-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:19830b7dba7d5356d3e26e2427a2ec91c994cd92d983142cbd025ebe81d69cf3"}, - {file = "mypy-0.971-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:02ef476f6dcb86e6f502ae39a16b93285fef97e7f1ff22932b657d1ef1f28655"}, - {file = "mypy-0.971-cp310-cp310-win_amd64.whl", hash = "sha256:25c5750ba5609a0c7550b73a33deb314ecfb559c350bb050b655505e8aed4103"}, - {file = "mypy-0.971-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:d3348e7eb2eea2472db611486846742d5d52d1290576de99d59edeb7cd4a42ca"}, - {file = "mypy-0.971-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3fa7a477b9900be9b7dd4bab30a12759e5abe9586574ceb944bc29cddf8f0417"}, - {file = "mypy-0.971-cp36-cp36m-win_amd64.whl", hash = "sha256:2ad53cf9c3adc43cf3bea0a7d01a2f2e86db9fe7596dfecb4496a5dda63cbb09"}, - {file = "mypy-0.971-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:855048b6feb6dfe09d3353466004490b1872887150c5bb5caad7838b57328cc8"}, - {file = "mypy-0.971-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:23488a14a83bca6e54402c2e6435467a4138785df93ec85aeff64c6170077fb0"}, - {file = "mypy-0.971-cp37-cp37m-win_amd64.whl", hash = "sha256:4b21e5b1a70dfb972490035128f305c39bc4bc253f34e96a4adf9127cf943eb2"}, - {file = "mypy-0.971-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:9796a2ba7b4b538649caa5cecd398d873f4022ed2333ffde58eaf604c4d2cb27"}, - {file = "mypy-0.971-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5a361d92635ad4ada1b1b2d3630fc2f53f2127d51cf2def9db83cba32e47c856"}, - {file = "mypy-0.971-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b793b899f7cf563b1e7044a5c97361196b938e92f0a4343a5d27966a53d2ec71"}, - {file = "mypy-0.971-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d1ea5d12c8e2d266b5fb8c7a5d2e9c0219fedfeb493b7ed60cd350322384ac27"}, - {file = "mypy-0.971-cp38-cp38-win_amd64.whl", hash = "sha256:23c7ff43fff4b0df93a186581885c8512bc50fc4d4910e0f838e35d6bb6b5e58"}, - {file = "mypy-0.971-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1f7656b69974a6933e987ee8ffb951d836272d6c0f81d727f1d0e2696074d9e6"}, - {file = "mypy-0.971-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d2022bfadb7a5c2ef410d6a7c9763188afdb7f3533f22a0a32be10d571ee4bbe"}, - {file = "mypy-0.971-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef943c72a786b0f8d90fd76e9b39ce81fb7171172daf84bf43eaf937e9f220a9"}, - {file = "mypy-0.971-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:d744f72eb39f69312bc6c2abf8ff6656973120e2eb3f3ec4f758ed47e414a4bf"}, - {file = "mypy-0.971-cp39-cp39-win_amd64.whl", hash = "sha256:77a514ea15d3007d33a9e2157b0ba9c267496acf12a7f2b9b9f8446337aac5b0"}, - {file = "mypy-0.971-py3-none-any.whl", hash = "sha256:0d054ef16b071149917085f51f89555a576e2618d5d9dd70bd6eea6410af3ac9"}, - {file = "mypy-0.971.tar.gz", hash = "sha256:40b0f21484238269ae6a57200c807d80debc6459d444c0489a102d7c6a75fa56"}, + {file = "mypy-0.982-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:5085e6f442003fa915aeb0a46d4da58128da69325d8213b4b35cc7054090aed5"}, + {file = "mypy-0.982-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:41fd1cf9bc0e1c19b9af13a6580ccb66c381a5ee2cf63ee5ebab747a4badeba3"}, + {file = "mypy-0.982-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f793e3dd95e166b66d50e7b63e69e58e88643d80a3dcc3bcd81368e0478b089c"}, + {file = "mypy-0.982-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86ebe67adf4d021b28c3f547da6aa2cce660b57f0432617af2cca932d4d378a6"}, + {file = "mypy-0.982-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:175f292f649a3af7082fe36620369ffc4661a71005aa9f8297ea473df5772046"}, + {file = "mypy-0.982-cp310-cp310-win_amd64.whl", hash = "sha256:8ee8c2472e96beb1045e9081de8e92f295b89ac10c4109afdf3a23ad6e644f3e"}, + {file = "mypy-0.982-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:58f27ebafe726a8e5ccb58d896451dd9a662a511a3188ff6a8a6a919142ecc20"}, + {file = "mypy-0.982-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6af646bd46f10d53834a8e8983e130e47d8ab2d4b7a97363e35b24e1d588947"}, + {file = "mypy-0.982-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e7aeaa763c7ab86d5b66ff27f68493d672e44c8099af636d433a7f3fa5596d40"}, + {file = "mypy-0.982-cp37-cp37m-win_amd64.whl", hash = "sha256:724d36be56444f569c20a629d1d4ee0cb0ad666078d59bb84f8f887952511ca1"}, + {file = "mypy-0.982-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:14d53cdd4cf93765aa747a7399f0961a365bcddf7855d9cef6306fa41de01c24"}, + {file = "mypy-0.982-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:26ae64555d480ad4b32a267d10cab7aec92ff44de35a7cd95b2b7cb8e64ebe3e"}, + {file = "mypy-0.982-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:6389af3e204975d6658de4fb8ac16f58c14e1bacc6142fee86d1b5b26aa52bda"}, + {file = "mypy-0.982-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b35ce03a289480d6544aac85fa3674f493f323d80ea7226410ed065cd46f206"}, + {file = "mypy-0.982-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:c6e564f035d25c99fd2b863e13049744d96bd1947e3d3d2f16f5828864506763"}, + {file = "mypy-0.982-cp38-cp38-win_amd64.whl", hash = "sha256:cebca7fd333f90b61b3ef7f217ff75ce2e287482206ef4a8b18f32b49927b1a2"}, + {file = "mypy-0.982-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a705a93670c8b74769496280d2fe6cd59961506c64f329bb179970ff1d24f9f8"}, + {file = "mypy-0.982-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:75838c649290d83a2b83a88288c1eb60fe7a05b36d46cbea9d22efc790002146"}, + {file = "mypy-0.982-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:91781eff1f3f2607519c8b0e8518aad8498af1419e8442d5d0afb108059881fc"}, + {file = "mypy-0.982-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaa97b9ddd1dd9901a22a879491dbb951b5dec75c3b90032e2baa7336777363b"}, + {file = "mypy-0.982-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a692a8e7d07abe5f4b2dd32d731812a0175626a90a223d4b58f10f458747dd8a"}, + {file = "mypy-0.982-cp39-cp39-win_amd64.whl", hash = "sha256:eb7a068e503be3543c4bd329c994103874fa543c1727ba5288393c21d912d795"}, + {file = "mypy-0.982-py3-none-any.whl", hash = "sha256:1021c241e8b6e1ca5a47e4d52601274ac078a89845cfde66c6d5f769819ffa1d"}, + {file = "mypy-0.982.tar.gz", hash = "sha256:85f7a343542dc8b1ed0a888cdd34dca56462654ef23aa673907305b260b3d746"}, ] mypy-extensions = [ {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"}, @@ -1510,12 +1611,18 @@ py = [ {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, ] -pycodestyle = [] +pycodestyle = [ + {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, + {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, +] pydocstyle = [ {file = "pydocstyle-6.1.1-py3-none-any.whl", hash = "sha256:6987826d6775056839940041beef5c08cc7e3d71d63149b48e36727f70144dc4"}, {file = "pydocstyle-6.1.1.tar.gz", hash = "sha256:1d41b7c459ba0ee6c345f2eb9ae827cab14a7533a88c5c6f7e94923f72df92dc"}, ] -pyflakes = [] +pyflakes = [ + {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, + {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, +] pygments = [ {file = "Pygments-2.13.0-py3-none-any.whl", hash = "sha256:f643f331ab57ba3c9d89212ee4a2dabc6e94f117cf4eefde99a0574720d14c42"}, {file = "Pygments-2.13.0.tar.gz", hash = "sha256:56a8508ae95f98e2b9bdf93a6be5ae3f7d8af858b43e02c5a2ff083726be40c1"}, @@ -1537,10 +1644,13 @@ pytest-asyncio = [ {file = "pytest_asyncio-0.19.0-py3-none-any.whl", hash = "sha256:7a97e37cfe1ed296e2e84941384bdd37c376453912d397ed39293e0916f521fa"}, ] pytest-cov = [ - {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, - {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, + {file = "pytest-cov-4.0.0.tar.gz", hash = "sha256:996b79efde6433cdbd0088872dbc5fb3ed7fe1578b68cdbba634f14bb8dd0470"}, + {file = "pytest_cov-4.0.0-py3-none-any.whl", hash = "sha256:2feb1b751d66a8bd934e5edfa2e961d11309dc37b73b0eabe73b5945fee20f6b"}, +] +pytz = [ + {file = "pytz-2022.4-py2.py3-none-any.whl", hash = "sha256:2c0784747071402c6e99f0bafdb7da0fa22645f06554c7ae06bf6358897e9c91"}, + {file = "pytz-2022.4.tar.gz", hash = "sha256:48ce799d83b6f8aab2020e369b627446696619e79645419610b9facd909b3174"}, ] -pytz = [] pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, @@ -1549,6 +1659,13 @@ pyyaml = [ {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, + {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, + {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, + {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, + {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, @@ -1584,6 +1701,10 @@ rfc3986 = [ {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, ] +setuptools = [ + {file = "setuptools-65.4.1-py3-none-any.whl", hash = "sha256:1b6bdc6161661409c5f21508763dc63ab20a9ac2f8ba20029aaaa7fdb9118012"}, + {file = "setuptools-65.4.1.tar.gz", hash = "sha256:3050e338e5871e70c72983072fe34f6032ae1cdeeeb67338199c2f74e083a80e"}, +] six = [ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, @@ -1601,18 +1722,21 @@ soupsieve = [ {file = "soupsieve-2.3.2.post1.tar.gz", hash = "sha256:fc53893b3da2c33de295667a0e19f078c14bf86544af307354de5fcf12a3f30d"}, ] sphinx = [ - {file = "Sphinx-5.1.1-py3-none-any.whl", hash = "sha256:309a8da80cb6da9f4713438e5b55861877d5d7976b69d87e336733637ea12693"}, - {file = "Sphinx-5.1.1.tar.gz", hash = "sha256:ba3224a4e206e1fbdecf98a4fae4992ef9b24b85ebf7b584bb340156eaf08d89"}, + {file = "Sphinx-5.2.3.tar.gz", hash = "sha256:5b10cb1022dac8c035f75767799c39217a05fc0fe2d6fe5597560d38e44f0363"}, + {file = "sphinx-5.2.3-py3-none-any.whl", hash = "sha256:7abf6fabd7b58d0727b7317d5e2650ef68765bbe0ccb63c8795fa8683477eaa2"}, ] sphinx-autoapi = [ - {file = "sphinx-autoapi-1.9.0.tar.gz", hash = "sha256:c897ea337df16ad0cde307cbdfe2bece207788dde1587fa4fc8b857d1fc5dcba"}, - {file = "sphinx_autoapi-1.9.0-py2.py3-none-any.whl", hash = "sha256:d217953273b359b699d8cb81a5a72985a3e6e15cfe3f703d9a3c201ffc30849b"}, + {file = "sphinx-autoapi-2.0.0.tar.gz", hash = "sha256:97dcf1b5b54cd0d8efef867594e4a4f3e2d3a2c0ec1e5a891e0a61bc77046006"}, + {file = "sphinx_autoapi-2.0.0-py2.py3-none-any.whl", hash = "sha256:dab2753a38cad907bf4e61473c0da365a26bfbe69fbf5aa6e4f7d48e1cf8a148"}, ] sphinx-copybutton = [ {file = "sphinx-copybutton-0.5.0.tar.gz", hash = "sha256:a0c059daadd03c27ba750da534a92a63e7a36a7736dcf684f26ee346199787f6"}, {file = "sphinx_copybutton-0.5.0-py3-none-any.whl", hash = "sha256:9684dec7434bd73f0eea58dda93f9bb879d24bff2d8b187b1f2ec08dfe7b5f48"}, ] -sphinx-rtd-theme = [] +sphinx-rtd-theme = [ + {file = "sphinx_rtd_theme-0.5.1-py2.py3-none-any.whl", hash = "sha256:fa6bebd5ab9a73da8e102509a86f3fcc36dec04a0b52ea80e5a033b2aba00113"}, + {file = "sphinx_rtd_theme-0.5.1.tar.gz", hash = "sha256:eda689eda0c7301a80cf122dad28b1861e5605cbf455558f3775e1e8200e83a5"}, +] sphinxcontrib-applehelp = [ {file = "sphinxcontrib-applehelp-1.0.2.tar.gz", hash = "sha256:a072735ec80e7675e3f432fcae8610ecf509c5f1869d17e2eecff44389cdbc58"}, {file = "sphinxcontrib_applehelp-1.0.2-py2.py3-none-any.whl", hash = "sha256:806111e5e962be97c29ec4c1e7fe277bfd19e9652fb1a4392105b43e01af885a"}, @@ -1637,17 +1761,13 @@ sphinxcontrib-serializinghtml = [ {file = "sphinxcontrib-serializinghtml-1.1.5.tar.gz", hash = "sha256:aa5f6de5dfdf809ef505c4895e51ef5c9eac17d0f287933eb49ec495280b6952"}, {file = "sphinxcontrib_serializinghtml-1.1.5-py2.py3-none-any.whl", hash = "sha256:352a9a00ae864471d3a7ead8d7d79f5fc0b57e8b3f95e9867eb9eb28999b92fd"}, ] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] tomli = [ {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, ] tox = [ - {file = "tox-3.25.1-py2.py3-none-any.whl", hash = "sha256:c38e15f4733683a9cc0129fba078633e07eb0961f550a010ada879e95fb32632"}, - {file = "tox-3.25.1.tar.gz", hash = "sha256:c138327815f53bc6da4fe56baec5f25f00622ae69ef3fe4e1e385720e22486f9"}, + {file = "tox-3.26.0-py2.py3-none-any.whl", hash = "sha256:bf037662d7c740d15c9924ba23bb3e587df20598697bb985ac2b49bdc2d847f6"}, + {file = "tox-3.26.0.tar.gz", hash = "sha256:44f3c347c68c2c68799d7d44f1808f9d396fc8a1a500cbc624253375c7ae107e"}, ] typed-ast = [ {file = "typed_ast-1.5.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:669dd0c4167f6f2cd9f57041e03c3c2ebf9063d0757dc89f79ba1daa2bfca9d4"}, @@ -1680,17 +1800,20 @@ typer = [ {file = "typer-0.6.1.tar.gz", hash = "sha256:2d5720a5e63f73eaf31edaa15f6ab87f35f0690f8ca233017d7d23d743a91d73"}, ] typing-extensions = [ - {file = "typing_extensions-4.3.0-py3-none-any.whl", hash = "sha256:25642c956049920a5aa49edcdd6ab1e06d7e5d467fc00e0506c44ac86fbfca02"}, - {file = "typing_extensions-4.3.0.tar.gz", hash = "sha256:e6d2677a32f47fc7eb2795db1dd15c1f34eff616bcaf2cfb5e997f854fa1c4a6"}, + {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, + {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] unidecode = [ - {file = "Unidecode-1.3.4-py3-none-any.whl", hash = "sha256:afa04efcdd818a93237574791be9b2817d7077c25a068b00f8cff7baa4e59257"}, - {file = "Unidecode-1.3.4.tar.gz", hash = "sha256:8e4352fb93d5a735c788110d2e7ac8e8031eb06ccbfe8d324ab71735015f9342"}, + {file = "Unidecode-1.3.6-py3-none-any.whl", hash = "sha256:547d7c479e4f377b430dd91ac1275d593308dce0fc464fb2ab7d41f82ec653be"}, + {file = "Unidecode-1.3.6.tar.gz", hash = "sha256:fed09cf0be8cf415b391642c2a5addfc72194407caee4f98719e40ec2a72b830"}, +] +urllib3 = [ + {file = "urllib3-1.26.12-py2.py3-none-any.whl", hash = "sha256:b930dd878d5a8afb066a637fbb35144fe7901e3b209d1cd4f524bd0e9deee997"}, + {file = "urllib3-1.26.12.tar.gz", hash = "sha256:3fa96cf423e6987997fc326ae8df396db2a8b7c667747d47ddd8ecba91f4a74e"}, ] -urllib3 = [] virtualenv = [ - {file = "virtualenv-20.16.4-py3-none-any.whl", hash = "sha256:035ed57acce4ac35c82c9d8802202b0e71adac011a511ff650cbcf9635006a22"}, - {file = "virtualenv-20.16.4.tar.gz", hash = "sha256:014f766e4134d0008dcaa1f95bafa0fb0f575795d07cae50b1bee514185d6782"}, + {file = "virtualenv-20.16.5-py3-none-any.whl", hash = "sha256:d07dfc5df5e4e0dbc92862350ad87a36ed505b978f6c39609dc489eadd5b0d27"}, + {file = "virtualenv-20.16.5.tar.gz", hash = "sha256:227ea1b9994fdc5ea31977ba3383ef296d7472ea85be9d6732e42a91c04e80da"}, ] wrapt = [ {file = "wrapt-1.14.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:1b376b3f4896e7930f1f772ac4b064ac12598d1c38d04907e696cc4d794b43d3"}, @@ -1820,6 +1943,6 @@ yarl = [ {file = "yarl-1.8.1.tar.gz", hash = "sha256:af887845b8c2e060eb5605ff72b6f2dd2aab7a761379373fd89d314f4752abbf"}, ] zipp = [ - {file = "zipp-3.8.1-py3-none-any.whl", hash = "sha256:47c40d7fe183a6f21403a199b3e4192cca5774656965b0a4988ad2f8feb5f009"}, - {file = "zipp-3.8.1.tar.gz", hash = "sha256:05b45f1ee8f807d0cc928485ca40a07cb491cf092ff587c0df9cb1fd154848d2"}, + {file = "zipp-3.9.0-py3-none-any.whl", hash = "sha256:972cfa31bc2fedd3fa838a51e9bc7e64b7fb725a8c00e7431554311f180e9980"}, + {file = "zipp-3.9.0.tar.gz", hash = "sha256:3a7af91c3db40ec72dd9d154ae18e008c69efe8ca88dde4f9a731bb82fe2f9eb"}, ] From 6ef1dac9777a8b4a3a0badf7a847ab8ed33e24b6 Mon Sep 17 00:00:00 2001 From: shred86 Date: Mon, 10 Oct 2022 16:51:37 -0700 Subject: [PATCH 79/84] Fix pydocstyle --- teslajsonpy/car.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 26268f14..0822738f 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -124,7 +124,9 @@ def charge_port_latch(self) -> str: Returns str: Engaged + Other states? + """ return self._vehicle_data.get("charge_state", {}).get("charge_port_latch") @@ -182,8 +184,9 @@ def charging_state(self) -> str: """Return charging state. Returns - str: Charging, Stopped, Complete, Disconnected, NoPower - None: When car is asleep + str: Charging, Stopped, Complete, Disconnected, NoPower + None: When car is asleep + """ return self._vehicle_data.get("charge_state", {}).get("charging_state") @@ -200,6 +203,7 @@ def climate_keeper_mode(self) -> str: str: dog, camp, on, off Not supported on all Tesla models. + """ return self._vehicle_data.get("climate_state", {}).get("climate_keeper_mode") @@ -214,6 +218,7 @@ def defrost_mode(self) -> int: Returns int: 2 (on), 0 (off) + """ return self._vehicle_data.get("climate_state", {}).get("defrost_mode", 0) @@ -299,6 +304,7 @@ def is_frunk_closed(self) -> bool: Returns bool: True (0), False (255) + """ response = self._vehicle_data.get("vehicle_state", {}).get("ft") return True if response == 0 else False @@ -324,6 +330,7 @@ def is_trunk_closed(self) -> bool: Returns bool: True (0), False (1-255) + """ response = self._vehicle_data.get("vehicle_state", {}).get("rt") return True if response == 0 else False @@ -411,6 +418,7 @@ def rear_seat_heaters(self) -> int: Returns int: 0 (no rear heated seats), int: ? (rear heated seats) + """ return self._vehicle_data.get("vehicle_config", {}).get("rear_seat_heaters") @@ -450,6 +458,7 @@ def third_row_seats(self) -> str: Returns str: None + """ return self._vehicle_data.get("vehicle_config", {}).get("third_row_seats") @@ -461,7 +470,7 @@ def time_to_full_charge(self) -> float: async def _send_command( self, name: str, *, path_vars: dict, wake_if_asleep: bool = False, **kwargs ) -> dict: - """Wrapper for sending commands to the Tesla API.""" + """Wrap commands sent to Tesla API.""" _LOGGER.debug("Sending command: %s", name) data = await self._controller.api( name, path_vars=path_vars, wake_if_asleep=wake_if_asleep, **kwargs @@ -553,9 +562,10 @@ async def remote_seat_heater_request(self, level: int, seat_id: int) -> None: """Send command to change seat heat. Args - levels: 0 (off), 1 (low), 2 (medium), 3 (high) + level: 0 (off), 1 (low), 2 (medium), 3 (high) seat_id: 0 (front left), 1 (front right), 2 (rear left), 4 (rear center) 5 (rear right), 6 (third row left), 7 (third row right) + """ data = await self._send_command( @@ -610,6 +620,7 @@ async def set_cabin_overheat_protection(self, option: str) -> None: Args option: "Off", "No A/C", "On" + """ if option == "Off": @@ -638,6 +649,7 @@ async def set_climate_keeper_mode(self, keeper_id: int) -> None: Args keeper_id: 1 (keep on), 2 (dog mode), 3 (camp mode) + """ data = await self._send_command( "SET_CLIMATE_KEEPER_MODE", @@ -669,8 +681,8 @@ async def set_hvac_mode(self, value: str) -> None: """Send command to set HVAC mode. Args - "off" - "on" + value: on, off + """ if value == "off": data = await self._send_command( @@ -705,6 +717,7 @@ async def set_max_defrost(self, state: int) -> None: Args state: 2 = on, 0 = off + """ data = await self._send_command( "MAX_DEFROST", From b9184e2dea08434af00f97019f2e0881c3941b15 Mon Sep 17 00:00:00 2001 From: shred86 Date: Mon, 10 Oct 2022 16:55:40 -0700 Subject: [PATCH 80/84] Pydocstyle updates --- teslajsonpy/controller.py | 1 + teslajsonpy/energy.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 0868a352..7ee13b4c 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -545,6 +545,7 @@ async def generate_car_objects( Args wake_if_asleep (bool, optional): Wake up vehicles if asleep. filtered_vins (list, optional): If not empty, filters the cars by the provided VINs. + """ for car in self._vehicle_list: vin = car["vin"] diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index 1ecce8c3..24f85e1d 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -41,7 +41,7 @@ def has_solar(self) -> bool: @property def id(self) -> str: - """Return id (aka battery_id).""" + """Return battery_id.""" return self._energysite.get("id") @property @@ -52,7 +52,7 @@ def resource_type(self) -> str: async def _send_command( self, name: str, *, path_vars: dict, wake_if_asleep: bool = False, **kwargs ) -> dict: - """Wrapper for sending commands to the Tesla API.""" + """Wrap commands sent to Tesla API.""" _LOGGER.debug("Sending command: %s", name) data = await self._api( name, path_vars=path_vars, wake_if_asleep=wake_if_asleep, **kwargs From 3e306e94bd50fe2f6b2a2680c85cdc3aa88d2111 Mon Sep 17 00:00:00 2001 From: shred86 Date: Mon, 10 Oct 2022 17:19:21 -0700 Subject: [PATCH 81/84] Fix lint errors --- teslajsonpy/__init__.py | 2 ++ teslajsonpy/car.py | 9 +++++++-- teslajsonpy/connection.py | 9 +++++---- teslajsonpy/controller.py | 4 ++-- teslajsonpy/energy.py | 16 +++++----------- 5 files changed, 21 insertions(+), 19 deletions(-) diff --git a/teslajsonpy/__init__.py b/teslajsonpy/__init__.py index 3d3cd99f..4dd66944 100644 --- a/teslajsonpy/__init__.py +++ b/teslajsonpy/__init__.py @@ -5,8 +5,10 @@ For more details about this api, please refer to the documentation at https://github.com/zabuldon/teslajsonpy """ +from teslajsonpy.car import TeslaCar from teslajsonpy.connection import Connection from teslajsonpy.controller import Controller +from teslajsonpy.energy import EnergySite, PowerwallSite, SolarPowerwallSite, SolarSite from teslajsonpy.exceptions import ( RetryLimitError, IncompleteCredentials, diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index 0822738f..bb6ef873 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -25,6 +25,7 @@ class TeslaCar: + # pylint: disable=too-many-public-methods """Represents a Tesla car. This class shouldn't be instantiated directly; it will be instantiated @@ -48,6 +49,7 @@ def display_name(self) -> str: @property def id(self) -> int: + # pylint: disable=invalid-name """Return id.""" return self._car.get("id") @@ -73,6 +75,7 @@ def data_available(self) -> bool: # Only return True if data specifically from VEHICLE_DATA endpoint is available if self._vehicle_data.get("vehicle_config", {}): return True + return None @property def battery_level(self) -> float: @@ -282,6 +285,7 @@ def in_service(self) -> bool: """Return car in_service.""" if self.data_available: return self._vehicle_data.get("in_service") + return None @property def inside_temp(self) -> float: @@ -307,7 +311,7 @@ def is_frunk_closed(self) -> bool: """ response = self._vehicle_data.get("vehicle_state", {}).get("ft") - return True if response == 0 else False + return response == 0 @property def is_in_gear(self) -> bool: @@ -333,7 +337,7 @@ def is_trunk_closed(self) -> bool: """ response = self._vehicle_data.get("vehicle_state", {}).get("rt") - return True if response == 0 else False + return response == 0 @property def is_on(self) -> bool: @@ -584,6 +588,7 @@ def get_seat_heater_status(self, seat_id: int) -> int: seat_id = f"seat_heater_{SEAT_ID_MAP[seat_id]}" if self.data_available: return self._vehicle_data.get("climate_state").get(seat_id) + return None async def schedule_software_update(self, offset_sec: Optional[int] = 0) -> None: """Send command to install software update.""" diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index 2b380a27..fab78b12 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -8,19 +8,20 @@ For more details about this api, please refer to the documentation at https://github.com/zabuldon/teslajsonpy """ -import aiohttp + import asyncio import base64 -from bs4 import BeautifulSoup +import time import calendar import datetime import hashlib -import httpx import json import logging import secrets -import time from typing import Dict, Text +from bs4 import BeautifulSoup +import aiohttp +import httpx import yarl from yarl import URL diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 7ee13b4c..89c495b1 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -9,13 +9,13 @@ https://github.com/zabuldon/teslajsonpy """ import asyncio -import backoff -import httpx import json import logging import pkgutil import time from typing import Callable, Dict, List, Optional, Text +import backoff +import httpx import wrapt from yarl import URL diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index 24f85e1d..de361938 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -41,6 +41,7 @@ def has_solar(self) -> bool: @property def id(self) -> str: + # pylint: disable=invalid-name """Return battery_id.""" return self._energysite.get("id") @@ -137,6 +138,7 @@ def battery_power(self) -> float: """Return battery power in Watts.""" if self._battery_data.get("power_reading"): return self._battery_data["power_reading"][0]["battery_power"] + return None @property def data_available(self) -> bool: @@ -153,6 +155,7 @@ def grid_power(self) -> float: """Return grid power in Watts.""" if self._battery_data.get("power_reading"): return self._battery_data["power_reading"][0]["grid_power"] + return None @property def grid_status(self) -> str: @@ -164,6 +167,7 @@ def load_power(self) -> float: """Return load power in Watts.""" if self._battery_data.get("power_reading"): return self._battery_data["power_reading"][0]["load_power"] + return None @property def operation_mode(self) -> str: @@ -187,6 +191,7 @@ def solar_power(self) -> float: """Return solar power in Watts.""" if self._battery_data.get("power_reading"): return self._battery_data["power_reading"][0]["solar_power"] + return None @property def version(self) -> float: @@ -227,17 +232,6 @@ class SolarPowerwallSite(PowerwallSite): by :meth:`teslajsonpy.controller.generate_energysite_objects`. """ - def __init__( - self, - api: Callable, - energysite: dict, - site_config: dict, - battery_data: dict, - battery_summary: dict, - ) -> None: - """Initialize SolarPowerwallSite.""" - super().__init__(api, energysite, site_config, battery_data, battery_summary) - @property def export_rule(self) -> str: """Return energy export rule setting.""" From 4a9e03148ffe4d185c9b231b7e36f210fa5559ee Mon Sep 17 00:00:00 2001 From: shred86 Date: Tue, 11 Oct 2022 07:56:34 -0700 Subject: [PATCH 82/84] Add header and license identifiers --- teslajsonpy/__version__.py | 3 --- teslajsonpy/car.py | 8 +++++++- teslajsonpy/connection.py | 6 +----- teslajsonpy/controller.py | 5 +---- teslajsonpy/energy.py | 8 +++++++- 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/teslajsonpy/__version__.py b/teslajsonpy/__version__.py index 06829be6..f13b2f1d 100644 --- a/teslajsonpy/__version__.py +++ b/teslajsonpy/__version__.py @@ -2,10 +2,7 @@ """ Python Package for controlling Tesla API. -This is the version info. - For more details about this api, please refer to the documentation at https://github.com/zabuldon/teslajsonpy """ - __version__ = "2.4.5" diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index bb6ef873..c6ab0432 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -1,4 +1,10 @@ -"""Tesla car.""" +# 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 from typing import Optional diff --git a/teslajsonpy/connection.py b/teslajsonpy/connection.py index fab78b12..a091fe1a 100644 --- a/teslajsonpy/connection.py +++ b/teslajsonpy/connection.py @@ -1,14 +1,10 @@ +# SPDX-License-Identifier: Apache-2.0 """ Python Package for controlling Tesla API. -SPDX-License-Identifier: Apache-2.0 - -Underlying connection logic. - For more details about this api, please refer to the documentation at https://github.com/zabuldon/teslajsonpy """ - import asyncio import base64 import time diff --git a/teslajsonpy/controller.py b/teslajsonpy/controller.py index 89c495b1..a8dcfdf1 100644 --- a/teslajsonpy/controller.py +++ b/teslajsonpy/controller.py @@ -1,10 +1,7 @@ +# SPDX-License-Identifier: Apache-2.0 """ Python Package for controlling Tesla API. -SPDX-License-Identifier: Apache-2.0 - -Controller to control access to the Tesla API. - For more details about this api, please refer to the documentation at https://github.com/zabuldon/teslajsonpy """ diff --git a/teslajsonpy/energy.py b/teslajsonpy/energy.py index de361938..00c916af 100644 --- a/teslajsonpy/energy.py +++ b/teslajsonpy/energy.py @@ -1,4 +1,10 @@ -"""Tesla energy site.""" +# 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 from typing import Callable From 4b836e2657d9605a1e81cda587df00ece1e9455e Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 11 Oct 2022 10:40:32 -0700 Subject: [PATCH 83/84] style: fix pydocs indent --- teslajsonpy/car.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/teslajsonpy/car.py b/teslajsonpy/car.py index c6ab0432..e564162e 100644 --- a/teslajsonpy/car.py +++ b/teslajsonpy/car.py @@ -574,7 +574,7 @@ async def remote_seat_heater_request(self, level: int, seat_id: int) -> None: Args level: 0 (off), 1 (low), 2 (medium), 3 (high) seat_id: 0 (front left), 1 (front right), 2 (rear left), 4 (rear center) - 5 (rear right), 6 (third row left), 7 (third row right) + 5 (rear right), 6 (third row left), 7 (third row right) """ From d9da828a949e99cf9f713b03ea5ae43151f23850 Mon Sep 17 00:00:00 2001 From: "Alan D. Tse" Date: Tue, 11 Oct 2022 10:41:26 -0700 Subject: [PATCH 84/84] docs: generate docs --- docs/html/.doctrees/environment.pickle | Bin 100425 -> 63693 bytes docs/html/.doctrees/index.doctree | Bin 21980 -> 21966 bytes .../teslajsonpy.__version__.doctree | Bin 4297 -> 4087 bytes .../teslajsonpy/teslajsonpy.car.doctree | Bin 0 -> 3987 bytes .../teslajsonpy.connection.doctree | Bin 4477 -> 4078 bytes .../teslajsonpy/teslajsonpy.const.doctree | Bin 4069 -> 4013 bytes .../teslajsonpy.controller.doctree | Bin 4513 -> 3916 bytes .../.doctrees/teslajsonpy/teslajsonpy.doctree | Bin 792712 -> 627355 bytes .../teslajsonpy/teslajsonpy.energy.doctree | Bin 0 -> 4026 bytes .../teslajsonpy.exceptions.doctree | Bin 4134 -> 4078 bytes .../teslajsonpy.teslaproxy.doctree | Bin 4637 -> 4581 bytes ...bc7738a653e6b377a0ecd13ac7f7484bedad11.svg | 21 + ...6386a5234fea17fedeb56cf70872e286545e64.svg | 51 + ...b6f0b12ec45c5414897664f71e38a513d053d0.svg | 21 + ...cc9d6fd208a1891895f19fca09248de5ca339f.svg | 21 + ...77e39150bae1f6ca80df3344fe0b0241b09280.svg | 36 + ...9b4170781ce0b4e800e5675fdd5de65016c0e2.svg | 36 + ...b811f84e619cd52e2025b473822a54db9f3ec8.svg | 36 + ...9dc26e9acdebb26819697bc4f39b77066ed854.svg | 36 + ...8cd69abc9505aca94c8f8b91a1c17f81333abe.svg | 36 + ...90ad758ca83c619b0aa35f221997c855e80113.svg | 21 + ...e061804218301f8d8f2d415586d1b456ddee87.svg | 36 + ...e9c65668e9f0bb1a9a274c79c2e9024cc89c59.svg | 21 + .../teslajsonpy/teslajsonpy.car.rst.txt | 10 + .../teslajsonpy/teslajsonpy.energy.rst.txt | 10 + .../_sources/teslajsonpy/teslajsonpy.rst.txt | 220 +- docs/html/genindex.html | 654 ++--- docs/html/index.html | 6 +- docs/html/objects.inv | Bin 2480 -> 2074 bytes docs/html/py-modindex.html | 85 +- docs/html/search.html | 3 +- docs/html/searchindex.js | 2 +- .../teslajsonpy/teslajsonpy.__version__.html | 8 +- docs/html/teslajsonpy/teslajsonpy.car.html | 125 + .../teslajsonpy/teslajsonpy.connection.html | 9 +- docs/html/teslajsonpy/teslajsonpy.const.html | 3 +- .../teslajsonpy/teslajsonpy.controller.html | 9 +- docs/html/teslajsonpy/teslajsonpy.energy.html | 125 + .../teslajsonpy/teslajsonpy.exceptions.html | 11 +- docs/html/teslajsonpy/teslajsonpy.html | 2593 +++++++---------- .../teslajsonpy/teslajsonpy.teslaproxy.html | 7 +- docs/requirements.txt | 109 +- docs/teslajsonpy/teslajsonpy.car.rst | 4 +- docs/teslajsonpy/teslajsonpy.energy.rst | 4 +- docs/teslajsonpy/teslajsonpy.rst | 51 +- 45 files changed, 2086 insertions(+), 2334 deletions(-) create mode 100644 docs/html/.doctrees/teslajsonpy/teslajsonpy.car.doctree create mode 100644 docs/html/.doctrees/teslajsonpy/teslajsonpy.energy.doctree create mode 100644 docs/html/_images/inheritance-0dbc7738a653e6b377a0ecd13ac7f7484bedad11.svg create mode 100644 docs/html/_images/inheritance-4f6386a5234fea17fedeb56cf70872e286545e64.svg create mode 100644 docs/html/_images/inheritance-71b6f0b12ec45c5414897664f71e38a513d053d0.svg create mode 100644 docs/html/_images/inheritance-77cc9d6fd208a1891895f19fca09248de5ca339f.svg create mode 100644 docs/html/_images/inheritance-7877e39150bae1f6ca80df3344fe0b0241b09280.svg create mode 100644 docs/html/_images/inheritance-7d9b4170781ce0b4e800e5675fdd5de65016c0e2.svg create mode 100644 docs/html/_images/inheritance-81b811f84e619cd52e2025b473822a54db9f3ec8.svg create mode 100644 docs/html/_images/inheritance-8a9dc26e9acdebb26819697bc4f39b77066ed854.svg create mode 100644 docs/html/_images/inheritance-928cd69abc9505aca94c8f8b91a1c17f81333abe.svg create mode 100644 docs/html/_images/inheritance-a090ad758ca83c619b0aa35f221997c855e80113.svg create mode 100644 docs/html/_images/inheritance-cae061804218301f8d8f2d415586d1b456ddee87.svg create mode 100644 docs/html/_images/inheritance-f7e9c65668e9f0bb1a9a274c79c2e9024cc89c59.svg create mode 100644 docs/html/_sources/teslajsonpy/teslajsonpy.car.rst.txt create mode 100644 docs/html/_sources/teslajsonpy/teslajsonpy.energy.rst.txt create mode 100644 docs/html/teslajsonpy/teslajsonpy.car.html create mode 100644 docs/html/teslajsonpy/teslajsonpy.energy.html diff --git a/docs/html/.doctrees/environment.pickle b/docs/html/.doctrees/environment.pickle index a69374e81487269a38e9a004a21b1e65de2ac0f2..fe89b37bd96f5aa233ace3f1355d3c02caf4ae2f 100644 GIT binary patch literal 63693 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

3>_+9PJOlFu_4f6WKbA7E_{L&byphhqG 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 Ud|C(ds{nk(esmhOaxhc*e|y2d5C8xG diff --git a/docs/html/.doctrees/index.doctree b/docs/html/.doctrees/index.doctree index a56639aa6f1bbcbc32ed5bf22a9cf802efa48ea9..408e335f5dd0809945e9fb0a2bf5e4da7d285d83 100644 GIT binary patch delta 37 tcmcb!n(^FfMwSNFsfRbR9AOgB(hn_8Eh^Se%t_2kDJf3f{EX>a5C9qp4>$k- delta 51 zcmX@Nn(@wRMwSNFsTVi09AT2P(9g)vP1P?d&C5$I(l5_1%GNJQEzU{ID$dU01y06OR=Z diff --git a/docs/html/.doctrees/teslajsonpy/teslajsonpy.__version__.doctree b/docs/html/.doctrees/teslajsonpy/teslajsonpy.__version__.doctree index af537b108081aa00ce6be3180a7ead9cd3040a28..c48416ba09d86651906d23da08d8215606c7142e 100644 GIT binary patch delta 279 zcmX@9_+6g0fpzK|{*A0F7zI4^LyJ?3iuDt767y0@ic=?_W_%zJjV}0*S#q*GpT%S; z=B>CSoj2cQkz-_xp8SWkhcRvPLT0te>)E(?GfFcIGEy=WGNMb9QYWutR+xN-MPTzc zwpu2}Wt&SlKQc2nWYkX1;8o{s@XL7RSDK+#J4FL1GKbegbAvmA>&BV*=dJJue?5}=>~Z&_)EK}Hc!O=f9Q>g4%s zlDr@RuMDM(s0?Lq*3Eis?M#eYHgDtj$jscA(LH$;k2-IkU&ar=(hN0_1>KYH@py>+ z%~0yW>th$jpOdHa<}mV4X5kBP$>L#TU?@#W%uoRtp$7EK&%_Lk*eM!49Lf24C8>EO o#Z!7%bMliDbEc$bXaVJaBxdLUDZSV!K<8y~0kvYF#>0n}n~Z&9R8d|I6Q&$J!Tq?JFt^97 z)pAn7V(@!MriOg%B^fgYLVR}$me*gqX;rQU6P8?PJ|&&X^(>pwlovNeB#vQQyoW%F zJ8zUKd3DF%} z&&aP&TYjCt%&(S-5xG%bo{_6KOfiep^k;j+#cjCQcYWM1n`u^xmYeRw`n@CbnAIqZ?R26Lyh8UiRTkeZjv+XK@UztTj>Ff4I5psg>ya1;y zcn*>K;Me#Jm;Yz|6>7RT%(Q)&7o)~?amY$bF9ZQ7B{!-G|0R@(uZRLx$XG2n5%<>k z@)XJD`GS(xrie~FYqQiIEbi0?7k6utdD+n$f)#h^=-Y0PRGO$bUJ~!YLQ)g3+cn%; zq$g_7F<~(jm8nMF@*?%F`!|+V;HKqm9gSUo#tRe(tF*@;W#AFZ@)CpndYc%wFEHF| zhLz4RI5N8 zhbV-z3N(&zrx;&B-{Ed(o+zEhDK7yIY(rIR?7Cq2DSSjYWEz*q9y#}D0|ux112~XGfyi|tA?Uex{iH#pg zBWrvf#i=xGl940LDSL}-5O>`HoH4+EpkEk0Y}DeZ%tA?auhSPcL4QCLjq4?ADeik8 z4cc-Dd>&Iu0XD@9D9mIot^f1258R;SWxV7)a^`>}6O@9OGQ+*Opg)KWsxVd2q0+OF zcQ;8h8~~cJkyH)aHiXwIPA$K#xjfF$D$|tZzDJ(&}DGsWsuq<-_l12g%Ar^w$+96@kyvB}K@S{E_TQL|%owf_a10JG^*AeXQH9xdq7#Xrc#{NERDgre_>ng&y`8hXm9tC>AhEscU@# z_FI$Ioh`($HeC*=AW>5cd5?tk_lbBap5R#!s2W?eJ}cKJJ$qRq{?r0L9g82wmAFIS zPtf(=LoM#sXIoKw^I}*Px_fLYq`0`PcuswfYH%>{wOGFfzS-X1+6r`DbB=d!Evw;*h=yo`n z1RYPIUeoT;^>o5^er!;+|FhPb{uE+)(z>LDG7k@vAJ?k>zr{PD+V@udMCA_K$r#XZ zm`J2F!3hYdCB(T4y&86HNv-z)iC`}CiOQDw9aLZ25H7X*YZm*9UBja5YxYq)wWVTx zbJJEcv*q?s5fW7q-+Q;8;B|mRfe%1%@AF3f9fJ9-{|nd+5eHA=@n?|F^@hv)SQGCr d#UTavWszi6N)#}o?AuGJb~Q9l{Sf}IH)ya2QxHr#cuV-Rh zwpp6%BQtYDM(t#NK6Ty(zl>LYr5S3qQ#61gZhY>d8SgWcdT_YIk@59pLH-;@#>sX3 z0d^VxN|O>ZR6ttQVy9%hPR!7Voubjhk({4blA2diJf(*{#g>cvh0Ix0&7sCIKFUn(O2Fca9HzIp Xr7SkzW9H+;BR}~bGxug6-b5wMr9P9Wn|VulWorx!Z~WZ$37HUg=P08X1q7ytkO delta 603 zcmX>jw@{h2fpzLM!HulT7==RhGxBp&^@~dL@=}ZR%kzt}C!b-In0%b^qEI?^sT+(u z{QAlHc_l^pIXS6CdIgnMlV>vBB~7`;!8BEDwkDZbs-9wbx)+vosYNu#qux7+&C}c!W_F&d!%ZSQQ z_GX?upINkC&l&D?g_3*)s7DkMlao`6i$NkK8L0{(sl_>o3XTDudN8X=)rN3IHqaHB z8HyRq8A^T`%98_`RUpn`-8`RJlaaA}@=2B+#wMUM)h5@la&fhkW*B5N0QHnlp1`WX z$i8_eYbO)qq0P=5ADNlwWXzmw&Z90c$1jVGk%7UlG(!#K)tQsCdEBM4xPd|$N^UShLuHS|`8Z$zhb89LXCXmL&z`mnJ1}Iy4}l0 diff --git a/docs/html/.doctrees/teslajsonpy/teslajsonpy.doctree b/docs/html/.doctrees/teslajsonpy/teslajsonpy.doctree index ecc13b200096da0a263968542ced52cf1962f510..459c599ba34bded13791bfa794d23bce7d032695 100644 GIT binary patch literal 627355 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

nz*uW9lI0g)X^Hzu_0O; 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 z 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 C>awr^ literal 792712 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%0DflO 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 diff --git a/docs/html/.doctrees/teslajsonpy/teslajsonpy.energy.doctree b/docs/html/.doctrees/teslajsonpy/teslajsonpy.energy.doctree new file mode 100644 index 0000000000000000000000000000000000000000..0527871995620589963b6aba6ebe5dfe56b5cf12 GIT binary patch literal 4026 zcmcgv-;W!&5x(Kae0H6tbqs8i*0~fpDGvc0T%QU9G1_YdH%Ri(!dO$gl*ruf za!Yb;DS!d@A%G3=E$qM0*ZyD0L;r!I=r`Pzv^vLb1EduoU^yHPIm2(h89wX$_3uYp z;pdJ_DP%DpvNSDJ%8hrssYQ7u>vnNJXI^o`JK_d||OEoY2L+=Kp zZe?bM&HA3St0j}#yPxJNtup?r=;htK`$;`{ z`W>YNh_+!Fj4vFOE#&pR26MxhDe@>Tkhi@X4`dE|6mpor9ZW= zi|`YC=LP6)Nr8ymyWhuWxcp!1ud&m`VW#cFyc#>Mi$hjgdLal#DY;Qi_((rG~6rhk^|drkW`vjI$m+| z!BSFlv)eV?TBIt9)G;A4RGq0t4)Y@QuKPL5Dsa~7&W^^eKO;AoRoY{aGBAr}d5OV( zy-f_;7Z~m}!%Alu9a@#d#N{-L?K;GV3ly5EQnpZ*w42Y@9 z7unkqSvFn2Ir|=QskZD{ZhveL& zLm05is(3AOS0oU1m{xfiuTY1m27v_Nbipz*coZZEZDHu{*P_z!Dpm13Cp=x_^HLce zw^s&;BsP95jjZu`6sOX#Nk)z|r|d1WLELo*aK-@tfrMf7uu+SrG7F{Jy+L2t1g!%R zHLjPerMT~Xba1O7@Oexr1=tibpfHoUwEp+k-gSeLm+^}I$e9C@Oi&PF$_)4Bk^~_( zD9Th(hf2>z-rXe4Z~$n=mgl%0i|g2h{PY(ee291s^fb9-o+NAYcb4Xnsf_S?!Szsb zI}Ljv=SRZk88+)%HbDxdR`~N>cN>7r<5h019EeRx2zlYInMGmQJQi{$GW-GFtC=F) zADEmW%l=#;sSN^kBaG`|oT-F0p}n05G!+RhzD_noIfM|aFRBt-P#+wc+lB;Q06i*^ z#S|D=vch%Z2~$lj>B*eHn<-YOBDJCy9+=4|JVpL4a0J1*#U?isXkElSN9B&uvTGh1 z(&z5DX}ST{Y+7^L3wKak2<8n|@9^R=wYYA#<`yI~po!j4B3W!~nVxZQ724Zl91>8o zpjf~trH1w?*l$BxH@y(U+H^IbvPAtd)IX>4@{j*ZoAoV@+OevX z$;&6jQ-+v`$aYnrI_}NkFf^f5B+kzEq9{5GpgXI3(APW{zS!R0+6tszGY@+3RZU#e z(Gab0i1)o^=?Z;v)N=} Y=B-#IH~(k$H3%fpzLF!HulT7zNz*LyJ?3iuDt767y0@ic=?_WV|a7i7vR8QF3wuKkH-` b7Q@NH%v*6P*bG#_jZ+q=-FovH{z4`IBNHtc delta 132 zcmaE=JXeLafpw~o&_>o}j6$LM8Tq-X`bDLAd8tME<@rU~llL%7PCmhSK`0%Yl-%TV b%(j!iGi}8oy%{LYjYAHo)qQgbe<2e9wQMnQ diff --git a/docs/html/_images/inheritance-0dbc7738a653e6b377a0ecd13ac7f7484bedad11.svg b/docs/html/_images/inheritance-0dbc7738a653e6b377a0ecd13ac7f7484bedad11.svg new file mode 100644 index 00000000..5ea77e30 --- /dev/null +++ b/docs/html/_images/inheritance-0dbc7738a653e6b377a0ecd13ac7f7484bedad11.svg @@ -0,0 +1,21 @@ + + + + + + +inheritance12a151631b + + +TeslaCar + + +TeslaCar + + + + + diff --git a/docs/html/_images/inheritance-4f6386a5234fea17fedeb56cf70872e286545e64.svg b/docs/html/_images/inheritance-4f6386a5234fea17fedeb56cf70872e286545e64.svg new file mode 100644 index 00000000..c4d61aa4 --- /dev/null +++ b/docs/html/_images/inheritance-4f6386a5234fea17fedeb56cf70872e286545e64.svg @@ -0,0 +1,51 @@ + + + + + + +inheritance89bd98568d + + +EnergySite + + +EnergySite + + + + + +PowerwallSite + + +PowerwallSite + + + + + +EnergySite->PowerwallSite + + + + + +SolarPowerwallSite + + +SolarPowerwallSite + + + + + +PowerwallSite->SolarPowerwallSite + + + + + diff --git a/docs/html/_images/inheritance-71b6f0b12ec45c5414897664f71e38a513d053d0.svg b/docs/html/_images/inheritance-71b6f0b12ec45c5414897664f71e38a513d053d0.svg new file mode 100644 index 00000000..9a141412 --- /dev/null +++ b/docs/html/_images/inheritance-71b6f0b12ec45c5414897664f71e38a513d053d0.svg @@ -0,0 +1,21 @@ + + + + + + +inheritance7cdf7be845 + + +EnergySite + + +EnergySite + + + + + diff --git a/docs/html/_images/inheritance-77cc9d6fd208a1891895f19fca09248de5ca339f.svg b/docs/html/_images/inheritance-77cc9d6fd208a1891895f19fca09248de5ca339f.svg new file mode 100644 index 00000000..3ad700c2 --- /dev/null +++ b/docs/html/_images/inheritance-77cc9d6fd208a1891895f19fca09248de5ca339f.svg @@ -0,0 +1,21 @@ + + + + + + +inheritance087f428942 + + +Connection + + +Connection + + + + + diff --git a/docs/html/_images/inheritance-7877e39150bae1f6ca80df3344fe0b0241b09280.svg b/docs/html/_images/inheritance-7877e39150bae1f6ca80df3344fe0b0241b09280.svg new file mode 100644 index 00000000..eaa3bd0d --- /dev/null +++ b/docs/html/_images/inheritance-7877e39150bae1f6ca80df3344fe0b0241b09280.svg @@ -0,0 +1,36 @@ + + + + + + +inheritancec0913a979f + + +EnergySite + + +EnergySite + + + + + +PowerwallSite + + +PowerwallSite + + + + + +EnergySite->PowerwallSite + + + + + diff --git a/docs/html/_images/inheritance-7d9b4170781ce0b4e800e5675fdd5de65016c0e2.svg b/docs/html/_images/inheritance-7d9b4170781ce0b4e800e5675fdd5de65016c0e2.svg new file mode 100644 index 00000000..0f0fd946 --- /dev/null +++ b/docs/html/_images/inheritance-7d9b4170781ce0b4e800e5675fdd5de65016c0e2.svg @@ -0,0 +1,36 @@ + + + + + + +inheritancec2ed502f44 + + +TeslaException + + +TeslaException + + + + + +UnknownPresetMode + + +UnknownPresetMode + + + + + +TeslaException->UnknownPresetMode + + + + + diff --git a/docs/html/_images/inheritance-81b811f84e619cd52e2025b473822a54db9f3ec8.svg b/docs/html/_images/inheritance-81b811f84e619cd52e2025b473822a54db9f3ec8.svg new file mode 100644 index 00000000..d98c932e --- /dev/null +++ b/docs/html/_images/inheritance-81b811f84e619cd52e2025b473822a54db9f3ec8.svg @@ -0,0 +1,36 @@ + + + + + + +inheritance8fc76f5186 + + +AuthCaptureProxy + + +AuthCaptureProxy + + + + + +TeslaProxy + + +TeslaProxy + + + + + +AuthCaptureProxy->TeslaProxy + + + + + diff --git a/docs/html/_images/inheritance-8a9dc26e9acdebb26819697bc4f39b77066ed854.svg b/docs/html/_images/inheritance-8a9dc26e9acdebb26819697bc4f39b77066ed854.svg new file mode 100644 index 00000000..85448e29 --- /dev/null +++ b/docs/html/_images/inheritance-8a9dc26e9acdebb26819697bc4f39b77066ed854.svg @@ -0,0 +1,36 @@ + + + + + + +inheritancefae8dcd8d6 + + +RetryLimitError + + +RetryLimitError + + + + + +TeslaException + + +TeslaException + + + + + +TeslaException->RetryLimitError + + + + + diff --git a/docs/html/_images/inheritance-928cd69abc9505aca94c8f8b91a1c17f81333abe.svg b/docs/html/_images/inheritance-928cd69abc9505aca94c8f8b91a1c17f81333abe.svg new file mode 100644 index 00000000..9d863d01 --- /dev/null +++ b/docs/html/_images/inheritance-928cd69abc9505aca94c8f8b91a1c17f81333abe.svg @@ -0,0 +1,36 @@ + + + + + + +inheritancee0516ce7cc + + +EnergySite + + +EnergySite + + + + + +SolarSite + + +SolarSite + + + + + +EnergySite->SolarSite + + + + + diff --git a/docs/html/_images/inheritance-a090ad758ca83c619b0aa35f221997c855e80113.svg b/docs/html/_images/inheritance-a090ad758ca83c619b0aa35f221997c855e80113.svg new file mode 100644 index 00000000..7631346a --- /dev/null +++ b/docs/html/_images/inheritance-a090ad758ca83c619b0aa35f221997c855e80113.svg @@ -0,0 +1,21 @@ + + + + + + +inheritancea58a391388 + + +TeslaException + + +TeslaException + + + + + diff --git a/docs/html/_images/inheritance-cae061804218301f8d8f2d415586d1b456ddee87.svg b/docs/html/_images/inheritance-cae061804218301f8d8f2d415586d1b456ddee87.svg new file mode 100644 index 00000000..058609c9 --- /dev/null +++ b/docs/html/_images/inheritance-cae061804218301f8d8f2d415586d1b456ddee87.svg @@ -0,0 +1,36 @@ + + + + + + +inheritance49d735891d + + +IncompleteCredentials + + +IncompleteCredentials + + + + + +TeslaException + + +TeslaException + + + + + +TeslaException->IncompleteCredentials + + + + + diff --git a/docs/html/_images/inheritance-f7e9c65668e9f0bb1a9a274c79c2e9024cc89c59.svg b/docs/html/_images/inheritance-f7e9c65668e9f0bb1a9a274c79c2e9024cc89c59.svg new file mode 100644 index 00000000..70b4ae36 --- /dev/null +++ b/docs/html/_images/inheritance-f7e9c65668e9f0bb1a9a274c79c2e9024cc89c59.svg @@ -0,0 +1,21 @@ + + + + + + +inheritancef1ba0f48d0 + + +Controller + + +Controller + + + + + diff --git a/docs/html/_sources/teslajsonpy/teslajsonpy.car.rst.txt b/docs/html/_sources/teslajsonpy/teslajsonpy.car.rst.txt new file mode 100644 index 00000000..d33380f2 --- /dev/null +++ b/docs/html/_sources/teslajsonpy/teslajsonpy.car.rst.txt @@ -0,0 +1,10 @@ +=================== +``teslajsonpy.car`` +=================== + +.. automodule:: teslajsonpy.car + + .. contents:: + :local: + +.. currentmodule:: teslajsonpy.car diff --git a/docs/html/_sources/teslajsonpy/teslajsonpy.energy.rst.txt b/docs/html/_sources/teslajsonpy/teslajsonpy.energy.rst.txt new file mode 100644 index 00000000..969a71df --- /dev/null +++ b/docs/html/_sources/teslajsonpy/teslajsonpy.energy.rst.txt @@ -0,0 +1,10 @@ +====================== +``teslajsonpy.energy`` +====================== + +.. automodule:: teslajsonpy.energy + + .. contents:: + :local: + +.. currentmodule:: teslajsonpy.energy diff --git a/docs/html/_sources/teslajsonpy/teslajsonpy.rst.txt b/docs/html/_sources/teslajsonpy/teslajsonpy.rst.txt index bb64b34e..e997ba14 100644 --- a/docs/html/_sources/teslajsonpy/teslajsonpy.rst.txt +++ b/docs/html/_sources/teslajsonpy/teslajsonpy.rst.txt @@ -14,11 +14,12 @@ Submodules .. toctree:: teslajsonpy.__version__ + teslajsonpy.car teslajsonpy.connection teslajsonpy.const teslajsonpy.controller + teslajsonpy.energy teslajsonpy.exceptions - teslajsonpy.homeassistant teslajsonpy.teslaproxy .. currentmodule:: teslajsonpy @@ -27,75 +28,37 @@ Submodules Classes ======= +- :py:class:`TeslaCar`: + Represents a Tesla car. + - :py:class:`Connection`: Connection to Tesla Motors API. - :py:class:`Controller`: Controller for connections to Tesla Motors API. -- :py:class:`TeslaProxy`: - Class to handle proxy login connections to Alexa. - -- :py:class:`Battery`: - Home-Assistant battery class for a Tesla VehicleDevice. - -- :py:class:`Range`: - Home-Assistant class of the battery range for a Tesla VehicleDevice. - -- :py:class:`ChargerConnectionSensor`: - Home-assistant charger connection class for Tesla vehicles. - -- :py:class:`ChargingSensor`: - Home-Assistant charging sensor class for a Tesla VehicleDevice. - -- :py:class:`OnlineSensor`: - Home-Assistant Online sensor class for a Tesla VehicleDevice. - -- :py:class:`ParkingSensor`: - Home-assistant parking brake class for Tesla vehicles. - -- :py:class:`UpdateSensor`: - Home-Assistant update sensor class for a Tesla VehicleDevice. - -- :py:class:`ChargerSwitch`: - Home-Assistant class for the charger of a Tesla VehicleDevice. - -- :py:class:`RangeSwitch`: - Home-Assistant class for setting range limit for charger. - -- :py:class:`Climate`: - Home-assistant class of HVAC for Tesla vehicles. - -- :py:class:`TempSensor`: - Home-assistant class of temperature sensors for Tesla vehicles. +- :py:class:`EnergySite`: + Base class to represents a Tesla energy site. -- :py:class:`GPS`: - Home-assistant class for GPS of Tesla vehicles. +- :py:class:`PowerwallSite`: + Represents a Tesla Energy Powerwall site. -- :py:class:`Odometer`: - Home-assistant class for odometer of Tesla vehicles. - -- :py:class:`Lock`: - Home-assistant lock class for Tesla vehicles. - -- :py:class:`SentryModeSwitch`: - Home-Assistant class for sentry mode of Tesla vehicles. - -- :py:class:`Horn`: - Home-Assistant class for horn of Tesla vehicles. +- :py:class:`TeslaProxy`: + Class to handle proxy login connections to Alexa. -- :py:class:`FlashLights`: - Home-Assistant class for flash lights of Tesla vehicles. +- :py:class:`SolarPowerwallSite`: + Represents a Tesla Energy Solar site with Powerwall(s). -- :py:class:`TriggerHomelink`: - Home-Assistant class for trigger homelink of Tesla vehicles. +- :py:class:`SolarSite`: + Represents a Tesla Energy Solar site. -- :py:class:`TrunkLock`: - Home-Assistant rear trunk lock for a Tesla VehicleDevice. -- :py:class:`FrunkLock`: - Home-Assistant front trunk (frunk) lock for a Tesla VehicleDevice. +.. autoclass:: TeslaCar + :members: + .. rubric:: Inheritance + .. inheritance-diagram:: TeslaCar + :parts: 1 .. autoclass:: Connection :members: @@ -111,151 +74,39 @@ Classes .. inheritance-diagram:: Controller :parts: 1 -.. autoclass:: TeslaProxy - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: TeslaProxy - :parts: 1 - -.. autoclass:: Battery - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: Battery - :parts: 1 - -.. autoclass:: Range - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: Range - :parts: 1 - -.. autoclass:: ChargerConnectionSensor - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: ChargerConnectionSensor - :parts: 1 - -.. autoclass:: ChargingSensor - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: ChargingSensor - :parts: 1 - -.. autoclass:: OnlineSensor +.. autoclass:: EnergySite :members: .. rubric:: Inheritance - .. inheritance-diagram:: OnlineSensor + .. inheritance-diagram:: EnergySite :parts: 1 -.. autoclass:: ParkingSensor +.. autoclass:: PowerwallSite :members: .. rubric:: Inheritance - .. inheritance-diagram:: ParkingSensor + .. inheritance-diagram:: PowerwallSite :parts: 1 -.. autoclass:: UpdateSensor - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: UpdateSensor - :parts: 1 - -.. autoclass:: ChargerSwitch - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: ChargerSwitch - :parts: 1 - -.. autoclass:: RangeSwitch - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: RangeSwitch - :parts: 1 - -.. autoclass:: Climate - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: Climate - :parts: 1 - -.. autoclass:: TempSensor - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: TempSensor - :parts: 1 - -.. autoclass:: GPS - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: GPS - :parts: 1 - -.. autoclass:: Odometer - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: Odometer - :parts: 1 - -.. autoclass:: Lock - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: Lock - :parts: 1 - -.. autoclass:: SentryModeSwitch - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: SentryModeSwitch - :parts: 1 - -.. autoclass:: Horn - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: Horn - :parts: 1 - -.. autoclass:: FlashLights - :members: - - .. rubric:: Inheritance - .. inheritance-diagram:: FlashLights - :parts: 1 - -.. autoclass:: TriggerHomelink +.. autoclass:: TeslaProxy :members: .. rubric:: Inheritance - .. inheritance-diagram:: TriggerHomelink + .. inheritance-diagram:: TeslaProxy :parts: 1 -.. autoclass:: TrunkLock +.. autoclass:: SolarPowerwallSite :members: .. rubric:: Inheritance - .. inheritance-diagram:: TrunkLock + .. inheritance-diagram:: SolarPowerwallSite :parts: 1 -.. autoclass:: FrunkLock +.. autoclass:: SolarSite :members: .. rubric:: Inheritance - .. inheritance-diagram:: FrunkLock + .. inheritance-diagram:: SolarSite :parts: 1 @@ -268,9 +119,6 @@ Exceptions - :py:exc:`UnknownPresetMode`: Class of exceptions for Unknown Preset. -- :py:exc:`HomelinkError`: - Class of exceptions for Homelink Error. - - :py:exc:`RetryLimitError`: Class of exceptions for hitting retry limits. @@ -290,12 +138,6 @@ Exceptions .. inheritance-diagram:: UnknownPresetMode :parts: 1 -.. autoexception:: HomelinkError - - .. rubric:: Inheritance - .. inheritance-diagram:: HomelinkError - :parts: 1 - .. autoexception:: RetryLimitError .. rubric:: Inheritance @@ -319,4 +161,4 @@ Variables .. code-block:: text - '2.4.0' + '2.4.5' diff --git a/docs/html/genindex.html b/docs/html/genindex.html index 4f252700..50036bce 100644 --- a/docs/html/genindex.html +++ b/docs/html/genindex.html @@ -40,11 +40,12 @@

  • teslajsonpy
    • Submodules
    • @@ -94,6 +95,7 @@

      Index

      | I | L | M + | N | O | P | R @@ -115,73 +117,23 @@

      _

      A

      -

      B

      @@ -189,37 +141,55 @@

      B

      C

        -
      • charging_rate (teslajsonpy.ChargingSensor property) +
      • charge_port_door_open() (teslajsonpy.TeslaCar method) +
      • +
      • charge_port_latch (teslajsonpy.TeslaCar property) +
      • +
      • charge_rate (teslajsonpy.TeslaCar property) +
      • +
      • charger_actual_current (teslajsonpy.TeslaCar property)
      • -
      • charging_state() (teslajsonpy.Controller method) +
      • charger_phases (teslajsonpy.TeslaCar property)
      • -
      • ChargingSensor (class in teslajsonpy) +
      • charger_power (teslajsonpy.TeslaCar property)
      • -
      • Climate (class in teslajsonpy) +
      • charger_voltage (teslajsonpy.TeslaCar property) +
      • +
      • charging_state (teslajsonpy.TeslaCar property) +
      • +
      • climate_keeper_mode (teslajsonpy.TeslaCar property)
      • close() (teslajsonpy.Connection method)
      • -
      • command() (teslajsonpy.Controller method) +
      • conn_charge_cable (teslajsonpy.TeslaCar property)
      • connect() (teslajsonpy.Controller method)
      • @@ -233,25 +203,23 @@

        C

        D

        @@ -259,7 +227,15 @@

        D

        E

        +
        @@ -267,13 +243,17 @@

        E

        F

        @@ -281,63 +261,45 @@

        F

        G

        - +
        @@ -371,47 +331,21 @@

        G

        H

        @@ -419,35 +353,41 @@

        H

        I

        @@ -455,22 +395,30 @@

        I

        L

        +

        M

        +
        +
        + +

        N

        + + +

        O

        @@ -542,23 +482,23 @@

        O

        P

        @@ -566,55 +506,19 @@

        P

        R

        @@ -893,10 +727,14 @@

        U

        V

        @@ -904,6 +742,10 @@

        V

        W

        + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/docs/html/search.html b/docs/html/search.html index b5dfc331..687311af 100644 --- a/docs/html/search.html +++ b/docs/html/search.html @@ -43,11 +43,12 @@
      • teslajsonpy
        • Submodules
        • diff --git a/docs/html/searchindex.js b/docs/html/searchindex.js index 8237f540..7f08b7b0 100644 --- a/docs/html/searchindex.js +++ b/docs/html/searchindex.js @@ -1 +1 @@ -Search.setIndex({"docnames": ["index", "teslajsonpy/teslajsonpy", "teslajsonpy/teslajsonpy.__version__", "teslajsonpy/teslajsonpy.connection", "teslajsonpy/teslajsonpy.const", "teslajsonpy/teslajsonpy.controller", "teslajsonpy/teslajsonpy.exceptions", "teslajsonpy/teslajsonpy.homeassistant", "teslajsonpy/teslajsonpy.homeassistant.alerts", "teslajsonpy/teslajsonpy.homeassistant.battery_sensor", "teslajsonpy/teslajsonpy.homeassistant.binary_sensor", "teslajsonpy/teslajsonpy.homeassistant.charger", "teslajsonpy/teslajsonpy.homeassistant.climate", "teslajsonpy/teslajsonpy.homeassistant.gps", "teslajsonpy/teslajsonpy.homeassistant.heated_seats", "teslajsonpy/teslajsonpy.homeassistant.heated_steering_wheel", "teslajsonpy/teslajsonpy.homeassistant.homelink", "teslajsonpy/teslajsonpy.homeassistant.lock", "teslajsonpy/teslajsonpy.homeassistant.power", "teslajsonpy/teslajsonpy.homeassistant.sentry_mode", "teslajsonpy/teslajsonpy.homeassistant.trunk", "teslajsonpy/teslajsonpy.homeassistant.vehicle", "teslajsonpy/teslajsonpy.homeassistant.vehicle_data", "teslajsonpy/teslajsonpy.teslaproxy"], "filenames": ["index.rst", "teslajsonpy/teslajsonpy.rst", "teslajsonpy/teslajsonpy.__version__.rst", "teslajsonpy/teslajsonpy.connection.rst", "teslajsonpy/teslajsonpy.const.rst", "teslajsonpy/teslajsonpy.controller.rst", "teslajsonpy/teslajsonpy.exceptions.rst", "teslajsonpy/teslajsonpy.homeassistant.rst", "teslajsonpy/teslajsonpy.homeassistant.alerts.rst", "teslajsonpy/teslajsonpy.homeassistant.battery_sensor.rst", "teslajsonpy/teslajsonpy.homeassistant.binary_sensor.rst", "teslajsonpy/teslajsonpy.homeassistant.charger.rst", "teslajsonpy/teslajsonpy.homeassistant.climate.rst", "teslajsonpy/teslajsonpy.homeassistant.gps.rst", "teslajsonpy/teslajsonpy.homeassistant.heated_seats.rst", "teslajsonpy/teslajsonpy.homeassistant.heated_steering_wheel.rst", "teslajsonpy/teslajsonpy.homeassistant.homelink.rst", "teslajsonpy/teslajsonpy.homeassistant.lock.rst", "teslajsonpy/teslajsonpy.homeassistant.power.rst", "teslajsonpy/teslajsonpy.homeassistant.sentry_mode.rst", "teslajsonpy/teslajsonpy.homeassistant.trunk.rst", "teslajsonpy/teslajsonpy.homeassistant.vehicle.rst", "teslajsonpy/teslajsonpy.homeassistant.vehicle_data.rst", "teslajsonpy/teslajsonpy.teslaproxy.rst"], "titles": ["Welcome to teslajsonpy\u2019s documentation!", "teslajsonpy", "teslajsonpy.__version__", "teslajsonpy.connection", "teslajsonpy.const", "teslajsonpy.controller", "teslajsonpy.exceptions", "teslajsonpy.homeassistant", "teslajsonpy.homeassistant.alerts", "teslajsonpy.homeassistant.battery_sensor", "teslajsonpy.homeassistant.binary_sensor", "teslajsonpy.homeassistant.charger", "teslajsonpy.homeassistant.climate", "teslajsonpy.homeassistant.gps", "teslajsonpy.homeassistant.heated_seats", "teslajsonpy.homeassistant.heated_steering_wheel", "teslajsonpy.homeassistant.homelink", "teslajsonpy.homeassistant.lock", "teslajsonpy.homeassistant.power", "teslajsonpy.homeassistant.sentry_mode", "teslajsonpy.homeassistant.trunk", "teslajsonpy.homeassistant.vehicle", "teslajsonpy.homeassistant.vehicle_data", "teslajsonpy.teslaproxy"], "terms": {"async": [0, 1], "python": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "modul": 0, "tesla": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "primarili": 0, "enabl": [0, 1], "home": [0, 1], "assist": [0, 1], "note": 0, "ha": [0, 1], "offici": 0, "therefor": 0, "thi": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "librari": 0, "mai": [0, 1], "stop": [0, 1], "work": 0, "ani": [0, 1], "time": [0, 1], "without": 0, "warn": 0, "origin": 0, "inspir": 0, "code": [0, 1], "also": [0, 1], "thank": 0, "tim": 0, "dorr": 0, "addit": 0, "repo": 0, "scaffold": 0, "from": [0, 1], "simplisaf": 0, "check": 0, "open": [0, 1], "featur": 0, "bug": 0, "initi": 0, "discuss": 0, "one": 0, "fork": 0, "repositori": 0, "instal": 0, "dev": [0, 1], "environ": 0, "make": 0, "init": 0, "enter": 0, "virtual": 0, "poetri": 0, "shell": 0, "your": 0, "new": [0, 1], "fix": 0, "develop": 0, "write": 0, "test": [0, 1], "cover": 0, "function": [0, 1], "updat": [0, 1], "readm": 0, "md": 0, "run": [0, 1], "ensur": 0, "100": 0, "coverag": 0, "you": 0, "have": 0, "lint": 0, "error": [0, 1], "type": [0, 1], "correctli": 0, "add": 0, "yourself": 0, "author": [0, 1], "submit": 0, "pull": 0, "request": [0, 1], "doc": 0, "apach": [0, 3, 5], "2": [0, 1, 3, 5], "0": [0, 1, 3, 5], "By": 0, "provid": [0, 1], "agre": 0, "under": [0, 1], "warranti": 0, "us": [0, 1], "own": 0, "risk": 0, "submodul": 0, "__version__": [0, 1], "connect": [0, 1], "const": [0, 1], "control": [0, 1, 2, 3, 4, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "except": 0, "homeassist": [0, 1], "teslaproxi": [0, 1], "class": 0, "variabl": 0, "index": [0, 1], "search": 0, "page": 0, "packag": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "api": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "For": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "more": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "detail": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "about": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "pleas": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "refer": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "document": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "http": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "github": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "com": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "zabuldon": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "alert": [1, 7], "battery_sensor": [1, 7], "binary_sensor": [1, 7], "charger": [1, 7], "climat": [1, 7], "gp": [1, 7], "heated_seat": [1, 7], "heated_steering_wheel": [1, 7], "homelink": [1, 7], "lock": [1, 7], "power": [1, 7], "sentry_mod": [1, 7], "trunk": [1, 7], "vehicl": [1, 7], "vehicle_data": [1, 7], "motor": 1, "handl": 1, "proxi": [1, 23], "login": 1, "alexa": 1, "batteri": 1, "vehicledevic": 1, "rang": 1, "chargerconnectionsensor": 1, "chargingsensor": 1, "charg": 1, "sensor": 1, "onlinesensor": 1, "onlin": 1, "parkingsensor": 1, "park": 1, "brake": 1, "updatesensor": 1, "chargerswitch": 1, "rangeswitch": 1, "set": 1, "limit": 1, "hvac": 1, "tempsensor": 1, "temperatur": 1, "odomet": 1, "sentrymodeswitch": 1, "sentri": 1, "mode": 1, "horn": 1, "flashlight": 1, "flash": 1, "light": 1, "triggerhomelink": 1, "trigger": 1, "trunklock": 1, "rear": 1, "frunklock": 1, "front": 1, "frunk": 1, "websess": 1, "asynccli": 1, "email": 1, "option": 1, "str": 1, "none": 1, "password": 1, "access_token": 1, "refresh_token": 1, "authorization_token": 1, "expir": 1, "int": 1, "auth_domain": 1, "auth": 1, "inherit": 1, "close": 1, "get": 1, "command": 1, "data": 1, "get_authorization_cod": 1, "mfa_cod": 1, "mfa_devic": 1, "retry_limit": 1, "3": 1, "oauth3": 1, "method": 1, "get_authorization_code_link": 1, "fals": 1, "url": 1, "get_bearer_token": 1, "bearer": 1, "token": 1, "owner": 1, "get_sso_auth_token": 1, "sso": 1, "post": 1, "refresh_access_token": 1, "refresh": 1, "access": [1, 5], "websocket_connect": 1, "vin": 1, "vehicle_id": 1, "kwarg": 1, "stream": 1, "websocket": 1, "paramet": 1, "on_messag": 1, "call": 1, "valid": 1, "messag": 1, "It": 1, "must": 1, "process": 1, "json": 1, "deliv": 1, "on_disconnect": 1, "disconnect": 1, "update_interv": 1, "300": 1, "enable_websocket": 1, "bool": 1, "polling_polici": 1, "name": 1, "path_var": 1, "wake_if_asleep": 1, "perform": 1, "given": 1, "endpoint": 1, "keyword": 1, "argument": 1, "tdorsser": 1, "teslapi": 1, "blob": 1, "master": 1, "__init__": 1, "py": 1, "l242": 1, "l277": 1, "mit": 1, "string": 1, "e": 1, "g": 1, "statu": 1, "see": 1, "dict": 1, "path": 1, "replac": 1, "default": 1, "timdorr": 1, "basic": 1, "vs": 1, "id": 1, "underli": [1, 3], "whether": 1, "fail": 1, "respons": 1, "should": 1, "wake": 1, "up": 1, "retri": 1, "pass": 1, "rais": 1, "valueerror": 1, "If": 1, "found": 1, "notimplementederror": 1, "implement": 1, "miss": 1, "return": 1, "object": 1, "charging_st": 1, "car_id": 1, "state": 1, "singl": 1, "true": 1, "product_typ": 1, "deprec": 1, "instead": 1, "identifi": [1, 3, 5], "car": 1, "field": 1, "across": 1, "indic": 1, "energi": 1, "site": 1, "tesla_product_type_vehicl": 1, "test_login": 1, "filtered_vin": 1, "list": 1, "arg": 1, "credenti": 1, "onli": 1, "sleep": 1, "empti": 1, "filter": 1, "text": 1, "mfa": 1, "id_token": 1, "expires_in": 1, "send": 1, "wrap": 1, "wake_up": 1, "decor": 1, "get_car_onlin": 1, "all": 1, "number": 1, "both": 1, "overrid": 1, "exist": 1, "boolean": 1, "othewis": 1, "entir": 1, "dictionari": 1, "get_charging_param": 1, "cach": 1, "copi": 1, "charging_param": 1, "get_climate_param": 1, "climate_param": 1, "get_config_param": 1, "config_param": 1, "config": 1, "get_drive_param": 1, "drive_param": 1, "drive": 1, "get_energysit": 1, "teslaapi": 1, "solar": 1, "get_expir": 1, "oauth": [1, 23], "timestamp": 1, "when": 1, "get_gui_param": 1, "gui_param": 1, "gui": 1, "get_homeassistant_compon": 1, "compon": 1, "setup": 1, "get_vehicl": 1, "gener": 1, "get_last_park_tim": 1, "park_tim": 1, "complet": 1, "wa": 1, "last": 1, "get_last_update_tim": 1, "last_upd": 1, "get_last_wake_up_tim": 1, "wakeup_tim": 1, "waken": 1, "get_oauth_url": 1, "get_power_param": 1, "site_id": 1, "get_state_param": 1, "state_param": 1, "get_token": 1, "includ": 1, "self": 1, "__connect": 1, "token_refresh": 1, "get_update_interval_vin": 1, "interv": 1, "specif": 1, "get_upd": 1, "is_car_onlin": 1, "alia": 1, "better": 1, "readabl": 1, "is_climate_on": 1, "is_in_gear": 1, "gear": 1, "unknown": 1, "is_sentry_mode_on": 1, "is_token_refresh": 1, "been": 1, "chang": 1, "retriev": 1, "sinc": 1, "register_websocket_callback": 1, "callback": 1, "regist": 1, "entri": 1, "set_authorization_cod": 1, "set_authorization_domain": 1, "domain": 1, "set_car_onlin": 1, "online_statu": 1, "Will": 1, "last_wake_up_tim": 1, "offlin": 1, "awak": 1, "out": 1, "reach": 1, "set_charging_param": 1, "param": 1, "set_climate_param": 1, "set_config_param": 1, "set_drive_param": 1, "set_gui_param": 1, "set_id_vin": 1, "map": 1, "set_last_park_tim": 1, "float": 1, "shift_stat": 1, "set_last_update_tim": 1, "updated_tim": 1, "set_last_wake_up_tim": 1, "set_state_param": 1, "set_update_interval_vin": 1, "valu": 1, "set_upd": 1, "forc": 1, "an": 1, "next": 1, "poll": 1, "confusingli": 1, "differ": 1, "set_vehicle_id_vin": 1, "shift": 1, "attribut": 1, "first": 1, "assum": 1, "attempt": 1, "least": 1, "ar": 1, "occur": 1, "thei": 1, "blank": 1, "The": 1, "regardless": 1, "success": 1, "retrylimiterror": 1, "properti": 1, "second": 1, "between": 1, "vehicle_data_request": 1, "data_request": 1, "which": 1, "roll": 1, "plu": 1, "configur": 1, "vin_to_vehicle_id": 1, "proxy_url": 1, "host_url": 1, "authcaptureproxi": [1, 23], "auth_capture_proxi": [1, 23], "modify_head": 1, "multidict": 1, "modifi": 1, "header": 1, "base": [1, 23], "To": 1, "disabl": 1, "auto": 1, "kei": 1, "skip_auto_head": 1, "exampl": 1, "prevent": 1, "user": 1, "agent": 1, "host": 1, "web": 1, "direct": 1, "need": 1, "actual": 1, "after": 1, "modif": 1, "static": 1, "prepend_i18n_path": 1, "base_url": 1, "html": 1, "prepend": 1, "i18n": 1, "loadpath": 1, "so": 1, "ll": 1, "intend": 1, "place": 1, "rel": 1, "i18next": 1, "prepend_relative_url": 1, "src": 1, "reset_data": 1, "reset": 1, "store": 1, "A": 1, "servic": 1, "multipl": 1, "rout": 1, "torn": 1, "down": 1, "test_url": 1, "resp": 1, "queri": 1, "authent": 1, "step": 1, "obtain": 1, "httpx": 1, "captur": 1, "through": 1, "overwrit": 1, "duplic": 1, "union": 1, "302": 1, "redirect": 1, "displai": 1, "did": 1, "async_upd": 1, "battery_charg": 1, "level": 1, "battery_level": 1, "device_class": 1, "devic": 1, "get_valu": 1, "has_batteri": 1, "alreadi": 1, "either": 1, "rate": 1, "ideal": 1, "gui_set": 1, "partial": 1, "assit": 1, "entiti": 1, "binarysensor": 1, "cabl": 1, "added_rang": 1, "ad": 1, "charge_current_request": 1, "current": 1, "charge_current_request_max": 1, "max": 1, "charge_energy_ad": 1, "charge_limit_soc": 1, "charger_actual_curr": 1, "charger_pow": 1, "charger_voltag": 1, "voltag": 1, "charging_r": 1, "state_class": 1, "time_left": 1, "left": 1, "full": 1, "hour": 1, "engag": 1, "device_state_attribut": 1, "is_charg": 1, "start_charg": 1, "start": 1, "stop_charg": 1, "is_maxrang": 1, "set_max": 1, "trip": 1, "set_standard": 1, "standard": 1, "daili": 1, "commut": 1, "get_current_temp": 1, "insid": 1, "get_fan_statu": 1, "fan": 1, "get_goal_temp": 1, "driver": 1, "is_hvac_en": 1, "preset_mod": 1, "preset": 1, "awai": 1, "temp": 1, "requir": 1, "support_preset_mod": 1, "avail": 1, "set_preset_mod": 1, "set_statu": 1, "set_temperatur": 1, "passeng": 1, "get_inside_temp": 1, "get_outside_temp": 1, "outsid": 1, "locat": 1, "get_loc": 1, "unit": 1, "measur": 1, "read": 1, "is_lock": 1, "door": 1, "unlock": 1, "extend": 1, "where": 1, "applic": 1, "disable_sentry_mod": 1, "enable_sentry_mod": 1, "is_on": 1, "honk_horn": 1, "flash_light": 1, "trigger_homelink": 1, "teslaexcept": 1, "unknownpresetmod": 1, "homelinkerror": 1, "hit": 1, "incompletecredenti": 1, "incomplet": 1, "bytes_or_buff": 1, "encod": 1, "creat": 1, "specifi": 1, "expos": 1, "buffer": 1, "decod": 1, "handler": 1, "otherwis": 1, "result": 1, "__str__": 1, "defin": 1, "repr": 1, "sy": 1, "getdefaultencod": 1, "strict": 1, "version": 2, "info": 2, "spdx": [3, 5], "licens": [3, 5], "logic": 3, "alandts": 23, "power_param": 1, "4": 1}, "objects": {"": [[1, 0, 0, "-", "teslajsonpy"]], "teslajsonpy": [[1, 1, 1, "", "Battery"], [1, 1, 1, "", "ChargerConnectionSensor"], [1, 1, 1, "", "ChargerSwitch"], [1, 1, 1, "", "ChargingSensor"], [1, 1, 1, "", "Climate"], [1, 1, 1, "", "Connection"], [1, 1, 1, "", "Controller"], [1, 1, 1, "", "FlashLights"], [1, 1, 1, "", "FrunkLock"], [1, 1, 1, "", "GPS"], [1, 4, 1, "", "HomelinkError"], [1, 1, 1, "", "Horn"], [1, 4, 1, "", "IncompleteCredentials"], [1, 1, 1, "", "Lock"], [1, 1, 1, "", "Odometer"], [1, 1, 1, "", "OnlineSensor"], [1, 1, 1, "", "ParkingSensor"], [1, 1, 1, "", "Range"], [1, 1, 1, "", "RangeSwitch"], [1, 4, 1, "", "RetryLimitError"], [1, 1, 1, "", "SentryModeSwitch"], [1, 1, 1, "", "TempSensor"], [1, 4, 1, "", "TeslaException"], [1, 1, 1, "", "TeslaProxy"], [1, 1, 1, "", "TriggerHomelink"], [1, 1, 1, "", "TrunkLock"], [1, 4, 1, "", "UnknownPresetMode"], [1, 1, 1, "", "UpdateSensor"], [2, 0, 0, "-", "__version__"], [3, 0, 0, "-", "connection"], [4, 0, 0, "-", "const"], [5, 0, 0, "-", "controller"], [6, 0, 0, "-", "exceptions"], [7, 0, 0, "-", "homeassistant"], [23, 0, 0, "-", "teslaproxy"]], "teslajsonpy.Battery": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "battery_charging"], [1, 2, 1, "", "battery_level"], [1, 3, 1, "", "device_class"], [1, 2, 1, "", "get_value"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "refresh"]], "teslajsonpy.ChargerConnectionSensor": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "get_value"], [1, 2, 1, "", "refresh"]], "teslajsonpy.ChargerSwitch": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "is_charging"], [1, 2, 1, "", "refresh"], [1, 2, 1, "", "start_charge"], [1, 2, 1, "", "stop_charge"]], "teslajsonpy.ChargingSensor": [[1, 3, 1, "", "added_range"], [1, 2, 1, "", "async_update"], [1, 3, 1, "", "charge_current_request"], [1, 3, 1, "", "charge_current_request_max"], [1, 3, 1, "", "charge_energy_added"], [1, 3, 1, "", "charge_limit_soc"], [1, 3, 1, "", "charger_actual_current"], [1, 3, 1, "", "charger_power"], [1, 3, 1, "", "charger_voltage"], [1, 3, 1, "", "charging_rate"], [1, 3, 1, "", "device_class"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "refresh"], [1, 3, 1, "", "state_class"], [1, 3, 1, "", "time_left"]], "teslajsonpy.Climate": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "get_current_temp"], [1, 2, 1, "", "get_fan_status"], [1, 2, 1, "", "get_goal_temp"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "is_hvac_enabled"], [1, 3, 1, "", "preset_mode"], [1, 3, 1, "", "preset_modes"], [1, 2, 1, "", "refresh"], [1, 2, 1, "", "set_preset_mode"], [1, 2, 1, "", "set_status"], [1, 2, 1, "", "set_temperature"]], "teslajsonpy.Connection": [[1, 2, 1, "", "close"], [1, 2, 1, "", "get"], [1, 2, 1, "", "get_authorization_code"], [1, 2, 1, "", "get_authorization_code_link"], [1, 2, 1, "", "get_bearer_token"], [1, 2, 1, "", "get_sso_auth_token"], [1, 2, 1, "", "post"], [1, 2, 1, "", "refresh_access_token"], [1, 2, 1, "", "websocket_connect"]], "teslajsonpy.Controller": [[1, 2, 1, "", "api"], [1, 2, 1, "", "charging_state"], [1, 2, 1, "", "command"], [1, 2, 1, "", "connect"], [1, 2, 1, "", "disconnect"], [1, 2, 1, "", "get"], [1, 2, 1, "", "get_car_online"], [1, 2, 1, "", "get_charging_params"], [1, 2, 1, "", "get_climate_params"], [1, 2, 1, "", "get_config_params"], [1, 2, 1, "", "get_drive_params"], [1, 2, 1, "", "get_energysites"], [1, 2, 1, "", "get_expiration"], [1, 2, 1, "", "get_gui_params"], [1, 2, 1, "", "get_homeassistant_components"], [1, 2, 1, "", "get_last_park_time"], [1, 2, 1, "", "get_last_update_time"], [1, 2, 1, "", "get_last_wake_up_time"], [1, 2, 1, "", "get_oauth_url"], [1, 2, 1, "", "get_power_params"], [1, 2, 1, "", "get_state_params"], [1, 2, 1, "", "get_tokens"], [1, 2, 1, "", "get_update_interval_vin"], [1, 2, 1, "", "get_updates"], [1, 2, 1, "", "get_vehicles"], [1, 2, 1, "", "is_car_online"], [1, 2, 1, "", "is_climate_on"], [1, 2, 1, "", "is_in_gear"], [1, 2, 1, "", "is_sentry_mode_on"], [1, 2, 1, "", "is_token_refreshed"], [1, 2, 1, "", "post"], [1, 2, 1, "", "register_websocket_callback"], [1, 2, 1, "", "set_authorization_code"], [1, 2, 1, "", "set_authorization_domain"], [1, 2, 1, "", "set_car_online"], [1, 2, 1, "", "set_charging_params"], [1, 2, 1, "", "set_climate_params"], [1, 2, 1, "", "set_config_params"], [1, 2, 1, "", "set_drive_params"], [1, 2, 1, "", "set_gui_params"], [1, 2, 1, "", "set_id_vin"], [1, 2, 1, "", "set_last_park_time"], [1, 2, 1, "", "set_last_update_time"], [1, 2, 1, "", "set_last_wake_up_time"], [1, 2, 1, "", "set_state_params"], [1, 2, 1, "", "set_update_interval_vin"], [1, 2, 1, "", "set_updates"], [1, 2, 1, "", "set_vehicle_id_vin"], [1, 2, 1, "", "shift_state"], [1, 2, 1, "", "update"], [1, 3, 1, "", "update_interval"], [1, 2, 1, "", "vehicle_data_request"], [1, 2, 1, "", "vin_to_vehicle_id"]], "teslajsonpy.FlashLights": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "flash_lights"], [1, 2, 1, "", "has_battery"]], "teslajsonpy.FrunkLock": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "is_locked"], [1, 2, 1, "", "lock"], [1, 2, 1, "", "refresh"], [1, 2, 1, "", "unlock"]], "teslajsonpy.GPS": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "get_location"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "refresh"]], "teslajsonpy.Horn": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "honk_horn"]], "teslajsonpy.Lock": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "is_locked"], [1, 2, 1, "", "lock"], [1, 2, 1, "", "refresh"], [1, 2, 1, "", "unlock"]], "teslajsonpy.Odometer": [[1, 2, 1, "", "async_update"], [1, 3, 1, "", "device_class"], [1, 2, 1, "", "get_value"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "refresh"]], "teslajsonpy.OnlineSensor": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "get_value"], [1, 2, 1, "", "refresh"]], "teslajsonpy.ParkingSensor": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "get_value"], [1, 2, 1, "", "refresh"]], "teslajsonpy.Range": [[1, 2, 1, "", "async_update"], [1, 3, 1, "", "device_class"], [1, 2, 1, "", "get_value"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "refresh"]], "teslajsonpy.RangeSwitch": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "is_maxrange"], [1, 2, 1, "", "refresh"], [1, 2, 1, "", "set_max"], [1, 2, 1, "", "set_standard"]], "teslajsonpy.SentryModeSwitch": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "available"], [1, 2, 1, "", "disable_sentry_mode"], [1, 2, 1, "", "enable_sentry_mode"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "is_on"], [1, 2, 1, "", "refresh"]], "teslajsonpy.TempSensor": [[1, 2, 1, "", "async_update"], [1, 3, 1, "", "device_class"], [1, 2, 1, "", "get_inside_temp"], [1, 2, 1, "", "get_outside_temp"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "refresh"]], "teslajsonpy.TeslaProxy": [[1, 2, 1, "", "modify_headers"], [1, 2, 1, "", "prepend_i18n_path"], [1, 2, 1, "", "prepend_relative_urls"], [1, 2, 1, "", "reset_data"], [1, 2, 1, "", "test_url"]], "teslajsonpy.TriggerHomelink": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "available"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "refresh"], [1, 2, 1, "", "trigger_homelink"]], "teslajsonpy.TrunkLock": [[1, 2, 1, "", "async_update"], [1, 2, 1, "", "has_battery"], [1, 2, 1, "", "is_locked"], [1, 2, 1, "", "lock"], [1, 2, 1, "", "refresh"], [1, 2, 1, "", "unlock"]], "teslajsonpy.UpdateSensor": [[1, 2, 1, "", "async_update"], [1, 3, 1, "", "device_state_attributes"], [1, 2, 1, "", "get_value"], [1, 2, 1, "", "refresh"]], "teslajsonpy.homeassistant": [[8, 0, 0, "-", "alerts"], [9, 0, 0, "-", "battery_sensor"], [10, 0, 0, "-", "binary_sensor"], [11, 0, 0, "-", "charger"], [12, 0, 0, "-", "climate"], [13, 0, 0, "-", "gps"], [14, 0, 0, "-", "heated_seats"], [15, 0, 0, "-", "heated_steering_wheel"], [16, 0, 0, "-", "homelink"], [17, 0, 0, "-", "lock"], [18, 0, 0, "-", "power"], [19, 0, 0, "-", "sentry_mode"], [20, 0, 0, "-", "trunk"], [21, 0, 0, "-", "vehicle"], [22, 0, 0, "-", "vehicle_data"]]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:method", "3": "py:property", "4": "py:exception"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "method", "Python method"], "3": ["py", "property", "Python property"], "4": ["py", "exception", "Python exception"]}, "titleterms": {"welcom": 0, "teslajsonpi": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23], "s": 0, "document": 0, "credit": 0, "contribut": 0, "licens": 0, "api": 0, "refer": 0, "indic": 0, "tabl": 0, "submodul": [1, 7], "class": 1, "except": [1, 6], "variabl": 1, "__version__": 2, "connect": 3, "const": 4, "control": 5, "homeassist": [7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22], "alert": 8, "battery_sensor": 9, "binary_sensor": 10, "charger": 11, "climat": 12, "gp": 13, "heated_seat": 14, "heated_steering_wheel": 15, "homelink": 16, "lock": 17, "power": 18, "sentry_mod": 19, "trunk": 20, "vehicl": 21, "vehicle_data": 22, "teslaproxi": 23}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 56}}) \ No newline at end of file +Search.setIndex({"docnames": ["index", "teslajsonpy/teslajsonpy", "teslajsonpy/teslajsonpy.__version__", "teslajsonpy/teslajsonpy.car", "teslajsonpy/teslajsonpy.connection", "teslajsonpy/teslajsonpy.const", "teslajsonpy/teslajsonpy.controller", "teslajsonpy/teslajsonpy.energy", "teslajsonpy/teslajsonpy.exceptions", "teslajsonpy/teslajsonpy.teslaproxy"], "filenames": ["index.rst", "teslajsonpy/teslajsonpy.rst", "teslajsonpy/teslajsonpy.__version__.rst", "teslajsonpy/teslajsonpy.car.rst", "teslajsonpy/teslajsonpy.connection.rst", "teslajsonpy/teslajsonpy.const.rst", "teslajsonpy/teslajsonpy.controller.rst", "teslajsonpy/teslajsonpy.energy.rst", "teslajsonpy/teslajsonpy.exceptions.rst", "teslajsonpy/teslajsonpy.teslaproxy.rst"], "titles": ["Welcome to teslajsonpy\u2019s documentation!", "teslajsonpy", "teslajsonpy.__version__", "teslajsonpy.car", "teslajsonpy.connection", "teslajsonpy.const", "teslajsonpy.controller", "teslajsonpy.energy", "teslajsonpy.exceptions", "teslajsonpy.teslaproxy"], "terms": {"async": [0, 1], "python": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "modul": 0, "tesla": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "primarili": 0, "enabl": [0, 1], "home": 0, "assist": 0, "note": 0, "ha": [0, 1], "offici": 0, "therefor": 0, "thi": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "librari": 0, "mai": [0, 1], "stop": [0, 1], "work": 0, "ani": [0, 1], "time": [0, 1], "without": 0, "warn": 0, "origin": 0, "inspir": 0, "code": [0, 1], "also": [0, 1], "thank": 0, "tim": 0, "dorr": 0, "addit": 0, "repo": 0, "scaffold": 0, "from": [0, 1], "simplisaf": 0, "check": 0, "open": [0, 1], "featur": 0, "bug": 0, "initi": 0, "discuss": 0, "one": 0, "fork": 0, "repositori": 0, "instal": [0, 1], "dev": [0, 1], "environ": 0, "make": 0, "init": 0, "enter": 0, "virtual": 0, "poetri": 0, "shell": 0, "your": 0, "new": [0, 1], "fix": 0, "develop": 0, "write": 0, "test": [0, 1], "cover": 0, "function": [0, 1], "updat": [0, 1], "readm": 0, "md": 0, "run": 0, "ensur": 0, "100": [0, 1], "coverag": 0, "you": 0, "have": 0, "lint": 0, "error": [0, 1], "type": [0, 1], "correctli": 0, "add": 0, "yourself": 0, "author": [0, 1], "submit": 0, "pull": 0, "request": [0, 1], "doc": 0, "apach": 0, "2": [0, 1], "0": [0, 1], "By": 0, "provid": [0, 1], "agre": 0, "under": [0, 1], "warranti": 0, "us": [0, 1], "own": 0, "risk": 0, "submodul": 0, "__version__": [0, 1], "connect": [0, 1], "const": [0, 1], "control": [0, 1, 2, 3, 4, 5, 7, 8, 9], "except": 0, "homeassist": [], "teslaproxi": [0, 1], "class": 0, "variabl": 0, "index": [0, 1], "search": 0, "page": 0, "packag": [1, 2, 3, 4, 5, 6, 7, 8, 9], "api": [1, 2, 3, 4, 5, 6, 7, 8, 9], "For": [1, 2, 3, 4, 5, 6, 7, 8, 9], "more": [1, 2, 3, 4, 5, 6, 7, 8, 9], "detail": [1, 2, 3, 4, 5, 6, 7, 8, 9], "about": [1, 2, 3, 4, 5, 6, 7, 8, 9], "pleas": [1, 2, 3, 4, 5, 6, 7, 8, 9], "refer": [1, 2, 3, 4, 5, 6, 7, 8, 9], "document": [1, 2, 3, 4, 5, 6, 7, 8, 9], "http": [1, 2, 3, 4, 5, 6, 7, 8, 9], "github": [1, 2, 3, 4, 5, 6, 7, 8, 9], "com": [1, 2, 3, 4, 5, 6, 7, 8, 9], "zabuldon": [1, 2, 3, 4, 5, 6, 7, 8, 9], "alert": [], "battery_sensor": [], "binary_sensor": [], "charger": 1, "climat": 1, "gp": [], "heated_seat": [], "heated_steering_wheel": [], "homelink": 1, "lock": 1, "power": 1, "sentry_mod": 1, "trunk": 1, "vehicl": 1, "vehicle_data": 1, "motor": 1, "handl": 1, "proxi": [1, 9], "login": 1, "alexa": 1, "batteri": 1, "vehicledevic": [], "rang": 1, "chargerconnectionsensor": [], "chargingsensor": [], "charg": 1, "sensor": [], "onlinesensor": [], "onlin": 1, "parkingsensor": [], "park": 1, "brake": [], "updatesensor": [], "chargerswitch": [], "rangeswitch": [], "set": 1, "limit": 1, "hvac": 1, "tempsensor": [], "temperatur": 1, "odomet": 1, "sentrymodeswitch": [], "sentri": 1, "mode": 1, "horn": 1, "flashlight": [], "flash": 1, "light": 1, "triggerhomelink": [], "trigger": 1, "trunklock": [], "rear": 1, "frunklock": [], "front": 1, "frunk": 1, "websess": 1, "asynccli": 1, "email": 1, "option": 1, "str": 1, "none": 1, "password": 1, "access_token": 1, "refresh_token": 1, "authorization_token": 1, "expir": 1, "int": 1, "auth_domain": 1, "auth": 1, "inherit": 1, "close": 1, "get": 1, "command": 1, "data": 1, "get_authorization_cod": 1, "mfa_cod": 1, "mfa_devic": 1, "retry_limit": 1, "3": 1, "oauth3": 1, "method": 1, "get_authorization_code_link": 1, "fals": 1, "url": 1, "get_bearer_token": 1, "bearer": 1, "token": 1, "owner": 1, "get_sso_auth_token": 1, "sso": 1, "post": 1, "refresh_access_token": 1, "refresh": 1, "access": 1, "websocket_connect": 1, "vin": 1, "vehicle_id": 1, "kwarg": 1, "stream": 1, "websocket": 1, "paramet": 1, "on_messag": 1, "call": 1, "valid": 1, "messag": 1, "It": 1, "must": 1, "process": 1, "json": 1, "deliv": 1, "on_disconnect": 1, "disconnect": 1, "update_interv": 1, "300": 1, "enable_websocket": 1, "bool": 1, "polling_polici": 1, "name": 1, "path_var": 1, "wake_if_asleep": 1, "perform": 1, "given": 1, "endpoint": 1, "keyword": 1, "argument": 1, "tdorsser": 1, "teslapi": 1, "blob": 1, "master": 1, "__init__": 1, "py": 1, "l242": 1, "l277": 1, "mit": 1, "string": 1, "e": 1, "g": 1, "statu": 1, "see": 1, "dict": 1, "path": 1, "replac": 1, "default": 1, "timdorr": 1, "basic": 1, "vs": 1, "id": 1, "underli": 1, "whether": 1, "fail": 1, "respons": 1, "should": 1, "wake": 1, "up": 1, "retri": 1, "pass": 1, "rais": 1, "valueerror": 1, "If": 1, "found": 1, "notimplementederror": 1, "implement": 1, "miss": 1, "return": 1, "object": 1, "charging_st": 1, "car_id": 1, "state": 1, "singl": 1, "true": 1, "product_typ": [], "deprec": [], "instead": [], "identifi": 1, "car": [0, 1], "field": 1, "across": 1, "indic": 1, "energi": [0, 1], "site": 1, "tesla_product_type_vehicl": [], "test_login": 1, "filtered_vin": 1, "list": 1, "arg": 1, "credenti": 1, "onli": 1, "sleep": 1, "empti": 1, "filter": 1, "text": 1, "mfa": 1, "id_token": 1, "expires_in": 1, "send": 1, "wrap": [], "wake_up": 1, "decor": 1, "get_car_onlin": 1, "all": 1, "number": 1, "both": 1, "overrid": 1, "exist": 1, "boolean": 1, "othewis": 1, "entir": 1, "dictionari": 1, "get_charging_param": [], "cach": 1, "copi": [], "charging_param": [], "get_climate_param": [], "climate_param": [], "get_config_param": [], "config_param": [], "config": 1, "get_drive_param": [], "drive_param": [], "drive": 1, "get_energysit": [], "teslaapi": 1, "solar": 1, "get_expir": 1, "oauth": [1, 9], "timestamp": 1, "when": 1, "get_gui_param": [], "gui_param": [], "gui": 1, "get_homeassistant_compon": [], "compon": [], "setup": [], "get_vehicl": 1, "gener": 1, "get_last_park_tim": 1, "park_tim": 1, "complet": 1, "wa": 1, "last": 1, "get_last_update_tim": 1, "last_upd": 1, "get_last_wake_up_tim": 1, "wakeup_tim": 1, "waken": 1, "get_oauth_url": 1, "get_power_param": [], "site_id": 1, "get_state_param": [], "state_param": [], "get_token": 1, "includ": 1, "self": 1, "__connect": 1, "token_refresh": 1, "get_update_interval_vin": 1, "interv": 1, "specif": 1, "get_upd": 1, "is_car_onlin": 1, "alia": 1, "better": 1, "readabl": 1, "is_climate_on": 1, "is_in_gear": 1, "gear": 1, "unknown": 1, "is_sentry_mode_on": [], "is_token_refresh": 1, "been": 1, "chang": 1, "retriev": 1, "sinc": 1, "register_websocket_callback": 1, "callback": 1, "regist": 1, "entri": 1, "set_authorization_cod": 1, "set_authorization_domain": 1, "domain": 1, "set_car_onlin": 1, "online_statu": 1, "Will": 1, "last_wake_up_tim": 1, "offlin": 1, "awak": 1, "out": 1, "reach": 1, "set_charging_param": [], "param": [], "set_climate_param": [], "set_config_param": [], "set_drive_param": [], "set_gui_param": [], "set_id_vin": 1, "map": 1, "set_last_park_tim": 1, "float": 1, "shift_stat": 1, "set_last_update_tim": 1, "updated_tim": 1, "set_last_wake_up_tim": 1, "set_state_param": [], "set_update_interval_vin": 1, "valu": 1, "set_upd": 1, "forc": 1, "an": 1, "next": 1, "poll": 1, "confusingli": 1, "differ": 1, "set_vehicle_id_vin": 1, "shift": 1, "attribut": 1, "first": 1, "assum": 1, "attempt": 1, "least": 1, "ar": 1, "occur": 1, "thei": 1, "blank": 1, "The": 1, "regardless": 1, "success": 1, "retrylimiterror": 1, "properti": 1, "second": 1, "between": 1, "vehicle_data_request": [], "data_request": [], "which": [], "roll": [], "plu": [], "configur": [], "vin_to_vehicle_id": 1, "proxy_url": 1, "host_url": 1, "authcaptureproxi": [1, 9], "auth_capture_proxi": [1, 9], "modify_head": 1, "multidict": 1, "modifi": 1, "header": 1, "base": [1, 9], "To": 1, "disabl": 1, "auto": 1, "kei": 1, "skip_auto_head": 1, "exampl": 1, "prevent": 1, "user": 1, "agent": 1, "host": 1, "web": 1, "direct": 1, "need": 1, "actual": 1, "after": 1, "modif": 1, "static": 1, "prepend_i18n_path": 1, "base_url": 1, "html": 1, "prepend": 1, "i18n": 1, "loadpath": 1, "so": 1, "ll": 1, "intend": 1, "place": 1, "rel": 1, "i18next": 1, "prepend_relative_url": 1, "src": 1, "reset_data": 1, "reset": 1, "store": 1, "A": 1, "servic": 1, "multipl": 1, "rout": 1, "torn": 1, "down": 1, "test_url": 1, "resp": 1, "queri": 1, "authent": 1, "step": 1, "obtain": 1, "httpx": 1, "captur": 1, "through": 1, "overwrit": 1, "duplic": 1, "union": 1, "302": 1, "redirect": 1, "displai": 1, "did": 1, "async_upd": [], "battery_charg": [], "level": 1, "battery_level": 1, "device_class": [], "devic": 1, "get_valu": [], "has_batteri": 1, "alreadi": [], "either": [], "rate": 1, "ideal": 1, "gui_set": [], "partial": [], "assit": [], "entiti": [], "binarysensor": [], "cabl": 1, "added_rang": [], "ad": 1, "charge_current_request": 1, "current": 1, "charge_current_request_max": 1, "max": 1, "charge_energy_ad": 1, "charge_limit_soc": 1, "charger_actual_curr": 1, "charger_pow": 1, "charger_voltag": 1, "voltag": 1, "charging_r": [], "state_class": [], "time_left": [], "left": 1, "full": 1, "hour": 1, "engag": 1, "device_state_attribut": [], "is_charg": [], "start_charg": 1, "start": 1, "stop_charg": 1, "is_maxrang": [], "set_max": [], "trip": [], "set_standard": [], "standard": [], "daili": [], "commut": [], "get_current_temp": [], "insid": 1, "get_fan_statu": [], "fan": 1, "get_goal_temp": [], "driver": 1, "is_hvac_en": [], "preset_mod": [], "preset": 1, "awai": [], "temp": 1, "requir": [], "support_preset_mod": [], "avail": 1, "set_preset_mod": [], "set_statu": [], "set_temperatur": 1, "passeng": 1, "get_inside_temp": [], "get_outside_temp": [], "outsid": 1, "locat": 1, "get_loc": [], "unit": 1, "measur": [], "read": [], "is_lock": 1, "door": 1, "unlock": 1, "extend": [], "where": [], "applic": [], "disable_sentry_mod": [], "enable_sentry_mod": [], "is_on": 1, "honk_horn": 1, "flash_light": 1, "trigger_homelink": 1, "teslaexcept": 1, "unknownpresetmod": 1, "homelinkerror": [], "hit": 1, "incompletecredenti": 1, "incomplet": 1, "bytes_or_buff": 1, "encod": 1, "creat": 1, "specifi": 1, "expos": 1, "buffer": 1, "decod": 1, "handler": 1, "otherwis": 1, "result": 1, "__str__": 1, "defin": 1, "repr": 1, "sy": 1, "getdefaultencod": 1, "strict": 1, "version": 1, "info": [], "spdx": [], "licens": [], "logic": [], "alandts": 9, "power_param": [], "4": 1, "teslacar": 1, "repres": 1, "energysit": 1, "powerwallsit": 1, "powerwal": 1, "solarpowerwallsit": 1, "s": 1, "solarsit": 1, "shouldn": 1, "t": 1, "instanti": 1, "directli": 1, "generate_car_object": 1, "battery_rang": 1, "cabin_overheat_protect": 1, "cabin": 1, "overheat": 1, "protect": 1, "car_typ": 1, "car_vers": 1, "softwar": 1, "change_charge_limit": 1, "soc": 1, "charge_limit_soc_max": 1, "charge_limit_soc_min": 1, "min": 1, "charge_miles_added_id": 1, "mile": 1, "charge_miles_added_r": 1, "charge_port_door_clos": 1, "port": 1, "charge_port_door_open": 1, "charge_port_latch": 1, "latch": 1, "other": 1, "charge_r": 1, "charger_phas": 1, "phase": 1, "nopow": 1, "asleep": 1, "climate_keeper_mod": 1, "keeper": 1, "dog": 1, "camp": 1, "off": 1, "Not": 1, "support": 1, "model": 1, "conn_charge_c": 1, "data_avail": 1, "defrost_mod": 1, "defrost": 1, "display_nam": 1, "driver_temp_set": 1, "fan_statu": 1, "fast_charger_brand": 1, "fast": 1, "brand": 1, "fast_charger_pres": 1, "present": 1, "fast_charger_typ": 1, "get_seat_heater_statu": 1, "seat_id": 1, "seat": 1, "heater": 1, "gui_distance_unit": 1, "distanc": 1, "gui_range_displai": 1, "head": 1, "homelink_device_count": 1, "count": 1, "homelink_nearbi": 1, "nearbi": 1, "honk": 1, "ideal_battery_rang": 1, "in_servic": 1, "inside_temp": 1, "is_charge_port_door_open": 1, "is_frunk_clos": 1, "255": 1, "i": 1, "revers": 1, "is_steering_wheel_heater_on": 1, "steer": 1, "wheel": 1, "is_trunk_clos": 1, "1": 1, "latitud": 1, "longitud": 1, "max_avail_temp": 1, "min_avail_temp": 1, "native_head": 1, "nativ": 1, "native_latitud": 1, "native_location_support": 1, "native_longitud": 1, "native_typ": 1, "outside_temp": 1, "passenger_temp_set": 1, "powered_lift_g": 1, "lift": 1, "gate": 1, "rear_seat_heat": 1, "row": 1, "heat": 1, "remote_seat_heater_request": 1, "low": 1, "medium": 1, "high": 1, "right": 1, "center": 1, "5": 1, "6": 1, "third": 1, "7": 1, "schedule_software_upd": 1, "offset_sec": 1, "sentry_mode_avail": 1, "set_cabin_overheat_protect": 1, "No": 1, "c": 1, "On": 1, "set_charging_amp": 1, "amp": 1, "set_climate_keeper_mod": 1, "keeper_id": 1, "keep": 1, "set_heated_steering_wheel": 1, "set_hvac_mod": 1, "set_max_defrost": 1, "set_sentry_mod": 1, "software_upd": 1, "inform": 1, "speed": 1, "steering_wheel_heat": 1, "third_row_seat": 1, "time_to_full_charg": 1, "toggle_frunk": 1, "actuat": 1, "toggle_trunk": 1, "include_vehicl": 1, "include_energysit": 1, "generate_energysite_object": 1, "get_battery_data": 1, "battery_id": 1, "get_battery_summari": 1, "get_product_list": 1, "product": 1, "get_site_config": 1, "energysite_id": 1, "get_site_data": 1, "get_vehicle_data": 1, "callabl": 1, "site_config": 1, "aka": 1, "has_load_met": 1, "load": 1, "meter": 1, "has_solar": 1, "resource_typ": 1, "battery_data": 1, "battery_summari": 1, "backup_reserve_perc": 1, "backup": 1, "reserv": 1, "percentag": 1, "battery_pow": 1, "watt": 1, "energy_left": 1, "grid_pow": 1, "grid": 1, "grid_statu": 1, "load_pow": 1, "operation_mod": 1, "oper": 1, "percentage_charg": 1, "set_operation_mod": 1, "real_mod": 1, "self_consumpt": 1, "autonom": 1, "set_reserve_perc": 1, "site_nam": 1, "solar_pow": 1, "firmwar": 1, "export_rul": 1, "export": 1, "rule": 1, "grid_charg": 1, "set_export_rul": 1, "pv_onli": 1, "everyth": 1, "battery_ok": 1, "set_grid_charg": 1, "solar_typ": 1, "pv_panel": 1, "roof": 1, "site_data": 1}, "objects": {"": [[1, 0, 0, "-", "teslajsonpy"]], "teslajsonpy": [[1, 1, 1, "", "Connection"], [1, 1, 1, "", "Controller"], [1, 1, 1, "", "EnergySite"], [1, 4, 1, "", "IncompleteCredentials"], [1, 1, 1, "", "PowerwallSite"], [1, 4, 1, "", "RetryLimitError"], [1, 1, 1, "", "SolarPowerwallSite"], [1, 1, 1, "", "SolarSite"], [1, 1, 1, "", "TeslaCar"], [1, 4, 1, "", "TeslaException"], [1, 1, 1, "", "TeslaProxy"], [1, 4, 1, "", "UnknownPresetMode"], [2, 0, 0, "-", "__version__"], [3, 0, 0, "-", "car"], [4, 0, 0, "-", "connection"], [5, 0, 0, "-", "const"], [6, 0, 0, "-", "controller"], [7, 0, 0, "-", "energy"], [8, 0, 0, "-", "exceptions"], [9, 0, 0, "-", "teslaproxy"]], "teslajsonpy.Connection": [[1, 2, 1, "", "close"], [1, 2, 1, "", "get"], [1, 2, 1, "", "get_authorization_code"], [1, 2, 1, "", "get_authorization_code_link"], [1, 2, 1, "", "get_bearer_token"], [1, 2, 1, "", "get_sso_auth_token"], [1, 2, 1, "", "post"], [1, 2, 1, "", "refresh_access_token"], [1, 2, 1, "", "websocket_connect"]], "teslajsonpy.Controller": [[1, 2, 1, "", "api"], [1, 2, 1, "", "connect"], [1, 2, 1, "", "disconnect"], [1, 2, 1, "", "generate_car_objects"], [1, 2, 1, "", "generate_energysite_objects"], [1, 2, 1, "", "get_battery_data"], [1, 2, 1, "", "get_battery_summary"], [1, 2, 1, "", "get_car_online"], [1, 2, 1, "", "get_expiration"], [1, 2, 1, "", "get_last_park_time"], [1, 2, 1, "", "get_last_update_time"], [1, 2, 1, "", "get_last_wake_up_time"], [1, 2, 1, "", "get_oauth_url"], [1, 2, 1, "", "get_product_list"], [1, 2, 1, "", "get_site_config"], [1, 2, 1, "", "get_site_data"], [1, 2, 1, "", "get_tokens"], [1, 2, 1, "", "get_update_interval_vin"], [1, 2, 1, "", "get_updates"], [1, 2, 1, "", "get_vehicle_data"], [1, 2, 1, "", "get_vehicles"], [1, 2, 1, "", "is_car_online"], [1, 2, 1, "", "is_token_refreshed"], [1, 2, 1, "", "register_websocket_callback"], [1, 2, 1, "", "set_authorization_code"], [1, 2, 1, "", "set_authorization_domain"], [1, 2, 1, "", "set_car_online"], [1, 2, 1, "", "set_id_vin"], [1, 2, 1, "", "set_last_park_time"], [1, 2, 1, "", "set_last_update_time"], [1, 2, 1, "", "set_last_wake_up_time"], [1, 2, 1, "", "set_update_interval_vin"], [1, 2, 1, "", "set_updates"], [1, 2, 1, "", "set_vehicle_id_vin"], [1, 2, 1, "", "update"], [1, 3, 1, "", "update_interval"], [1, 2, 1, "", "vin_to_vehicle_id"]], "teslajsonpy.EnergySite": [[1, 3, 1, "", "energysite_id"], [1, 3, 1, "", "has_battery"], [1, 3, 1, "", "has_load_meter"], [1, 3, 1, "", "has_solar"], [1, 3, 1, "", "id"], [1, 3, 1, "", "resource_type"]], "teslajsonpy.PowerwallSite": [[1, 3, 1, "", "backup_reserve_percent"], [1, 3, 1, "", "battery_power"], [1, 3, 1, "", "data_available"], [1, 3, 1, "", "energy_left"], [1, 3, 1, "", "grid_power"], [1, 3, 1, "", "grid_status"], [1, 3, 1, "", "load_power"], [1, 3, 1, "", "operation_mode"], [1, 3, 1, "", "percentage_charged"], [1, 2, 1, "", "set_operation_mode"], [1, 2, 1, "", "set_reserve_percent"], [1, 3, 1, "", "site_name"], [1, 3, 1, "", "solar_power"], [1, 3, 1, "", "version"]], "teslajsonpy.SolarPowerwallSite": [[1, 3, 1, "", "export_rule"], [1, 3, 1, "", "grid_charging"], [1, 2, 1, "", "set_export_rule"], [1, 2, 1, "", "set_grid_charging"], [1, 3, 1, "", "solar_type"]], "teslajsonpy.SolarSite": [[1, 3, 1, "", "data_available"], [1, 3, 1, "", "grid_power"], [1, 3, 1, "", "load_power"], [1, 3, 1, "", "site_name"], [1, 3, 1, "", "solar_power"], [1, 3, 1, "", "solar_type"]], "teslajsonpy.TeslaCar": [[1, 3, 1, "", "battery_level"], [1, 3, 1, "", "battery_range"], [1, 3, 1, "", "cabin_overheat_protection"], [1, 3, 1, "", "car_type"], [1, 3, 1, "", "car_version"], [1, 2, 1, "", "change_charge_limit"], [1, 3, 1, "", "charge_current_request"], [1, 3, 1, "", "charge_current_request_max"], [1, 3, 1, "", "charge_energy_added"], [1, 3, 1, "", "charge_limit_soc"], [1, 3, 1, "", "charge_limit_soc_max"], [1, 3, 1, "", "charge_limit_soc_min"], [1, 3, 1, "", "charge_miles_added_ideal"], [1, 3, 1, "", "charge_miles_added_rated"], [1, 2, 1, "", "charge_port_door_close"], [1, 2, 1, "", "charge_port_door_open"], [1, 3, 1, "", "charge_port_latch"], [1, 3, 1, "", "charge_rate"], [1, 3, 1, "", "charger_actual_current"], [1, 3, 1, "", "charger_phases"], [1, 3, 1, "", "charger_power"], [1, 3, 1, "", "charger_voltage"], [1, 3, 1, "", "charging_state"], [1, 3, 1, "", "climate_keeper_mode"], [1, 3, 1, "", "conn_charge_cable"], [1, 3, 1, "", "data_available"], [1, 3, 1, "", "defrost_mode"], [1, 3, 1, "", "display_name"], [1, 3, 1, "", "driver_temp_setting"], [1, 3, 1, "", "fan_status"], [1, 3, 1, "", "fast_charger_brand"], [1, 3, 1, "", "fast_charger_present"], [1, 3, 1, "", "fast_charger_type"], [1, 2, 1, "", "flash_lights"], [1, 2, 1, "", "get_seat_heater_status"], [1, 3, 1, "", "gui_distance_units"], [1, 3, 1, "", "gui_range_display"], [1, 3, 1, "", "heading"], [1, 3, 1, "", "homelink_device_count"], [1, 3, 1, "", "homelink_nearby"], [1, 2, 1, "", "honk_horn"], [1, 3, 1, "", "id"], [1, 3, 1, "", "ideal_battery_range"], [1, 3, 1, "", "in_service"], [1, 3, 1, "", "inside_temp"], [1, 3, 1, "", "is_charge_port_door_open"], [1, 3, 1, "", "is_climate_on"], [1, 3, 1, "", "is_frunk_closed"], [1, 3, 1, "", "is_in_gear"], [1, 3, 1, "", "is_locked"], [1, 3, 1, "", "is_on"], [1, 3, 1, "", "is_steering_wheel_heater_on"], [1, 3, 1, "", "is_trunk_closed"], [1, 3, 1, "", "latitude"], [1, 2, 1, "", "lock"], [1, 3, 1, "", "longitude"], [1, 3, 1, "", "max_avail_temp"], [1, 3, 1, "", "min_avail_temp"], [1, 3, 1, "", "native_heading"], [1, 3, 1, "", "native_latitude"], [1, 3, 1, "", "native_location_supported"], [1, 3, 1, "", "native_longitude"], [1, 3, 1, "", "native_type"], [1, 3, 1, "", "odometer"], [1, 3, 1, "", "outside_temp"], [1, 3, 1, "", "passenger_temp_setting"], [1, 3, 1, "", "power"], [1, 3, 1, "", "powered_lift_gate"], [1, 3, 1, "", "rear_seat_heaters"], [1, 2, 1, "", "remote_seat_heater_request"], [1, 2, 1, "", "schedule_software_update"], [1, 3, 1, "", "sentry_mode"], [1, 3, 1, "", "sentry_mode_available"], [1, 2, 1, "", "set_cabin_overheat_protection"], [1, 2, 1, "", "set_charging_amps"], [1, 2, 1, "", "set_climate_keeper_mode"], [1, 2, 1, "", "set_heated_steering_wheel"], [1, 2, 1, "", "set_hvac_mode"], [1, 2, 1, "", "set_max_defrost"], [1, 2, 1, "", "set_sentry_mode"], [1, 2, 1, "", "set_temperature"], [1, 3, 1, "", "shift_state"], [1, 3, 1, "", "software_update"], [1, 3, 1, "", "speed"], [1, 2, 1, "", "start_charge"], [1, 3, 1, "", "state"], [1, 3, 1, "", "steering_wheel_heater"], [1, 2, 1, "", "stop_charge"], [1, 3, 1, "", "third_row_seats"], [1, 3, 1, "", "time_to_full_charge"], [1, 2, 1, "", "toggle_frunk"], [1, 2, 1, "", "toggle_trunk"], [1, 2, 1, "", "trigger_homelink"], [1, 2, 1, "", "unlock"], [1, 3, 1, "", "vehicle_id"], [1, 3, 1, "", "vin"], [1, 2, 1, "", "wake_up"]], "teslajsonpy.TeslaProxy": [[1, 2, 1, "", "modify_headers"], [1, 2, 1, "", "prepend_i18n_path"], [1, 2, 1, "", "prepend_relative_urls"], [1, 2, 1, "", "reset_data"], [1, 2, 1, "", "test_url"]]}, "objtypes": {"0": "py:module", "1": "py:class", "2": "py:method", "3": "py:property", "4": "py:exception"}, "objnames": {"0": ["py", "module", "Python module"], "1": ["py", "class", "Python class"], "2": ["py", "method", "Python method"], "3": ["py", "property", "Python property"], "4": ["py", "exception", "Python exception"]}, "titleterms": {"welcom": 0, "teslajsonpi": [0, 1, 2, 3, 4, 5, 6, 7, 8, 9], "s": 0, "document": 0, "credit": 0, "contribut": 0, "licens": 0, "api": 0, "refer": 0, "indic": 0, "tabl": 0, "submodul": 1, "class": 1, "except": [1, 8], "variabl": 1, "__version__": 2, "connect": 4, "const": 5, "control": 6, "homeassist": [], "alert": [], "battery_sensor": [], "binary_sensor": [], "charger": [], "climat": [], "gp": [], "heated_seat": [], "heated_steering_wheel": [], "homelink": [], "lock": [], "power": [], "sentry_mod": [], "trunk": [], "vehicl": [], "vehicle_data": [], "teslaproxi": 9, "car": 3, "energi": 7}, "envversion": {"sphinx.domains.c": 2, "sphinx.domains.changeset": 1, "sphinx.domains.citation": 1, "sphinx.domains.cpp": 6, "sphinx.domains.index": 1, "sphinx.domains.javascript": 2, "sphinx.domains.math": 2, "sphinx.domains.python": 3, "sphinx.domains.rst": 2, "sphinx.domains.std": 2, "sphinx": 56}}) \ No newline at end of file diff --git a/docs/html/teslajsonpy/teslajsonpy.__version__.html b/docs/html/teslajsonpy/teslajsonpy.__version__.html index 77fd07de..1ad2bac0 100644 --- a/docs/html/teslajsonpy/teslajsonpy.__version__.html +++ b/docs/html/teslajsonpy/teslajsonpy.__version__.html @@ -20,7 +20,7 @@ - + @@ -43,11 +43,12 @@
        • teslajsonpy
          • Submodules
          • @@ -86,7 +87,6 @@

            teslajsonpy.__version__

            Python Package for controlling Tesla API.

            -

            This is the version info.

            For more details about this api, please refer to the documentation at https://github.com/zabuldon/teslajsonpy

            @@ -96,7 +96,7 @@

            diff --git a/docs/html/teslajsonpy/teslajsonpy.car.html b/docs/html/teslajsonpy/teslajsonpy.car.html new file mode 100644 index 00000000..31ae5049 --- /dev/null +++ b/docs/html/teslajsonpy/teslajsonpy.car.html @@ -0,0 +1,125 @@ + + + + + + + teslajsonpy.car — teslajsonpy 0.12.2 documentation + + + + + + + + + + + + + + + + + + +
            + + +
            + +
            + +
            +
            +
            + + + + \ No newline at end of file diff --git a/docs/html/teslajsonpy/teslajsonpy.connection.html b/docs/html/teslajsonpy/teslajsonpy.connection.html index 30ec8436..8eb8f5df 100644 --- a/docs/html/teslajsonpy/teslajsonpy.connection.html +++ b/docs/html/teslajsonpy/teslajsonpy.connection.html @@ -21,7 +21,7 @@ - + @@ -43,11 +43,12 @@
          • teslajsonpy
            • Submodules
            • @@ -86,8 +87,6 @@

              teslajsonpy.connection

              Python Package for controlling Tesla API.

              -

              SPDX-License-Identifier: Apache-2.0

              -

              Underlying connection logic.

              For more details about this api, please refer to the documentation at https://github.com/zabuldon/teslajsonpy

              @@ -96,7 +95,7 @@
              diff --git a/docs/html/teslajsonpy/teslajsonpy.const.html b/docs/html/teslajsonpy/teslajsonpy.const.html index f8adacc4..46d74019 100644 --- a/docs/html/teslajsonpy/teslajsonpy.const.html +++ b/docs/html/teslajsonpy/teslajsonpy.const.html @@ -43,11 +43,12 @@
            • teslajsonpy
              • Submodules
              • diff --git a/docs/html/teslajsonpy/teslajsonpy.controller.html b/docs/html/teslajsonpy/teslajsonpy.controller.html index 48c1ed73..e3388234 100644 --- a/docs/html/teslajsonpy/teslajsonpy.controller.html +++ b/docs/html/teslajsonpy/teslajsonpy.controller.html @@ -20,7 +20,7 @@ - + @@ -43,11 +43,12 @@
              • teslajsonpy
                • Submodules
                • @@ -86,8 +87,6 @@

                  teslajsonpy.controller

                  Python Package for controlling Tesla API.

                  -

                  SPDX-License-Identifier: Apache-2.0

                  -

                  Controller to control access to the Tesla API.

                  For more details about this api, please refer to the documentation at https://github.com/zabuldon/teslajsonpy

                  @@ -97,7 +96,7 @@

                  diff --git a/docs/html/teslajsonpy/teslajsonpy.energy.html b/docs/html/teslajsonpy/teslajsonpy.energy.html new file mode 100644 index 00000000..ca0a5ccd --- /dev/null +++ b/docs/html/teslajsonpy/teslajsonpy.energy.html @@ -0,0 +1,125 @@ + + + + + + + teslajsonpy.energy — teslajsonpy 0.12.2 documentation + + + + + + + + + + + + + + + + + + +
                  + + +
                  + +
                  + +
                  +
                  +
                  + + + + \ No newline at end of file diff --git a/docs/html/teslajsonpy/teslajsonpy.exceptions.html b/docs/html/teslajsonpy/teslajsonpy.exceptions.html index 10b93704..c8972906 100644 --- a/docs/html/teslajsonpy/teslajsonpy.exceptions.html +++ b/docs/html/teslajsonpy/teslajsonpy.exceptions.html @@ -20,8 +20,8 @@ - - + + @@ -43,11 +43,12 @@
                • teslajsonpy
                  • Submodules
                  • @@ -94,8 +95,8 @@

                    diff --git a/docs/html/teslajsonpy/teslajsonpy.html b/docs/html/teslajsonpy/teslajsonpy.html index 6345d0bf..96e8caaf 100644 --- a/docs/html/teslajsonpy/teslajsonpy.html +++ b/docs/html/teslajsonpy/teslajsonpy.html @@ -43,11 +43,12 @@
                  • teslajsonpy
                    • Submodules
                    • @@ -100,31 +101,12 @@

                      Submodules @@ -132,1973 +114,1501 @@

                      Submodules

                      Classes

                        +
                      • TeslaCar: +Represents a Tesla car.

                      • Connection: Connection to Tesla Motors API.

                      • Controller: Controller for connections to Tesla Motors API.

                      • +
                      • EnergySite: +Base class to represents a Tesla energy site.

                      • +
                      • PowerwallSite: +Represents a Tesla Energy Powerwall site.

                      • TeslaProxy: Class to handle proxy login connections to Alexa.

                      • -
                      • Battery: -Home-Assistant battery class for a Tesla VehicleDevice.

                      • -
                      • Range: -Home-Assistant class of the battery range for a Tesla VehicleDevice.

                      • -
                      • ChargerConnectionSensor: -Home-assistant charger connection class for Tesla vehicles.

                      • -
                      • ChargingSensor: -Home-Assistant charging sensor class for a Tesla VehicleDevice.

                      • -
                      • OnlineSensor: -Home-Assistant Online sensor class for a Tesla VehicleDevice.

                      • -
                      • ParkingSensor: -Home-assistant parking brake class for Tesla vehicles.

                      • -
                      • UpdateSensor: -Home-Assistant update sensor class for a Tesla VehicleDevice.

                      • -
                      • ChargerSwitch: -Home-Assistant class for the charger of a Tesla VehicleDevice.

                      • -
                      • RangeSwitch: -Home-Assistant class for setting range limit for charger.

                      • -
                      • Climate: -Home-assistant class of HVAC for Tesla vehicles.

                      • -
                      • TempSensor: -Home-assistant class of temperature sensors for Tesla vehicles.

                      • -
                      • GPS: -Home-assistant class for GPS of Tesla vehicles.

                      • -
                      • Odometer: -Home-assistant class for odometer of Tesla vehicles.

                      • -
                      • Lock: -Home-assistant lock class for Tesla vehicles.

                      • -
                      • SentryModeSwitch: -Home-Assistant class for sentry mode of Tesla vehicles.

                      • -
                      • Horn: -Home-Assistant class for horn of Tesla vehicles.

                      • -
                      • FlashLights: -Home-Assistant class for flash lights of Tesla vehicles.

                      • -
                      • TriggerHomelink: -Home-Assistant class for trigger homelink of Tesla vehicles.

                      • -
                      • TrunkLock: -Home-Assistant rear trunk lock for a Tesla VehicleDevice.

                      • -
                      • FrunkLock: -Home-Assistant front trunk (frunk) lock for a Tesla VehicleDevice.

                      • +
                      • SolarPowerwallSite: +Represents a Tesla Energy Solar site with Powerwall(s).

                      • +
                      • SolarSite: +Represents a Tesla Energy Solar site.

                      -
                      -class teslajsonpy.Connection(websession: AsyncClient, email: Optional[str] = None, password: Optional[str] = None, access_token: Optional[str] = None, refresh_token: Optional[str] = None, authorization_token: Optional[str] = None, expiration: int = 0, auth_domain: str = 'https://auth.tesla.com')
                      -

                      Connection to Tesla Motors API.

                      +
                      +class teslajsonpy.TeslaCar(car: dict, controller, vehicle_data: dict)
                      +

                      Represents a Tesla car.

                      +

                      This class shouldn’t be instantiated directly; it will be instantiated +by teslajsonpy.controller.generate_car_objects().

                      Inheritance

                      -digraph inheritance087f428942 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "Connection" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.Connection",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Connection to Tesla Motors API."]; -} -
                      -
                      -async close() None
                      -

                      Close connection.

                      -
                      - -
                      -
                      -async get(command)
                      -

                      Get data from API.

                      -
                      - -
                      -
                      -async get_authorization_code(email: str, password: str, mfa_code: str = '', mfa_device: int = 0, retry_limit: int = 3) str
                      -

                      Get authorization code from the oauth3 login method.

                      -
                      - -
                      - -

                      Get authorization code url for the oauth3 login method.

                      -
                      - -
                      -
                      -async get_bearer_token(access_token)
                      -

                      Get bearer token. This is used by the owners API.

                      -
                      - -
                      -
                      -async get_sso_auth_token(code)
                      -

                      Get sso auth token.

                      -
                      - -
                      -
                      -async post(command, method='post', data=None, url='')
                      -

                      Post data to API.

                      +
                      +

                      Inheritance diagram of TeslaCar

                      +
                      +
                      +property battery_level: float
                      +

                      Return car battery level.

                      -
                      -
                      -async refresh_access_token(refresh_token)
                      -

                      Refresh access token from sso.

                      +
                      +
                      +property battery_range: float
                      +

                      Return car battery range.

                      -
                      -
                      -async websocket_connect(vin: int, vehicle_id: int, **kwargs)
                      -

                      Connect to Tesla streaming websocket.

                      -
                      -
                      Parameters
                      -
                        -
                      • vin (int) – vin of vehicle

                      • -
                      • vehicle_id (int) – vehicle_id from Tesla api

                      • -
                      • on_message (function) – function to call on a valid message. It must -process a json delivered in data

                      • -
                      • on_disconnect (function) – function to call on a disconnect message. It must -process a json delivered in data

                      • -
                      -
                      -
                      +
                      +
                      +property cabin_overheat_protection: str
                      +

                      Return cabin overheat protection.

                      +
                      +
                      +property car_type: str
                      +

                      Return car type.

                      -
                      -
                      -class teslajsonpy.Controller(websession: Optional[AsyncClient] = None, email: Optional[str] = None, password: Optional[str] = None, access_token: Optional[str] = None, refresh_token: Optional[str] = None, expiration: int = 0, update_interval: int = 300, enable_websocket: bool = False, polling_policy: Optional[str] = None, auth_domain: str = 'https://auth.tesla.com')
                      -

                      Controller for connections to Tesla Motors API.

                      -

                      Inheritance

                      -digraph inheritancef1ba0f48d0 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "Controller" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.Controller",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Controller for connections to Tesla Motors API."]; -} -
                      -
                      -async api(name: str, path_vars=None, wake_if_asleep: bool = False, **kwargs)
                      -

                      Perform api request for given endpoint name, with keyword arguments as parameters.

                      -

                      Code from https://github.com/tdorssers/TeslaPy/blob/master/teslapy/__init__.py#L242-L277 under MIT

                      -
                      -
                      Parameters
                      -
                      -
                      -
                      Raises
                      -
                        -
                      • ValueError: – If endpoint name is not found

                      • -
                      • NotImplementedError: – Endpoint method not implemented

                      • -
                      • ValueError: – Path variables missing

                      • -
                      -
                      -
                      Returns
                      -

                      Tesla json response object.

                      -
                      -
                      Return type
                      -

                      dict

                      -
                      -
                      +
                      +
                      +property car_version: str
                      +

                      Return installed car software version.

                      -
                      -charging_state(car_id: Optional[str] = None, vin: Optional[str] = None) str
                      -

                      Return charging state for a single vehicle.

                      +
                      +async change_charge_limit(value: float) None
                      +

                      Send command to change charge limit.

                      -
                      -
                      -async command(car_id, name, data=None, wake_if_asleep=True, product_type: str = 'vehicles')
                      -

                      Post name command to the car_id.

                      -

                      This will be deprecated. Use teslajsonpy.Controller.api() instead.

                      -
                      -
                      Parameters
                      -
                        -
                      • car_id (string) – Identifier for the car on the owner-api endpoint. It is the id -field for identifying the car across the owner-api endpoint. -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.

                      • -
                      • product_type (string) – Indicates whether this is a vehicle or a energy site. Defaults to TESLA_PRODUCT_TYPE_VEHICLES

                      • -
                      -
                      -
                      Returns
                      -

                      Tesla json object.

                      -
                      -
                      Return type
                      -

                      dict

                      -
                      -
                      +
                      +
                      +property charge_current_request: int
                      +

                      Return charge current request.

                      -
                      -
                      -async connect(test_login: bool = False, wake_if_asleep: bool = False, filtered_vins: Optional[List[str]] = None, mfa_code: str = '') Dict[str, str]
                      -

                      Connect controller to Tesla.

                      -
                      -
                      Args

                      test_login (bool, optional): Whether to test credentials only. Defaults to False. -wake_if_asleep (bool, optional): Whether to wake up any sleeping cars to update state. Defaults to False. -filtered_vins (list, optional): If not empty, filters the cars by the provided VINs. -mfa_code (Text, optional): MFA code to use for connection

                      -
                      -
                      Returns

                      Dict[Text, Text]: Returns the refresh_token, access_token, id_token and expires_in time

                      -
                      -
                      +
                      +
                      +property charge_current_request_max: int
                      +

                      Return charge current request max.

                      -
                      -
                      -async disconnect() None
                      -

                      Disconnect from Tesla api.

                      +
                      +
                      +property charge_energy_added: float
                      +

                      Return charge energy added.

                      -
                      -
                      -async get(car_id, command, wake_if_asleep=False, product_type: str = 'vehicles')
                      -

                      Send get command to the car_id.

                      -

                      This is a wrapped function by wake_up.

                      -
                      -
                      Parameters
                      -
                      -
                      -
                      Returns
                      -

                      Tesla json object.

                      -
                      -
                      Return type
                      -

                      dict

                      -
                      -
                      +
                      +
                      +property charge_limit_soc: int
                      +

                      Return charge limit soc.

                      -
                      -
                      -get_car_online(car_id: Optional[str] = None, vin: Optional[str] = None)
                      -

                      Get online status for car_id or all cars.

                      -
                      -
                      Parameters
                      -
                      -
                      -
                      Returns
                      -

                      If car_id or vin exists, a boolean with the online status for a -single car. -Othewise, the entire dictionary with all cars.

                      -
                      -
                      Return type
                      -

                      dict or boolean

                      -
                      -
                      +
                      +
                      +property charge_limit_soc_max: int
                      +

                      Return charge limit soc max.

                      -
                      -
                      -get_charging_params(car_id: Optional[str] = None, vin: Optional[str] = None) Dict
                      -

                      Return cached copy of charging_params for car_id or all cars.

                      -
                      -
                      Parameters
                      -
                      -
                      -
                      Returns
                      -

                      If car_id or vin exists, a dict with the charging parameters for a -single car. -Othewise, the entire dictionary with all cars.

                      -
                      -
                      Return type
                      -

                      dict

                      -
                      -
                      +
                      +
                      +property charge_limit_soc_min: int
                      +

                      Return charge limit soc min.

                      -
                      -
                      -get_climate_params(car_id: Optional[str] = None, vin: Optional[str] = None) Dict
                      -

                      Return cached copy of climate_params for car_id or all cars.

                      -
                      -
                      Parameters
                      -
                      -
                      -
                      Returns
                      -

                      If car_id or vin exists, a dict with the climate parameters for a -single car. -Othewise, the entire dictionary with all cars.

                      -
                      -
                      Return type
                      -

                      dict

                      -
                      -
                      +
                      +
                      +property charge_miles_added_ideal: float
                      +

                      Return charge ideal miles added.

                      -
                      -
                      -get_config_params(car_id: Optional[str] = None, vin: Optional[str] = None) Dict
                      -

                      Return cached copy of config_params for car_id or all cars.

                      -
                      -
                      Parameters
                      -
                      -
                      -
                      Returns
                      -

                      If car_id or vin exists, a dict with the config parameters for a -single car. -Othewise, the entire dictionary with all cars.

                      -
                      -
                      Return type
                      -

                      dict

                      -
                      -
                      +
                      +
                      +property charge_miles_added_rated: float
                      +

                      Return charge rated miles added.

                      -
                      -get_drive_params(car_id: Optional[str] = None, vin: Optional[str] = None) Dict
                      -

                      Return cached copy of drive_params for car_id or all cars.

                      -
                      -
                      Parameters
                      -
                      -
                      -
                      Returns
                      -

                      If car_id or vin exists, a dict with the drive parameters for a -single car. -Othewise, the entire dictionary with all cars.

                      -
                      -
                      Return type
                      -

                      dict

                      -
                      -
                      +
                      +async charge_port_door_close() None
                      +

                      Send command to close charge port door.

                      -
                      -async get_energysites()
                      -

                      Get energy sites json from TeslaAPI and filter to solar.

                      +
                      +async charge_port_door_open() None
                      +

                      Send command to open charge port door.

                      -
                      -
                      -get_expiration() int
                      -

                      Return expiration for oauth.

                      +
                      +
                      +property charge_port_latch: str
                      +

                      Return charger port latch state.

                      -
                      Returns

                      int: Returns timestamp when oauth expires

                      -
                      -
                      -
                      - -
                      -
                      -get_gui_params(car_id: Optional[str] = None, vin: Optional[str] = None) Dict
                      -

                      Return cached copy of gui_params for car_id or all cars.

                      -
                      -
                      Parameters
                      -
                      -
                      -
                      Returns
                      -

                      If car_id or vin exists, a dict with the gui parameters for a -single car. -Othewise, the entire dictionary with all cars.

                      -
                      -
                      Return type
                      -

                      dict

                      +
                      Returns

                      str: Engaged

                      +

                      Other states?

                      -
                      -
                      -get_homeassistant_components()
                      -

                      Return list of Tesla components for Home Assistant setup.

                      -

                      Use get_vehicles() for general API use.

                      -
                      - -
                      -
                      -get_last_park_time(car_id: Optional[str] = None, vin: Optional[str] = None)
                      -

                      Get park_time.

                      -
                      -
                      Parameters
                      -
                      -
                      -
                      Returns
                      -

                      If car_id exists, a int (time.time()) indicating when car was last -parked. Othewise, the entire updates dictionary.

                      -
                      -
                      Return type
                      -

                      int or dict of ints

                      -
                      -
                      -
                      - -
                      -
                      -get_last_update_time(car_id: Optional[str] = None, vin: Optional[str] = None)
                      -

                      Get last_update time dictionary.

                      -
                      -
                      Parameters
                      -
                      -
                      -
                      Returns
                      -

                      If car_id exists, a int (time.time()) indicating when updates last -processed. Othewise, the entire updates dictionary.

                      -
                      -
                      Return type
                      -

                      int or dict of ints

                      -
                      -
                      +
                      +
                      +property charge_rate: str
                      +

                      Return charge rate.

                      -
                      -
                      -get_last_wake_up_time(car_id: Optional[str] = None, vin: Optional[str] = None)
                      -

                      Get wakeup_time.

                      -
                      -
                      Parameters
                      -
                      -
                      -
                      Returns
                      -

                      If car_id exists, a int (time.time()) indicating when car was last -waken up. Othewise, the entire updates dictionary.

                      -
                      -
                      Return type
                      -

                      int or dict of ints

                      -
                      -
                      +
                      +
                      +property charger_actual_current: int
                      +

                      Return charger actual current.

                      -
                      -
                      -get_oauth_url() URL
                      -

                      Return oauth url.

                      +
                      +
                      +property charger_phases: int
                      +

                      Return charger phase.

                      -
                      -
                      -get_power_params(site_id: str) Dict
                      -

                      Return cached copy of power_params for site_id.

                      +
                      +
                      +property charger_power: int
                      +

                      Return charger power.

                      -
                      -
                      -get_state_params(car_id: Optional[str] = None, vin: Optional[str] = None) Dict
                      -

                      Return cached copy of state_params for car_id. or all cars.

                      -
                      -
                      Parameters
                      -
                      -
                      -
                      Returns
                      -

                      If car_id or vin exists, a dict with the state parameters for a -single car. -Othewise, the entire dictionary with all cars.

                      -
                      -
                      Return type
                      -

                      dict

                      -
                      -
                      +
                      +
                      +property charger_voltage: int
                      +

                      Return charger voltage.

                      -
                      -
                      -get_tokens() Dict[str, str]
                      -

                      Return oauth data including refresh and access tokens, and expires time.

                      -

                      This will set the the self.__connection token_refreshed to False.

                      +
                      +
                      +property charging_state: str
                      +

                      Return charging state.

                      -
                      Returns

                      Dict[Text, Text]: Returns the refresh_token, access_token, id_token and expires time

                      +
                      Returns

                      str: Charging, Stopped, Complete, Disconnected, NoPower +None: When car is asleep

                      -
                      -
                      -get_update_interval_vin(car_id: Optional[str] = None, vin: Optional[str] = None) int
                      -

                      Get update interval for specific vin or default if no vin specific.

                      -
                      - -
                      -
                      -get_updates(car_id: Optional[str] = None, vin: Optional[str] = None)
                      -

                      Get updates dictionary.

                      -
                      -
                      Parameters
                      -
                      -
                      -
                      Returns
                      -

                      If car_id or vin exists, a bool indicating whether updates should be -processed. Othewise, the entire updates dictionary.

                      -
                      -
                      Return type
                      -

                      bool or dict of booleans

                      +
                      +
                      +property climate_keeper_mode: str
                      +

                      Return climate keeper mode mode.

                      +
                      +
                      Returns

                      str: dog, camp, on, off

                      +

                      Not supported on all Tesla models.

                      -
                      -
                      -async get_vehicles()
                      -

                      Get vehicles json from TeslaAPI.

                      -
                      - -
                      -
                      -is_car_online(car_id: Optional[str] = None, vin: Optional[str] = None) bool
                      -

                      Alias for get_car_online for better readability.

                      -
                      - -
                      -
                      -is_climate_on(car_id: Optional[str] = None, vin: Optional[str] = None) bool
                      -

                      Return true if climate is on.

                      -
                      - -
                      -
                      -is_in_gear(car_id: Optional[str] = None, vin: Optional[str] = None) bool
                      -

                      Return true if car is in gear. False of car is parked or unknown.

                      -
                      - -
                      -
                      -is_sentry_mode_on(car_id: Optional[str] = None, vin: Optional[str] = None) bool
                      -

                      Return true if sentry_mode is on.

                      -
                      - -
                      -
                      -is_token_refreshed() bool
                      -

                      Return whether token has been changed and not retrieved.

                      -
                      -
                      Returns

                      bool: Whether token has been changed since the last return

                      -
                      -
                      +
                      +
                      +property conn_charge_cable: str
                      +

                      Return charge cable connection.

                      -
                      -
                      -async post(car_id, command, data=None, wake_if_asleep=True, product_type: str = 'vehicles')
                      -

                      Send post command to the car_id.

                      -

                      This is a wrapped function by wake_up.

                      -
                      -
                      Parameters
                      -
                        -
                      • car_id (string) – Identifier for the car on the owner-api endpoint. It is the id -field for identifying the car across the owner-api endpoint. -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.

                      • -
                      • product_type (string) – Indicates whether this is a vehicle or a energy site. Defaults to TESLA_PRODUCT_TYPE_VEHICLES

                      • -
                      -
                      -
                      Returns
                      -

                      Tesla json object.

                      -
                      -
                      Return type
                      -

                      dict

                      -
                      -
                      +
                      +
                      +property data_available: bool
                      +

                      Return if data from VEHICLE_DATA endpoint is available.

                      -
                      -
                      -register_websocket_callback(callback) int
                      -

                      Register callback for websocket messages.

                      +
                      +
                      +property defrost_mode: int
                      +

                      Return defrost mode.

                      -
                      Args

                      callback (function): function to call with json data

                      -
                      -
                      Returns

                      int: Return index of entry

                      +
                      Returns

                      int: 2 (on), 0 (off)

                      -
                      -
                      -set_authorization_code(code: str) None
                      -

                      Set authorization code in Connection.

                      +
                      +
                      +property display_name: str
                      +

                      Return display name.

                      -
                      -
                      -set_authorization_domain(domain: str) None
                      -

                      Set authorization domain in Connection.

                      +
                      +
                      +property driver_temp_setting: float
                      +

                      Return driver temperature setting.

                      -
                      -
                      -set_car_online(car_id: Optional[str] = None, vin: Optional[str] = None, online_status: bool = True) None
                      -

                      Set online status for car_id.

                      -

                      Will also update “last_wake_up_time” if the car changes from offline -to online

                      -
                      -
                      Parameters
                      -
                        -
                      • car_id (string) – Identifier for the car on the owner-api endpoint.

                      • -
                      • vin (string) – VIN number

                      • -
                      • online_status (boolean) – True if the car is online (awake) -False if the car is offline (out of reach or sleeping)

                      • -
                      -
                      -
                      +
                      +
                      +property fan_status: int
                      +

                      Return fan status setting.

                      -
                      -
                      -set_charging_params(car_id: Optional[str] = None, vin: Optional[str] = None, params: Optional[Dict] = None) None
                      -

                      Set charging_params for car_id.

                      +
                      +
                      +property fast_charger_brand: str
                      +

                      Return fast charger brand.

                      -
                      -
                      -set_climate_params(car_id: Optional[str] = None, vin: Optional[str] = None, params: Optional[Dict] = None) None
                      -

                      Set climate_params for car_id.

                      +
                      +
                      +property fast_charger_present: bool
                      +

                      Return fast charger present.

                      -
                      -
                      -set_config_params(car_id: Optional[str] = None, vin: Optional[str] = None, params: Optional[Dict] = None) None
                      -

                      Set config parameters for a car.

                      +
                      +
                      +property fast_charger_type: str
                      +

                      Return fast charger type.

                      -
                      -set_drive_params(car_id: Optional[str] = None, vin: Optional[str] = None, params: Optional[Dict] = None) None
                      -

                      Set drive_params for car_id.

                      +
                      +async flash_lights() None
                      +

                      Send command to flash lights.

                      -
                      -set_gui_params(car_id: Optional[str] = None, vin: Optional[str] = None, params: Optional[Dict] = None) None
                      -

                      Set GUI params for car.

                      +
                      +get_seat_heater_status(seat_id: int) int
                      +

                      Return status of seat heater for a given seat.

                      -
                      -
                      -set_id_vin(car_id: str, vin: str) None
                      -

                      Update mappings of car_id <–> vin.

                      +
                      +
                      +property gui_distance_units: str
                      +

                      Return gui distance units.

                      -
                      -
                      -set_last_park_time(car_id: Optional[str] = None, vin: Optional[str] = None, timestamp: float = 0, shift_state: Optional[str] = None) None
                      -

                      Set park_time for car_id.

                      +
                      +
                      +property gui_range_display: str
                      +

                      Return range display.

                      -
                      -
                      -set_last_update_time(car_id: Optional[str] = None, vin: Optional[str] = None, timestamp: float = 0) None
                      -

                      Set updated_time for car_id.

                      +
                      +
                      +property heading: int
                      +

                      Return heading.

                      -
                      -
                      -set_last_wake_up_time(car_id: Optional[str] = None, vin: Optional[str] = None, timestamp: float = 0) None
                      -

                      Set wakeup_time for car_id.

                      +
                      + +

                      Return Homelink device count.

                      -
                      -
                      -set_state_params(car_id: Optional[str] = None, vin: Optional[str] = None, params: Optional[Dict] = None) None
                      -

                      Set state_params for car_id.

                      +
                      + +

                      Return Homelink nearby.

                      -
                      -set_update_interval_vin(car_id: Optional[str] = None, vin: Optional[str] = None, value: Optional[int] = None) None
                      -

                      Set update interval for specific vin.

                      +
                      +async honk_horn() None
                      +

                      Send command to honk horn.

                      -
                      -
                      -set_updates(car_id: Optional[str] = None, vin: Optional[str] = None, value: bool = False) None
                      -

                      Set updates dictionary.

                      -

                      If a vehicle is enabled, the vehicle will force an update on next poll.

                      -
                      -
                      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

                      • -
                      • vin (string) – Vin number

                      • -
                      • value (bool) – Whether the specific car_id should be updated.

                      • -
                      -
                      -
                      Return type
                      -

                      None

                      -
                      -
                      +
                      +
                      +property id: int
                      +

                      Return id.

                      -
                      -
                      -set_vehicle_id_vin(vehicle_id: str, vin: str) None
                      -

                      Update mappings of vehicle_id <–> vin.

                      +
                      +
                      +property ideal_battery_range: float
                      +

                      Return car ideal battery range.

                      -
                      -
                      -shift_state(car_id: Optional[str] = None, vin: Optional[str] = None) str
                      -

                      Return shift state for a single vehicle.

                      +
                      +
                      +property in_service: bool
                      +

                      Return car in_service.

                      -
                      -
                      -async update(car_id: Optional[str] = None, wake_if_asleep: bool = False, force: bool = False) bool
                      -

                      Update all vehicle and energy site 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]. -It will then update all the cached values for cars that are awake -assuming no update has occurred for at least the [update_interval].

                      -

                      For energy sites, they will only be updated if car_id is blank.

                      -
                      -
                      Args

                      car_id (Text, optional): The vehicle to update. If None, all cars are updated. Defaults to None. -wake_if_asleep (bool, optional): force a vehicle awake. This is processed by the wake_up decorator. Defaults to False. -force (bool, optional): force a vehicle update regardless of the update_interval. Defaults to False.

                      -
                      -
                      Returns

                      Whether update was successful.

                      -
                      -
                      Raises

                      RetryLimitError

                      -
                      -
                      +
                      +
                      +property inside_temp: float
                      +

                      Return inside temperature.

                      -
                      -property update_interval: int
                      -

                      Return update_interval.

                      +
                      +property is_charge_port_door_open: bool
                      +

                      Return charger port door open.

                      +
                      + +
                      +
                      +property is_climate_on: bool
                      +

                      Return climate is on.

                      +
                      + +
                      +
                      +property is_frunk_closed: bool
                      +

                      Return car frunk is closed.

                      -
                      Returns

                      int: The number of seconds between updates

                      +
                      Returns

                      bool: True (0), False (255)

                      -
                      -
                      -async vehicle_data_request(car_id, name, wake_if_asleep=False)
                      -

                      Get requested data from car_id.

                      -
                      -
                      Parameters
                      -
                      -
                      -
                      Returns
                      -

                      Tesla json object.

                      -
                      -
                      Return type
                      -

                      dict

                      -
                      -
                      +
                      +
                      +property is_in_gear: bool
                      +

                      Return car is gear (i.e. drive or reverse).

                      -
                      -
                      -vin_to_vehicle_id(vin: str) Optional[str]
                      -

                      Return vehicle_id for a vin.

                      +
                      +
                      +property is_locked: bool
                      +

                      Return car is locked.

                      +
                      +
                      +property is_on: bool
                      +

                      Return car is on.

                      -
                      -
                      -class teslajsonpy.TeslaProxy(proxy_url: URL, host_url: URL)
                      -

                      Class to handle proxy login connections to Alexa.

                      -

                      Inheritance

                      -digraph inheritance8fc76f5186 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "AuthCaptureProxy" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Class to handle proxy login connections."]; - "TeslaProxy" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.TeslaProxy",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Class to handle proxy login connections to Alexa."]; - "AuthCaptureProxy" -> "TeslaProxy" [arrowsize=0.5,style="setlinewidth(0.5)"]; -} -
                      -
                      -async modify_headers(site: URL, request: Request) MultiDict
                      -

                      Modify headers.

                      -

                      Return modified headers based on site and request. To disable auto header generation, -pass in a key const.SKIP_AUTO_HEADERS with a list of keys to not generate.

                      -

                      For example, to prevent User-Agent generation: {SKIP_AUTO_HEADERS : [“User-Agent”]}

                      -
                      -
                      Parameters
                      -
                        -
                      • site (URL) – URL of the next host request.

                      • -
                      • request (web.Request) – Proxy directed request. This will need to be changed for the actual host request.

                      • -
                      -
                      -
                      -
                      -
                      Returns

                      dict: Headers after modifications

                      -
                      -
                      +
                      +
                      +property is_steering_wheel_heater_on: bool
                      +

                      Return steering wheel heater.

                      -
                      -
                      -async static prepend_i18n_path(base_url: URL, html: str) str
                      -

                      Prepend path for i18n loadPath so it’ll reach the proxy.

                      -

                      This is intended to be used for to place the proxy_url path in front of relative urls for loadPath in i18next.

                      -
                      -
                      Parameters
                      -
                        -
                      • base_url (URL) – Base URL to prepend

                      • -
                      • html (str) – text to replace

                      • -
                      -
                      -
                      +
                      +
                      +property is_trunk_closed: bool
                      +

                      Return car trunk is closed.

                      -
                      Returns

                      str: Replaced text

                      +
                      Returns

                      bool: True (0), False (1-255)

                      -
                      -
                      -async static prepend_relative_urls(base_url: URL, html: str) str
                      -

                      Prepend relative urls with url host.

                      -

                      This is intended to be used for to place the proxy_url in front of relative urls in src=”/

                      -
                      -
                      Parameters
                      -
                        -
                      • base_url (URL) – Base URL to prepend

                      • -
                      • html (str) – text to replace

                      • -
                      -
                      -
                      -
                      -
                      Returns

                      str: Replaced text

                      -
                      -
                      +
                      +
                      +property latitude: float
                      +

                      Return latitude.

                      -
                      -async reset_data() None
                      -

                      Reset all stored data.

                      -

                      A proxy may need to service multiple login requests if the route is not torn down. This function will reset all data between logins.

                      +
                      +async lock() None
                      +

                      Send lock command.

                      -
                      -
                      -async test_url(resp: Response, data: Dict[str, Any], query: Dict[str, Any])
                      -

                      Test for a successful Tesla URL.

                      -

                      https://tesla-api.timdorr.com/api-basics/authentication#step-2-obtain-an-authorization-code

                      -
                      -
                      Parameters
                      -
                        -
                      • resp (httpx.Response) – The httpx response.

                      • -
                      • data (Dict[str, Any]) – Dictionary of all post data captured through proxy with overwrites for duplicate keys.

                      • -
                      • query (Dict[str, Any]) – Dictionary of all query data with overwrites for duplicate keys.

                      • -
                      -
                      -
                      -
                      -
                      Returns

                      Optional[Union[URL, str]]: URL for a http 302 redirect or str to display on success. None indicates test did not pass.

                      -
                      -
                      +
                      +
                      +property longitude: float
                      +

                      Return longitude.

                      +
                      +
                      +property max_avail_temp: float
                      +

                      Return max available temperature.

                      -
                      -
                      -class teslajsonpy.Battery(data: Dict, controller)
                      -

                      Home-Assistant battery class for a Tesla VehicleDevice.

                      -

                      Inheritance

                      -digraph inheritance5da92de2cf { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "Battery" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.Battery",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-Assistant battery class for a Tesla VehicleDevice."]; - "VehicleDevice" -> "Battery" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -} -
                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the battery state.

                      +
                      +
                      +property min_avail_temp: float
                      +

                      Return min available temperature.

                      -
                      -
                      -battery_charging() bool
                      -

                      Return the battery level.

                      +
                      +
                      +property native_heading: int
                      +

                      Return native heading.

                      -
                      -
                      -battery_level() int
                      -

                      Return the battery level.

                      +
                      +
                      +property native_latitude: float
                      +

                      Return native latitude.

                      -
                      -property device_class: str
                      -

                      Return the HA device class.

                      +
                      +property native_location_supported: int
                      +

                      Return native location supported.

                      -
                      -
                      -get_value() int
                      -

                      Return the battery level.

                      +
                      +
                      +property native_longitude: float
                      +

                      Return native longitude.

                      -
                      -
                      -static has_battery() bool
                      -

                      Return whether the device has a battery.

                      +
                      +
                      +property native_type: float
                      +

                      Return native type.

                      -
                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +
                      +property odometer: float
                      +

                      Return odometer.

                      +
                      +
                      +property outside_temp: float
                      +

                      Return outside temperature.

                      -
                      -
                      -class teslajsonpy.Range(data: Dict, controller)
                      -

                      Home-Assistant class of the battery range for a Tesla VehicleDevice.

                      -

                      Inheritance

                      -digraph inheritance3b6ce52305 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "Range" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.Range",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-Assistant class of the battery range for a Tesla VehicleDevice."]; - "VehicleDevice" -> "Range" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -} -
                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the battery range state.

                      +
                      +
                      +property passenger_temp_setting: float
                      +

                      Return passenger temperature setting.

                      -
                      -property device_class: str
                      -

                      Return the HA device class.

                      +
                      +property power: int
                      +

                      Return power.

                      -
                      -
                      -get_value()
                      -

                      Return the battery range.

                      -

                      This function will return either the rated range or the ideal range -based on the gui_settings.

                      +
                      +
                      +property powered_lift_gate: bool
                      +

                      Return True if car has power lift gate.

                      -
                      -
                      -static has_battery()
                      -

                      Return whether the device has a battery.

                      +
                      +
                      +property rear_seat_heaters: int
                      +

                      Return if car has rear (second row) heated seats.

                      +
                      +
                      Returns

                      int: 0 (no rear heated seats), int: ? (rear heated seats)

                      +
                      +
                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +async remote_seat_heater_request(level: int, seat_id: int) None
                      +

                      Send command to change seat heat.

                      +
                      +
                      Args

                      level: 0 (off), 1 (low), 2 (medium), 3 (high) +seat_id: 0 (front left), 1 (front right), 2 (rear left), 4 (rear center) +5 (rear right), 6 (third row left), 7 (third row right)

                      +
                      +
                      +
                      +
                      +async schedule_software_update(offset_sec: Optional[int] = 0) None
                      +

                      Send command to install software update.

                      -
                      -
                      -class teslajsonpy.ChargerConnectionSensor(data, controller)
                      -

                      Home-assistant charger connection class for Tesla vehicles.

                      -

                      This is intended to be partially inherited by a Home-Assitant entity.

                      -

                      Inheritance

                      -digraph inheritancedc9e0604ed { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "BinarySensor" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant binary sensor class for Tesla vehicles."]; - "VehicleDevice" -> "BinarySensor" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "ChargerConnectionSensor" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.ChargerConnectionSensor",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-assistant charger connection class for Tesla vehicles."]; - "BinarySensor" -> "ChargerConnectionSensor" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -} -
                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the charger connection sensor.

                      +
                      +
                      +property sentry_mode: bool
                      +

                      Return sentry mode.

                      -
                      -
                      -get_value() Optional[bool]
                      -

                      Return whether the charger cable is connected.

                      +
                      +
                      +property sentry_mode_available: bool
                      +

                      Return sentry mode available.

                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +async set_cabin_overheat_protection(option: str) None
                      +

                      Send command to set cabin overheat protection.

                      +
                      +
                      Args

                      option: “Off”, “No A/C”, “On”

                      +
                      +
                      +
                      +
                      +async set_charging_amps(value: float) None
                      +

                      Send command to set charging amps.

                      -
                      -
                      -class teslajsonpy.ChargingSensor(data: Dict, controller)
                      -

                      Home-Assistant charging sensor class for a Tesla VehicleDevice.

                      -

                      Inheritance

                      -digraph inheritance6a3dfb1ac4 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "ChargingSensor" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.ChargingSensor",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-Assistant charging sensor class for a Tesla VehicleDevice."]; - "VehicleDevice" -> "ChargingSensor" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -} -
                      -
                      -property added_range: float
                      -

                      Return the added range.

                      +
                      +
                      +async set_climate_keeper_mode(keeper_id: int) None
                      +

                      Send command to set climate keeper mode.

                      +
                      +
                      Args

                      keeper_id: 1 (keep on), 2 (dog mode), 3 (camp mode)

                      +
                      +
                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the battery state.

                      +
                      +async set_heated_steering_wheel(value: bool) None
                      +

                      Send command to set heated steering wheel.

                      -
                      -
                      -property charge_current_request: float
                      -

                      Return the requested current.

                      +
                      +
                      +async set_hvac_mode(value: str) None
                      +

                      Send command to set HVAC mode.

                      +
                      +
                      Args

                      value: on, off

                      +
                      +
                      -
                      -
                      -property charge_current_request_max: float
                      -

                      Return the requested current max.

                      +
                      +
                      +async set_max_defrost(state: int) None
                      +

                      Send command to set max defrost.

                      +
                      +
                      Args

                      state: 2 = on, 0 = off

                      +
                      +
                      -
                      -
                      -property charge_energy_added: float
                      -

                      Return the energy added.

                      +
                      +
                      +async set_sentry_mode(value: bool) None
                      +

                      Send command to set sentry mode.

                      -
                      -
                      -property charge_limit_soc: int
                      -

                      Return the state of charge limit.

                      +
                      +
                      +async set_temperature(temp: float) None
                      +

                      Send command to set temperature.

                      -
                      -property charger_actual_current: float
                      -

                      Return the actual current.

                      +
                      +property shift_state: str
                      +

                      Return shift state.

                      -
                      -property charger_power: float
                      -

                      Return the state of charger power.

                      +
                      +property software_update: dict
                      +

                      Return software update version information.

                      -
                      -property charger_voltage: float
                      -

                      Return the voltage.

                      +
                      +property speed: float
                      +

                      Return speed.

                      -
                      -
                      -property charging_rate: float
                      -

                      Return the charging rate.

                      +
                      +
                      +async start_charge() None
                      +

                      Send command to start charge.

                      -
                      -property device_class: str
                      -

                      Return the HA device class.

                      +
                      +property state: str
                      +

                      Return car state.

                      -
                      -
                      -static has_battery() bool
                      -

                      Return whether the device has a battery.

                      +
                      +
                      +property steering_wheel_heater: bool
                      +

                      Return steering wheel heater option.

                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +async stop_charge() None
                      +

                      Send command to start charge.

                      -
                      -property state_class: str
                      -

                      Return the state class.

                      +
                      +property third_row_seats: str
                      +

                      Return third row seats option.

                      +
                      +
                      Returns

                      str: None

                      +
                      +
                      -
                      -property time_left: float
                      -

                      Return the time left to full in hours.

                      -
                      - +
                      +property time_to_full_charge: float
                      +

                      Return time to full charge.

                      -
                      -
                      -class teslajsonpy.OnlineSensor(data: Dict, controller)
                      -

                      Home-Assistant Online sensor class for a Tesla VehicleDevice.

                      -

                      Inheritance

                      -digraph inheritance163c0cebb3 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "BinarySensor" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant binary sensor class for Tesla vehicles."]; - "VehicleDevice" -> "BinarySensor" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "OnlineSensor" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.OnlineSensor",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-Assistant Online sensor class for a Tesla VehicleDevice."]; - "BinarySensor" -> "OnlineSensor" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -}
                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the battery state.

                      +
                      +async toggle_frunk() None
                      +

                      Actuate front trunk.

                      -
                      -get_value() Optional[bool]
                      -

                      Return the car is online.

                      +
                      +async toggle_trunk() None
                      +

                      Actuate rear trunk.

                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +

                      Send command to trigger homelink.

                      +
                      +
                      +async unlock() None
                      +

                      Send unlock command.

                      -
                      -
                      -class teslajsonpy.ParkingSensor(data: Dict, controller)
                      -

                      Home-assistant parking brake class for Tesla vehicles.

                      -

                      This is intended to be partially inherited by a Home-Assitant entity.

                      -

                      Inheritance

                      -digraph inheritanceb808b41618 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "BinarySensor" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant binary sensor class for Tesla vehicles."]; - "VehicleDevice" -> "BinarySensor" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "ParkingSensor" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.ParkingSensor",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-assistant parking brake class for Tesla vehicles."]; - "BinarySensor" -> "ParkingSensor" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -} -
                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the parking brake sensor.

                      +
                      +
                      +property vehicle_id: int
                      +

                      Return car id.

                      -
                      -
                      -get_value() Optional[bool]
                      -

                      Return whether parking brake engaged.

                      +
                      +
                      +property vin: str
                      +

                      Return car vin.

                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +async wake_up() None
                      +

                      Send command to wake up.

                      -
                      -class teslajsonpy.UpdateSensor(data: Dict, controller)
                      -

                      Home-Assistant update sensor class for a Tesla VehicleDevice.

                      +
                      +class teslajsonpy.Connection(websession: AsyncClient, email: Optional[str] = None, password: Optional[str] = None, access_token: Optional[str] = None, refresh_token: Optional[str] = None, authorization_token: Optional[str] = None, expiration: int = 0, auth_domain: str = 'https://auth.tesla.com')
                      +

                      Connection to Tesla Motors API.

                      Inheritance

                      -digraph inheritancec8d3478604 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "BinarySensor" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant binary sensor class for Tesla vehicles."]; - "VehicleDevice" -> "BinarySensor" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "UpdateSensor" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.UpdateSensor",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-Assistant update sensor class for a Tesla VehicleDevice."]; - "BinarySensor" -> "UpdateSensor" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -} +
                      +

                      Inheritance diagram of Connection

                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the battery state.

                      -
                      - -
                      -
                      -property device_state_attributes: Optional[dict]
                      -

                      Return the optional state attributes.

                      +
                      +async close() None
                      +

                      Close connection.

                      -
                      -get_value() Optional[bool]
                      -

                      Return the car is online.

                      +
                      +async get(command)
                      +

                      Get data from API.

                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      -
                      - +
                      +async get_authorization_code(email: str, password: str, mfa_code: str = '', mfa_device: int = 0, retry_limit: int = 3) str
                      +

                      Get authorization code from the oauth3 login method.

                      -
                      -
                      -class teslajsonpy.ChargerSwitch(data, controller)
                      -

                      Home-Assistant class for the charger of a Tesla VehicleDevice.

                      -

                      Inheritance

                      -digraph inheritanceca9a0e4ac7 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "ChargerSwitch" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.ChargerSwitch",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-Assistant class for the charger of a Tesla VehicleDevice."]; - "VehicleDevice" -> "ChargerSwitch" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -}
                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the charging state of the Tesla Vehicle.

                      +
                      +

                      Get authorization code url for the oauth3 login method.

                      -
                      -static has_battery()
                      -

                      Return whether the Tesla charger has a battery.

                      +
                      +async get_bearer_token(access_token)
                      +

                      Get bearer token. This is used by the owners API.

                      -
                      -is_charging()
                      -

                      Return whether the Tesla Vehicle is charging.

                      +
                      +async get_sso_auth_token(code)
                      +

                      Get sso auth token.

                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +async post(command, method='post', data=None, url='')
                      +

                      Post data to API.

                      -
                      -async start_charge()
                      -

                      Start charging the Tesla Vehicle.

                      +
                      +async refresh_access_token(refresh_token)
                      +

                      Refresh access token from sso.

                      -
                      -async stop_charge()
                      -

                      Stop charging the Tesla Vehicle.

                      +
                      +async websocket_connect(vin: int, vehicle_id: int, **kwargs)
                      +

                      Connect to Tesla streaming websocket.

                      +
                      +
                      Parameters
                      +
                        +
                      • vin (int) – vin of vehicle

                      • +
                      • vehicle_id (int) – vehicle_id from Tesla api

                      • +
                      • on_message (function) – function to call on a valid message. It must +process a json delivered in data

                      • +
                      • on_disconnect (function) – function to call on a disconnect message. It must +process a json delivered in data

                      • +
                      +
                      +
                      -
                      -class teslajsonpy.RangeSwitch(data, controller)
                      -

                      Home-Assistant class for setting range limit for charger.

                      +
                      +class teslajsonpy.Controller(websession: Optional[AsyncClient] = None, email: Optional[str] = None, password: Optional[str] = None, access_token: Optional[str] = None, refresh_token: Optional[str] = None, expiration: int = 0, update_interval: int = 300, enable_websocket: bool = False, polling_policy: Optional[str] = None, auth_domain: str = 'https://auth.tesla.com')
                      +

                      Controller for connections to Tesla Motors API.

                      Inheritance

                      -digraph inheritance2f47351d84 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "RangeSwitch" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.RangeSwitch",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-Assistant class for setting range limit for charger."]; - "VehicleDevice" -> "RangeSwitch" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -} +
                      +

                      Inheritance diagram of Controller

                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the status of the range setting.

                      +
                      +async api(name: str, path_vars=None, wake_if_asleep: bool = False, **kwargs)
                      +

                      Perform api request for given endpoint name, with keyword arguments as parameters.

                      +

                      Code from https://github.com/tdorssers/TeslaPy/blob/master/teslapy/__init__.py#L242-L277 under MIT

                      +
                      +
                      Parameters
                      +
                      +
                      +
                      Raises
                      +
                        +
                      • ValueError: – If endpoint name is not found

                      • +
                      • NotImplementedError: – Endpoint method not implemented

                      • +
                      • ValueError: – Path variables missing

                      • +
                      +
                      +
                      Returns
                      +

                      Tesla json response object.

                      +
                      +
                      Return type
                      +

                      dict

                      +
                      +
                      -
                      -static has_battery()
                      -

                      Return whether the device has a battery.

                      +
                      +async connect(test_login: bool = False, include_vehicles: bool = True, include_energysites: bool = True, mfa_code: str = '') Dict[str, str]
                      +

                      Connect controller to Tesla.

                      +
                      +
                      Args

                      test_login (bool, optional): Whether to test credentials only. Defaults to False. +include_vehicles (bool, optional): Whether to include vehicles. Defaults to True. +include_energysites(bool, optional): Whether to include energysites. Defaults to True. +mfa_code (Text, optional): MFA code to use for connection

                      +
                      +
                      Returns

                      Dict[Text, Text]: Returns the refresh_token, access_token, id_token and expires_in time

                      +
                      +
                      -
                      -is_maxrange()
                      -

                      Return whether max range setting is set.

                      +
                      +async disconnect() None
                      +

                      Disconnect from Tesla api.

                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +async generate_car_objects(wake_if_asleep: bool = False, filtered_vins: Optional[List[str]] = None) Dict[str, TeslaCar]
                      +

                      Generate car objects.

                      +
                      +
                      Args

                      wake_if_asleep (bool, optional): Wake up vehicles if asleep. +filtered_vins (list, optional): If not empty, filters the cars by the provided VINs.

                      +
                      +
                      -
                      -async set_max()
                      -

                      Set the charger to max range for trips.

                      +
                      +async generate_energysite_objects() Dict[int, EnergySite]
                      +

                      Generate energy site objects.

                      -
                      -async set_standard()
                      -

                      Set the charger to standard range for daily commute.

                      +
                      +async get_battery_data(battery_id: str) dict
                      +

                      Get battery data json from TeslaAPI for a given battery_id.

                      +
                      +
                      +async get_battery_summary(battery_id: str) dict
                      +

                      Get site config json from TeslaAPI for a given battery_id.

                      -
                      -
                      -class teslajsonpy.Climate(data, controller)
                      -

                      Home-assistant class of HVAC for Tesla vehicles.

                      -

                      This is intended to be partially inherited by a Home-Assitant entity.

                      -

                      Inheritance

                      -digraph inheritance5cb5c12563 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "Climate" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.Climate",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-assistant class of HVAC for Tesla vehicles."]; - "VehicleDevice" -> "Climate" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -}
                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the HVAC state.

                      +
                      +get_car_online(car_id: Optional[str] = None, vin: Optional[str] = None)
                      +

                      Get online status for car_id or all cars.

                      +
                      +
                      Parameters
                      +
                      +
                      +
                      Returns
                      +

                      If car_id or vin exists, a boolean with the online status for a +single car. +Othewise, the entire dictionary with all cars.

                      +
                      +
                      Return type
                      +

                      dict or boolean

                      +
                      +
                      -
                      -get_current_temp()
                      -

                      Return vehicle inside temperature.

                      +
                      +get_expiration() int
                      +

                      Return expiration for oauth.

                      +
                      +
                      Returns

                      int: Returns timestamp when oauth expires

                      +
                      +
                      -
                      -get_fan_status()
                      -

                      Return fan status.

                      +
                      +get_last_park_time(car_id: Optional[str] = None, vin: Optional[str] = None)
                      +

                      Get park_time.

                      +
                      +
                      Parameters
                      +
                      +
                      +
                      Returns
                      +

                      If car_id exists, a int (time.time()) indicating when car was last +parked. Othewise, the entire updates dictionary.

                      +
                      +
                      Return type
                      +

                      int or dict of ints

                      +
                      +
                      -
                      -get_goal_temp()
                      -

                      Return driver set temperature.

                      +
                      +get_last_update_time(car_id: Optional[str] = None, vin: Optional[str] = None)
                      +

                      Get last_update time dictionary.

                      +
                      +
                      Parameters
                      +
                      +
                      +
                      Returns
                      +

                      If car_id exists, a int (time.time()) indicating when updates last +processed. Othewise, the entire updates dictionary.

                      +
                      +
                      Return type
                      +

                      int or dict of ints

                      +
                      +
                      -
                      -static has_battery()
                      -

                      Return whether the device has a battery.

                      +
                      +get_last_wake_up_time(car_id: Optional[str] = None, vin: Optional[str] = None)
                      +

                      Get wakeup_time.

                      +
                      +
                      Parameters
                      +
                      +
                      +
                      Returns
                      +

                      If car_id exists, a int (time.time()) indicating when car was last +waken up. Othewise, the entire updates dictionary.

                      +
                      +
                      Return type
                      +

                      int or dict of ints

                      +
                      +
                      -
                      -is_hvac_enabled()
                      -

                      Return whether HVAC is running.

                      +
                      +get_oauth_url() URL
                      +

                      Return oauth url.

                      -
                      -
                      -property preset_mode: Optional[str]
                      -

                      Return the current preset mode, e.g., home, away, temp.

                      -

                      Requires SUPPORT_PRESET_MODE.

                      +
                      +
                      +async get_product_list(wake_if_asleep: bool = False) list
                      +

                      Get product list from Tesla.

                      -
                      -
                      -property preset_modes: Optional[List[str]]
                      -

                      Return a list of available preset modes.

                      -

                      Requires SUPPORT_PRESET_MODE.

                      +
                      +
                      +async get_site_config(energysite_id: int) dict
                      +

                      Get site config json from TeslaAPI for a given energysite_id.

                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +async get_site_data(energysite_id: int) dict
                      +

                      Get site data json from TeslaAPI for a given energysite_id.

                      -
                      -async set_preset_mode(preset_mode: str) None
                      -

                      Set new preset mode.

                      +
                      +get_tokens() Dict[str, str]
                      +

                      Return oauth data including refresh and access tokens, and expires time.

                      +

                      This will set the the self.__connection token_refreshed to False.

                      +
                      +
                      Returns

                      Dict[Text, Text]: Returns the refresh_token, access_token, id_token and expires time

                      +
                      +
                      -
                      -async set_status(enabled)
                      -

                      Enable or disable the HVAC.

                      +
                      +get_update_interval_vin(car_id: Optional[str] = None, vin: Optional[str] = None) int
                      +

                      Get update interval for specific vin or default if no vin specific.

                      -
                      -async set_temperature(temp)
                      -

                      Set both the driver and passenger temperature to temp.

                      +
                      +get_updates(car_id: Optional[str] = None, vin: Optional[str] = None)
                      +

                      Get updates dictionary.

                      +
                      +
                      Parameters
                      +
                      +
                      +
                      Returns
                      +

                      If car_id or vin exists, a bool indicating whether updates should be +processed. Othewise, the entire updates dictionary.

                      +
                      +
                      Return type
                      +

                      bool or dict of booleans

                      +
                      +
                      +
                      +
                      +async get_vehicle_data(vin: str, wake_if_asleep: bool = False) dict
                      +

                      Get vehicle data json from TeslaAPI for a given vin.

                      -
                      -
                      -class teslajsonpy.TempSensor(data, controller)
                      -

                      Home-assistant class of temperature sensors for Tesla vehicles.

                      -

                      This is intended to be partially inherited by a Home-Assitant entity.

                      -

                      Inheritance

                      -digraph inheritance1182b8abef { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "TempSensor" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.TempSensor",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-assistant class of temperature sensors for Tesla vehicles."]; - "VehicleDevice" -> "TempSensor" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -}
                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the temperature.

                      +
                      +async get_vehicles(wake_if_asleep: bool = False) list
                      +

                      Get vehicles json from TeslaAPI.

                      -
                      -
                      -property device_class: str
                      -

                      Return the HA device class.

                      +
                      +
                      +is_car_online(car_id: Optional[str] = None, vin: Optional[str] = None) bool
                      +

                      Alias for get_car_online for better readability.

                      -
                      -get_inside_temp()
                      -

                      Get inside temperature.

                      +
                      +is_token_refreshed() bool
                      +

                      Return whether token has been changed and not retrieved.

                      +
                      +
                      Returns

                      bool: Whether token has been changed since the last return

                      +
                      +
                      -
                      -get_outside_temp()
                      -

                      Get outside temperature.

                      +
                      +register_websocket_callback(callback) int
                      +

                      Register callback for websocket messages.

                      +
                      +
                      Args

                      callback (function): function to call with json data

                      +
                      +
                      Returns

                      int: Return index of entry

                      +
                      +
                      -
                      -static has_battery()
                      -

                      Return whether the device has a battery.

                      +
                      +set_authorization_code(code: str) None
                      +

                      Set authorization code in Connection.

                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +set_authorization_domain(domain: str) None
                      +

                      Set authorization domain in Connection.

                      +
                      +
                      +set_car_online(car_id: Optional[str] = None, vin: Optional[str] = None, online_status: bool = True) None
                      +

                      Set online status for car_id.

                      +

                      Will also update “last_wake_up_time” if the car changes from offline +to online

                      +
                      +
                      Parameters
                      +
                        +
                      • car_id (string) – Identifier for the car on the owner-api endpoint.

                      • +
                      • vin (string) – VIN number

                      • +
                      • online_status (boolean) – True if the car is online (awake) +False if the car is offline (out of reach or sleeping)

                      • +
                      +
                      +
                      -
                      -
                      -class teslajsonpy.GPS(data, controller)
                      -

                      Home-assistant class for GPS of Tesla vehicles.

                      -

                      Inheritance

                      -digraph inheritance8afb2b6446 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "GPS" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.GPS",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-assistant class for GPS of Tesla vehicles."]; - "VehicleDevice" -> "GPS" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -}
                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the current GPS location.

                      +
                      +set_id_vin(car_id: str, vin: str) None
                      +

                      Update mappings of car_id <–> vin.

                      -
                      -get_location()
                      -

                      Return the current location.

                      +
                      +set_last_park_time(car_id: Optional[str] = None, vin: Optional[str] = None, timestamp: float = 0, shift_state: Optional[str] = None) None
                      +

                      Set park_time for car_id.

                      -
                      -static has_battery()
                      -

                      Return whether the device has a battery.

                      +
                      +set_last_update_time(car_id: Optional[str] = None, vin: Optional[str] = None, timestamp: float = 0) None
                      +

                      Set updated_time for car_id.

                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +set_last_wake_up_time(car_id: Optional[str] = None, vin: Optional[str] = None, timestamp: float = 0) None
                      +

                      Set wakeup_time for car_id.

                      +
                      +
                      +set_update_interval_vin(car_id: Optional[str] = None, vin: Optional[str] = None, value: Optional[int] = None) None
                      +

                      Set update interval for specific vin.

                      -
                      -
                      -class teslajsonpy.Odometer(data, controller)
                      -

                      Home-assistant class for odometer of Tesla vehicles.

                      -

                      Inheritance

                      -digraph inheritancea98c743a5d { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "Odometer" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.Odometer",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-assistant class for odometer of Tesla vehicles."]; - "VehicleDevice" -> "Odometer" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -}
                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the odometer and the unit of measurement based on GUI.

                      +
                      +set_updates(car_id: Optional[str] = None, vin: Optional[str] = None, value: bool = False) None
                      +

                      Set updates dictionary.

                      +

                      If a vehicle is enabled, the vehicle will force an update on next poll.

                      +
                      +
                      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

                      • +
                      • vin (string) – Vin number

                      • +
                      • value (bool) – Whether the specific car_id should be updated.

                      • +
                      +
                      +
                      Return type
                      +

                      None

                      +
                      +
                      -
                      -
                      -property device_class: str
                      -

                      Return the HA device class.

                      +
                      +
                      +set_vehicle_id_vin(vehicle_id: str, vin: str) None
                      +

                      Update mappings of vehicle_id <–> vin.

                      -
                      -get_value()
                      -

                      Return the odometer reading.

                      +
                      +async update(car_id: Optional[str] = None, wake_if_asleep: bool = False, force: bool = False) bool
                      +

                      Update all vehicle and energy site 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]. +It will then update all the cached values for cars that are awake +assuming no update has occurred for at least the [update_interval].

                      +

                      For energy sites, they will only be updated if car_id is blank.

                      +
                      +
                      Args

                      car_id (Text, optional): The vehicle to update. If None, all cars are updated. Defaults to None. +wake_if_asleep (bool, optional): force a vehicle awake. This is processed by the wake_up decorator. Defaults to False. +force (bool, optional): force a vehicle update regardless of the update_interval. Defaults to False.

                      +
                      +
                      Returns

                      Whether update was successful.

                      +
                      +
                      Raises

                      RetryLimitError

                      +
                      +
                      -
                      -
                      -static has_battery()
                      -

                      Return whether the device has a battery.

                      +
                      +
                      +property update_interval: int
                      +

                      Return update_interval.

                      +
                      +
                      Returns

                      int: The number of seconds between updates

                      +
                      +
                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +vin_to_vehicle_id(vin: str) Optional[str]
                      +

                      Return vehicle_id for a vin.

                      -
                      -class teslajsonpy.Lock(data, controller)
                      -

                      Home-assistant lock class for Tesla vehicles.

                      -

                      This is intended to be partially inherited by a Home-Assitant entity.

                      +
                      +class teslajsonpy.EnergySite(api: Callable, energysite: dict, site_config: dict)
                      +

                      Base class to represents a Tesla energy site.

                      Inheritance

                      -digraph inheritance832c3edb9e { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "Lock" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.Lock",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-assistant lock class for Tesla vehicles."]; - "VehicleDevice" -> "Lock" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -} -
                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the lock state.

                      +
                      +

                      Inheritance diagram of EnergySite

                      +
                      +
                      +property energysite_id: int
                      +

                      Return energy site id (aka site_id).

                      -
                      -
                      -static has_battery()
                      -

                      Return whether the device has a battery.

                      +
                      +
                      +property has_battery: bool
                      +

                      Return True if energy site has battery.

                      -
                      -
                      -is_locked()
                      -

                      Return whether doors are locked.

                      +
                      +
                      +property has_load_meter: bool
                      +

                      Return True if energy site has a load meter.

                      -
                      -
                      -async lock()
                      -

                      Lock the doors.

                      +
                      +
                      +property has_solar: bool
                      +

                      Return True if energy site has solar.

                      -
                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +
                      +property id: str
                      +

                      Return battery_id.

                      -
                      -
                      -async unlock()
                      -

                      Unlock the doors and extend handles where applicable.

                      +
                      +
                      +property resource_type: str
                      +

                      Return energy site type.

                      -
                      -class teslajsonpy.SentryModeSwitch(data, controller)
                      -

                      Home-Assistant class for sentry mode of Tesla vehicles.

                      +
                      +class teslajsonpy.PowerwallSite(api: Callable, energysite: dict, site_config: dict, battery_data: dict, battery_summary: dict)
                      +

                      Represents a Tesla Energy Powerwall site.

                      +

                      This class shouldn’t be instantiated directly; it will be instantiated +by teslajsonpy.controller.generate_energysite_objects().

                      Inheritance

                      -digraph inheritance02c463f274 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "SentryModeSwitch" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.SentryModeSwitch",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-Assistant class for sentry mode of Tesla vehicles."]; - "VehicleDevice" -> "SentryModeSwitch" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -} -
                      -
                      -async async_update(wake_if_asleep=False, force=False)
                      -

                      Update the sentry mode of the vehicle.

                      +
                      +

                      Inheritance diagram of PowerwallSite

                      +
                      +
                      +property backup_reserve_percent: int
                      +

                      Return backup reserve percentage.

                      -
                      -
                      -available() bool
                      -

                      Return whether the sentry mode is available.

                      +
                      +
                      +property battery_power: float
                      +

                      Return battery power in Watts.

                      -
                      -
                      -async disable_sentry_mode() None
                      -

                      Disable the sentry mode.

                      +
                      +
                      +property data_available: bool
                      +

                      Return if data is available.

                      -
                      -
                      -async enable_sentry_mode() None
                      -

                      Enable the sentry mode.

                      +
                      +
                      +property energy_left: float
                      +

                      Return battery energy left in Watt hours.

                      -
                      -
                      -static has_battery() bool
                      -

                      Return whether the device has a battery.

                      +
                      +
                      +property grid_power: float
                      +

                      Return grid power in Watts.

                      -
                      -
                      -is_on() Optional[bool]
                      -

                      Return whether the sentry mode is enabled, or None if sentry mode is not available.

                      +
                      +
                      +property grid_status: str
                      +

                      Return grid status.

                      -
                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +
                      +property load_power: float
                      +

                      Return load power in Watts.

                      +
                      +
                      +property operation_mode: str
                      +

                      Return operation mode.

                      -
                      -
                      -class teslajsonpy.Horn(data, controller)
                      -

                      Home-Assistant class for horn of Tesla vehicles.

                      -

                      Inheritance

                      -digraph inheritance35b4af9171 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "Horn" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.Horn",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-Assistant class for horn of Tesla vehicles."]; - "VehicleDevice" -> "Horn" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -} -
                      -
                      -async async_update(wake_if_asleep=False, force=False)
                      -

                      Update the horn of the vehicle.

                      +
                      +
                      +property percentage_charged: float
                      +

                      Return battery percentage charged.

                      -
                      -static has_battery() bool
                      -

                      Return whether the device has a battery.

                      +
                      +async set_operation_mode(real_mode: str) None
                      +

                      Set operation mode of Powerwall.

                      +

                      Mode: “self_consumption”, “backup”, “autonomous”

                      -
                      -async honk_horn() None
                      -

                      Horn.

                      +
                      +async set_reserve_percent(value: int) None
                      +

                      Set reserve percentage of Powerwall.

                      +

                      Value: 0-100

                      +
                      +
                      +property site_name: str
                      +

                      Return energy site name.

                      -
                      -
                      -class teslajsonpy.FlashLights(data, controller)
                      -

                      Home-Assistant class for flash lights of Tesla vehicles.

                      -

                      Inheritance

                      -digraph inheritance743c440a86 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "FlashLights" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.FlashLights",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-Assistant class for flash lights of Tesla vehicles."]; - "VehicleDevice" -> "FlashLights" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -} -
                      -
                      -async async_update(wake_if_asleep=False, force=False)
                      -

                      Update the flash lights of the vehicle.

                      -
                      - -
                      -
                      -async flash_lights() None
                      -

                      Flash Lights.

                      +
                      +
                      +property solar_power: float
                      +

                      Return solar power in Watts.

                      -
                      -
                      -static has_battery() bool
                      -

                      Return whether the device has a battery.

                      +
                      +
                      +property version: float
                      +

                      Return firmware version.

                      - -

                      Home-Assistant class for trigger homelink of Tesla vehicles.

                      +
                      +class teslajsonpy.TeslaProxy(proxy_url: URL, host_url: URL)
                      +

                      Class to handle proxy login connections to Alexa.

                      Inheritance

                      -digraph inheritance94bdcd919d { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "TriggerHomelink" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.TriggerHomelink",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-Assistant class for trigger homelink of Tesla vehicles."]; - "VehicleDevice" -> "TriggerHomelink" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -} +
                      +

                      Inheritance diagram of TeslaProxy

                      -
                      -async async_update(wake_if_asleep=False, force=False)
                      -

                      Update the trigger homelink of the vehicle.

                      +
                      +async modify_headers(site: URL, request: Request) MultiDict
                      +

                      Modify headers.

                      +

                      Return modified headers based on site and request. To disable auto header generation, +pass in a key const.SKIP_AUTO_HEADERS with a list of keys to not generate.

                      +

                      For example, to prevent User-Agent generation: {SKIP_AUTO_HEADERS : [“User-Agent”]}

                      +
                      +
                      Parameters
                      +
                        +
                      • site (URL) – URL of the next host request.

                      • +
                      • request (web.Request) – Proxy directed request. This will need to be changed for the actual host request.

                      • +
                      +
                      +
                      +
                      +
                      Returns

                      dict: Headers after modifications

                      +
                      +
                      -
                      -available() bool
                      -

                      Return whether homelink is available.

                      +
                      +async static prepend_i18n_path(base_url: URL, html: str) str
                      +

                      Prepend path for i18n loadPath so it’ll reach the proxy.

                      +

                      This is intended to be used for to place the proxy_url path in front of relative urls for loadPath in i18next.

                      +
                      +
                      Parameters
                      +
                        +
                      • base_url (URL) – Base URL to prepend

                      • +
                      • html (str) – text to replace

                      • +
                      +
                      +
                      +
                      +
                      Returns

                      str: Replaced text

                      +
                      +
                      -
                      -static has_battery() bool
                      -

                      Return whether the device has a battery.

                      +
                      +async static prepend_relative_urls(base_url: URL, html: str) str
                      +

                      Prepend relative urls with url host.

                      +

                      This is intended to be used for to place the proxy_url in front of relative urls in src=”/

                      +
                      +
                      Parameters
                      +
                        +
                      • base_url (URL) – Base URL to prepend

                      • +
                      • html (str) – text to replace

                      • +
                      +
                      +
                      +
                      +
                      Returns

                      str: Replaced text

                      +
                      +
                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +async reset_data() None
                      +

                      Reset all stored data.

                      +

                      A proxy may need to service multiple login requests if the route is not torn down. This function will reset all data between logins.

                      - -

                      Trigger Homelink.

                      +
                      +async test_url(resp: Response, data: Dict[str, Any], query: Dict[str, Any])
                      +

                      Test for a successful Tesla URL.

                      +

                      https://tesla-api.timdorr.com/api-basics/authentication#step-2-obtain-an-authorization-code

                      +
                      +
                      Parameters
                      +
                        +
                      • resp (httpx.Response) – The httpx response.

                      • +
                      • data (Dict[str, Any]) – Dictionary of all post data captured through proxy with overwrites for duplicate keys.

                      • +
                      • query (Dict[str, Any]) – Dictionary of all query data with overwrites for duplicate keys.

                      • +
                      +
                      +
                      +
                      +
                      Returns

                      Optional[Union[URL, str]]: URL for a http 302 redirect or str to display on success. None indicates test did not pass.

                      +
                      +
                      -
                      -class teslajsonpy.TrunkLock(data, controller)
                      -

                      Home-Assistant rear trunk lock for a Tesla VehicleDevice.

                      +
                      +class teslajsonpy.SolarPowerwallSite(api: Callable, energysite: dict, site_config: dict, battery_data: dict, battery_summary: dict)
                      +

                      Represents a Tesla Energy Solar site with Powerwall(s).

                      +

                      This class shouldn’t be instantiated directly; it will be instantiated +by teslajsonpy.controller.generate_energysite_objects().

                      Inheritance

                      -digraph inheritanceb343d48f3f { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "TrunkLock" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.TrunkLock",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-Assistant rear trunk lock for a Tesla VehicleDevice."]; - "VehicleDevice" -> "TrunkLock" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -} -
                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the rear trunk state.

                      -
                      - -
                      -
                      -static has_battery()
                      -

                      Return whether the device has a battery.

                      +
                      +

                      Inheritance diagram of SolarPowerwallSite

                      +
                      +
                      +property export_rule: str
                      +

                      Return energy export rule setting.

                      -
                      -
                      -is_locked()
                      -

                      Return whether the rear trunk is closed.

                      +
                      +
                      +property grid_charging: bool
                      +

                      Return grid charging.

                      -
                      -async lock()
                      -

                      Close the rear trunk.

                      +
                      +async set_export_rule(setting: str) None
                      +

                      Set energy export setting of Powerwall.

                      +
                      +
                      Settings

                      Solar: “pv_only” +Everything: “battery_ok”

                      +
                      +
                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +async set_grid_charging(value: bool) None
                      +

                      Set grid charging setting of Powerwall.

                      -
                      -
                      -async unlock()
                      -

                      Open the rear trunk.

                      +
                      +
                      +property solar_type: str
                      +

                      Return type of solar (e.g. pv_panels or roof).

                      -
                      -class teslajsonpy.FrunkLock(data, controller)
                      -

                      Home-Assistant front trunk (frunk) lock for a Tesla VehicleDevice.

                      +
                      +class teslajsonpy.SolarSite(api: Callable, energysite: dict, site_config: dict, site_data: dict)
                      +

                      Represents a Tesla Energy Solar site.

                      +

                      This class shouldn’t be instantiated directly; it will be instantiated +by teslajsonpy.controller.generate_energysite_objects().

                      Inheritance

                      -digraph inheritancea7a0fc7c8a { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "FrunkLock" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.FrunkLock",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Home-Assistant front trunk (frunk) lock for a Tesla VehicleDevice."]; - "VehicleDevice" -> "FrunkLock" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "VehicleDevice" [fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",tooltip="Home-assistant class of Tesla vehicles."]; -} -
                      -
                      -async async_update(wake_if_asleep=False, force=False) None
                      -

                      Update the front trunk (frunk) state.

                      +
                      +

                      Inheritance diagram of SolarSite

                      +
                      +
                      +property data_available: bool
                      +

                      Return if data is available.

                      -
                      -
                      -static has_battery()
                      -

                      Return whether the device has a battery.

                      +
                      +
                      +property grid_power: float
                      +

                      Return grid power in Watts.

                      -
                      -
                      -is_locked()
                      -

                      Return whether the front trunk (frunk) is closed.

                      +
                      +
                      +property load_power: float
                      +

                      Return load power in Watts.

                      -
                      -
                      -async lock()
                      -

                      Close the front trunk (frunk).

                      +
                      +
                      +property site_name: str
                      +

                      Return energy site name.

                      -
                      -
                      -refresh() None
                      -

                      Refresh data.

                      -

                      This assumes the controller has already been updated

                      +
                      +
                      +property solar_power: float
                      +

                      Return solar power in Watts.

                      -
                      -
                      -async unlock()
                      -

                      Open the front trunk (frunk).

                      +
                      +
                      +property solar_type: str
                      +

                      Return type of solar (e.g. pv_panels or roof).

                      @@ -2111,8 +1621,6 @@

                      Exceptions
                    • UnknownPresetMode: Class of exceptions for Unknown Preset.

                    • -
                    • HomelinkError: -Class of exceptions for Homelink Error.

                    • RetryLimitError: Class of exceptions for hitting retry limits.

                    • IncompleteCredentials: @@ -2123,12 +1631,8 @@

                      Exceptionsexception teslajsonpy.TeslaException(code: str, *args, **kwargs)

                      Class of Tesla API exceptions.

                      Inheritance

                      -digraph inheritancea58a391388 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "TeslaException" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.TeslaException",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Class of Tesla API exceptions."]; -} +
                      +

                      Inheritance diagram of TeslaException

                    • @@ -2136,29 +1640,8 @@

                      Exceptionsexception teslajsonpy.UnknownPresetMode(code: str, *args, **kwargs)

                      Class of exceptions for Unknown Preset.

                      Inheritance

                      -digraph inheritancec2ed502f44 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "TeslaException" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.TeslaException",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Class of Tesla API exceptions."]; - "UnknownPresetMode" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.UnknownPresetMode",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Class of exceptions for Unknown Preset."]; - "TeslaException" -> "UnknownPresetMode" [arrowsize=0.5,style="setlinewidth(0.5)"]; -} -

                      - -
                      -
                      -exception teslajsonpy.HomelinkError(code: str, *args, **kwargs)
                      -

                      Class of exceptions for Homelink Error.

                      -

                      Inheritance

                      -digraph inheritance3e8a7e118c { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "HomelinkError" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.HomelinkError",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Class of exceptions for Homelink Error."]; - "TeslaException" -> "HomelinkError" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "TeslaException" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.TeslaException",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Class of Tesla API exceptions."]; -} +
                      +

                      Inheritance diagram of UnknownPresetMode

                      @@ -2166,14 +1649,8 @@

                      Exceptionsexception teslajsonpy.RetryLimitError(code: str, *args, **kwargs)

                      Class of exceptions for hitting retry limits.

                      Inheritance

                      -digraph inheritancefae8dcd8d6 { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "RetryLimitError" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.RetryLimitError",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Class of exceptions for hitting retry limits."]; - "TeslaException" -> "RetryLimitError" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "TeslaException" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.TeslaException",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Class of Tesla API exceptions."]; -} +
                      +

                      Inheritance diagram of RetryLimitError

                      @@ -2181,14 +1658,8 @@

                      Exceptionsexception teslajsonpy.IncompleteCredentials(code: str, *args, devices: Optional[Dict[Any, Any]] = None, **kwargs)

                      Class of exceptions for incomplete credentials.

                      Inheritance

                      -digraph inheritance49d735891d { -bgcolor=transparent; -rankdir=LR; -size="8.0, 12.0"; - "IncompleteCredentials" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.IncompleteCredentials",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Class of exceptions for incomplete credentials."]; - "TeslaException" -> "IncompleteCredentials" [arrowsize=0.5,style="setlinewidth(0.5)"]; - "TeslaException" [URL="../teslajsonpy/teslajsonpy.html#teslajsonpy.TeslaException",fillcolor=white,fontname="Vera Sans, DejaVu Sans, Liberation Sans, Arial, Helvetica, sans",fontsize=10,height=0.25,shape=box,style="setlinewidth(0.5),filled",target="_top",tooltip="Class of Tesla API exceptions."]; -} +
                      +

                      Inheritance diagram of IncompleteCredentials

                      @@ -2209,7 +1680,7 @@

                      Variables
                      '2.4.0'
                      +
                      '2.4.5'
                       

                      diff --git a/docs/html/teslajsonpy/teslajsonpy.teslaproxy.html b/docs/html/teslajsonpy/teslajsonpy.teslaproxy.html index dd7ae285..e43897c2 100644 --- a/docs/html/teslajsonpy/teslajsonpy.teslaproxy.html +++ b/docs/html/teslajsonpy/teslajsonpy.teslaproxy.html @@ -20,7 +20,7 @@ - + @@ -42,11 +42,12 @@
                    • teslajsonpy
                      • Submodules
                      • @@ -95,7 +96,7 @@

                        diff --git a/docs/requirements.txt b/docs/requirements.txt index 297b9602..e21ecb8a 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,87 +1,86 @@ -aiohttp==3.8.1; python_version >= "3.6" +aiohttp==3.8.3; python_version >= "3.6" aiosignal==1.2.0; python_version >= "3.7" and python_version < "4.0" -alabaster==0.7.12; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +alabaster==0.7.12; python_version >= "3.7" anyio==3.6.1; python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.6.2" -astroid==2.11.7; python_full_version >= "3.6.2" and python_version >= "3.6" +astroid==2.11.7; python_full_version >= "3.6.2" and python_version >= "3.7" async-timeout==4.0.2; python_version >= "3.7" and python_version < "4.0" asynctest==0.13.0; python_version < "3.8" and python_version >= "3.7" -atomicwrites==1.4.1; python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.4.0" -attrs==21.4.0; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4.0" or python_full_version >= "3.5.0" and python_version >= "3.7" and python_version < "4.0" +attrs==22.1.0; python_version >= "3.7" and python_version < "4.0" authcaptureproxy==1.1.4; python_version >= "3.7" and python_version < "4.0" autoapi==2.0.1 -babel==2.10.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -backoff==2.1.2; python_version >= "3.7" and python_version < "4.0" +babel==2.10.3; python_version >= "3.7" +backoff==2.2.1; python_version >= "3.7" and python_version < "4.0" beautifulsoup4==4.11.1; python_full_version >= "3.6.0" -black==22.6.0; python_full_version >= "3.6.2" -certifi==2022.6.15; python_version >= "3.7" and python_version < "4.0" -charset-normalizer==2.1.0; python_full_version >= "3.6.0" and python_version >= "3.7" and python_version < "4.0" -click==8.1.3; python_version >= "3.7" and python_full_version >= "3.6.2" and python_version < "4.0" -colorama==0.4.5; sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.6.2" and platform_system == "Windows" and python_version < "4.0" and (python_version >= "3.6" and python_full_version < "3.0.0" and sys_platform == "win32" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") or sys_platform == "win32" and python_version >= "3.6" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") and python_full_version >= "3.5.0") and (python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0") -coverage==6.4.1; python_version >= "3.7" +black==22.10.0; python_version >= "3.7" +certifi==2022.9.24; python_version >= "3.7" and python_version < "4.0" +charset-normalizer==2.1.1; python_full_version >= "3.6.0" and python_version >= "3.7" and python_version < "4.0" +click==8.1.3; python_version >= "3.7" and python_version < "4.0" +colorama==0.4.5; sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.6.2" and platform_system == "Windows" and (python_version >= "3.7" and python_full_version < "3.0.0" and platform_system == "Windows" and python_version < "4.0" or platform_system == "Windows" and python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.5.0") and (python_version >= "3.7" and python_full_version < "3.0.0" and sys_platform == "win32" or sys_platform == "win32" and python_version >= "3.7" and python_full_version >= "3.5.0") +coverage==6.5.0; python_version >= "3.7" dill==0.3.5.1; python_full_version >= "3.7.0" -distlib==0.3.4; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -docutils==0.17.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" -filelock==3.7.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" +distlib==0.3.6; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" +docutils==0.19; python_version >= "3.7" +filelock==3.8.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" flake8==3.9.2; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") -frozenlist==1.3.0; python_version >= "3.7" and python_version < "4.0" +frozenlist==1.3.1; python_version >= "3.7" and python_version < "4.0" h11==0.12.0; python_version >= "3.7" and python_version < "4.0" httpcore==0.15.0; python_version >= "3.7" and python_version < "4.0" httpx==0.23.0; python_version >= "3.7" -idna==3.3; python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.6.2" -imagesize==1.4.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -importlib-metadata==4.12.0; python_version >= "3.7" and python_version < "3.8" and (python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.5.0" and python_version < "3.8" and python_version >= "3.7") and python_full_version >= "3.6.2" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") +idna==3.4; python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.6.2" +imagesize==1.4.1; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" +importlib-metadata==5.0.0; python_version >= "3.7" and python_version < "3.8" and (python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.5.0" and python_version < "3.8" and python_version >= "3.7") and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6") iniconfig==1.1.1; python_version >= "3.7" isort==5.10.1; python_full_version >= "3.6.2" and python_version < "4.0" -jinja2==3.1.2; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.7" -lazy-object-proxy==1.7.1; python_full_version >= "3.6.2" and python_version >= "3.6" -m2r2==0.3.2 +jinja2==3.1.2; python_version >= "3.7" +lazy-object-proxy==1.7.1; python_full_version >= "3.6.2" and python_version >= "3.7" +m2r2==0.3.3 markupsafe==2.1.1; python_version >= "3.7" mccabe==0.6.1; python_full_version >= "3.6.2" mistune==0.8.4 multidict==6.0.2; python_version >= "3.7" and python_version < "4.0" -mypy-extensions==0.4.3; python_version >= "3.6" and python_full_version >= "3.6.2" -mypy==0.961; python_version >= "3.6" +mypy-extensions==0.4.3; python_version >= "3.7" +mypy==0.982; python_version >= "3.7" packaging==21.3; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" -pathspec==0.9.0; python_full_version >= "3.6.2" +pathspec==0.10.1; python_version >= "3.7" platformdirs==2.5.2; python_version >= "3.7" and python_full_version >= "3.6.2" and (python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7") pluggy==1.0.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" py==1.11.0; python_version >= "3.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.7" pycodestyle==2.7.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" pydocstyle==6.1.1; python_version >= "3.6" pyflakes==2.3.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -pygments==2.12.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +pygments==2.13.0; python_version >= "3.7" pylint==2.13.9; python_full_version >= "3.6.2" pyparsing==3.0.9; python_full_version >= "3.6.8" and python_version >= "3.7" -pytest-asyncio==0.18.3; python_version >= "3.7" -pytest-cov==3.0.0; python_version >= "3.6" -pytest==7.1.2; python_version >= "3.7" -pytz==2022.1; python_version >= "3.6" -pyyaml==6.0; python_version >= "3.6" -requests==2.28.1; python_version >= "3.7" and python_version < "4" and (python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6") +pytest-asyncio==0.19.0; python_version >= "3.7" +pytest-cov==4.0.0; python_version >= "3.6" +pytest==7.1.3; python_version >= "3.7" +pytz==2022.4; python_version >= "3.6" +pyyaml==6.0; python_version >= "3.7" +requests==2.28.1; python_version >= "3.7" and python_version < "4" rfc3986==1.5.0; python_version >= "3.7" and python_version < "4.0" +setuptools==65.4.1; python_full_version >= "3.6.2" and python_version >= "3.7" six==1.16.0; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -sniffio==1.2.0; python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.6.2" -snowballstemmer==2.2.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" +sniffio==1.3.0; python_version >= "3.7" and python_version < "4.0" and python_full_version >= "3.6.2" +snowballstemmer==2.2.0; python_version >= "3.7" soupsieve==2.3.2.post1; python_version >= "3.7" and python_full_version >= "3.6.0" and python_version < "4.0" -sphinx-autoapi==1.8.4; python_version >= "3.6" +sphinx-autoapi==2.0.0; python_version >= "3.7" sphinx-copybutton==0.5.0; python_version >= "3.6" -sphinx-rtd-theme==1.0.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.4.0") -sphinx==5.0.2; python_version >= "3.6" -sphinxcontrib-applehelp==1.0.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -sphinxcontrib-devhelp==1.0.2; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -sphinxcontrib-htmlhelp==2.0.0; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -sphinxcontrib-jsmath==1.0.1; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -sphinxcontrib-qthelp==1.0.3; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -sphinxcontrib-serializinghtml==1.1.5; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.4.0" and python_version >= "3.6" -toml==0.10.2; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" -tomli==2.0.1; python_version < "3.11" and python_version >= "3.7" and python_full_version <= "3.11.0a6" and python_full_version >= "3.6.2" -tox==3.25.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") -typed-ast==1.5.4; python_version < "3.8" and python_version >= "3.6" and implementation_name == "cpython" and python_full_version >= "3.6.2" -typer==0.5.0; python_version >= "3.7" and python_version < "4.0" -typing-extensions==4.3.0; python_version < "3.8" and python_version >= "3.7" and python_full_version >= "3.6.2" and (python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.5.0" and python_version < "3.8" and python_version >= "3.7") -unidecode==1.3.4; python_version >= "3.6" -urllib3==1.26.10; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.7" -virtualenv==20.15.1; python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" +sphinx-rtd-theme==0.5.1 +sphinx==5.2.3; python_version >= "3.6" +sphinxcontrib-applehelp==1.0.2; python_version >= "3.7" +sphinxcontrib-devhelp==1.0.2; python_version >= "3.7" +sphinxcontrib-htmlhelp==2.0.0; python_version >= "3.7" +sphinxcontrib-jsmath==1.0.1; python_version >= "3.7" +sphinxcontrib-qthelp==1.0.3; python_version >= "3.7" +sphinxcontrib-serializinghtml==1.1.5; python_version >= "3.7" +tomli==2.0.1; python_version < "3.11" and python_version >= "3.7" and python_full_version <= "3.11.0a6" and (python_version >= "2.7" and python_full_version < "3.0.0" or python_full_version >= "3.5.0") and python_full_version >= "3.6.2" +tox==3.26.0; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") +typed-ast==1.5.4; python_version < "3.8" and python_version >= "3.7" and implementation_name == "cpython" and python_full_version >= "3.6.2" +typer==0.6.1; python_version >= "3.7" and python_version < "4.0" +typing-extensions==4.4.0; python_version < "3.8" and python_version >= "3.7" and python_full_version >= "3.6.2" and (python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.5.0" and python_version < "3.8" and python_version >= "3.7") +unidecode==1.3.6; python_version >= "3.7" +urllib3==1.26.12; python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "4" or python_full_version >= "3.6.0" and python_version < "4" and python_version >= "3.7" +virtualenv==20.16.5; python_version >= "3.6" and python_full_version < "3.0.0" or python_full_version >= "3.5.0" and python_version >= "3.6" wrapt==1.14.1; (python_version >= "2.7" and python_full_version < "3.0.0") or (python_full_version >= "3.5.0") -yarl==1.7.2; python_version >= "3.7" and python_version < "4.0" -zipp==3.8.0; python_version >= "3.7" and python_version < "3.8" and (python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.5.0" and python_version < "3.8" and python_version >= "3.7") +yarl==1.8.1; python_version >= "3.7" and python_version < "4.0" +zipp==3.9.0; python_version >= "3.7" and python_version < "3.8" and (python_version >= "3.7" and python_full_version < "3.0.0" and python_version < "3.8" or python_full_version >= "3.5.0" and python_version < "3.8" and python_version >= "3.7") diff --git a/docs/teslajsonpy/teslajsonpy.car.rst b/docs/teslajsonpy/teslajsonpy.car.rst index f7044be2..d33380f2 100644 --- a/docs/teslajsonpy/teslajsonpy.car.rst +++ b/docs/teslajsonpy/teslajsonpy.car.rst @@ -1,6 +1,6 @@ -========================== +=================== ``teslajsonpy.car`` -========================== +=================== .. automodule:: teslajsonpy.car diff --git a/docs/teslajsonpy/teslajsonpy.energy.rst b/docs/teslajsonpy/teslajsonpy.energy.rst index 39d5ca6e..969a71df 100644 --- a/docs/teslajsonpy/teslajsonpy.energy.rst +++ b/docs/teslajsonpy/teslajsonpy.energy.rst @@ -1,6 +1,6 @@ -========================== +====================== ``teslajsonpy.energy`` -========================== +====================== .. automodule:: teslajsonpy.energy diff --git a/docs/teslajsonpy/teslajsonpy.rst b/docs/teslajsonpy/teslajsonpy.rst index 543b2e4c..e997ba14 100644 --- a/docs/teslajsonpy/teslajsonpy.rst +++ b/docs/teslajsonpy/teslajsonpy.rst @@ -29,7 +29,7 @@ Classes ======= - :py:class:`TeslaCar`: - Class to handle car attributes and methods. + Represents a Tesla car. - :py:class:`Connection`: Connection to Tesla Motors API. @@ -37,12 +37,21 @@ Classes - :py:class:`Controller`: Controller for connections to Tesla Motors API. -- :py:class:`Energy`: - Class to handle energy site attributes and methods. +- :py:class:`EnergySite`: + Base class to represents a Tesla energy site. + +- :py:class:`PowerwallSite`: + Represents a Tesla Energy Powerwall site. - :py:class:`TeslaProxy`: Class to handle proxy login connections to Alexa. +- :py:class:`SolarPowerwallSite`: + Represents a Tesla Energy Solar site with Powerwall(s). + +- :py:class:`SolarSite`: + Represents a Tesla Energy Solar site. + .. autoclass:: TeslaCar :members: @@ -65,11 +74,18 @@ Classes .. inheritance-diagram:: Controller :parts: 1 -.. autoclass:: Energy +.. autoclass:: EnergySite + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: EnergySite + :parts: 1 + +.. autoclass:: PowerwallSite :members: .. rubric:: Inheritance - .. inheritance-diagram:: Energy + .. inheritance-diagram:: PowerwallSite :parts: 1 .. autoclass:: TeslaProxy @@ -79,6 +95,20 @@ Classes .. inheritance-diagram:: TeslaProxy :parts: 1 +.. autoclass:: SolarPowerwallSite + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: SolarPowerwallSite + :parts: 1 + +.. autoclass:: SolarSite + :members: + + .. rubric:: Inheritance + .. inheritance-diagram:: SolarSite + :parts: 1 + Exceptions ========== @@ -89,9 +119,6 @@ Exceptions - :py:exc:`UnknownPresetMode`: Class of exceptions for Unknown Preset. -- :py:exc:`HomelinkError`: - Class of exceptions for Homelink Error. - - :py:exc:`RetryLimitError`: Class of exceptions for hitting retry limits. @@ -111,12 +138,6 @@ Exceptions .. inheritance-diagram:: UnknownPresetMode :parts: 1 -.. autoexception:: HomelinkError - - .. rubric:: Inheritance - .. inheritance-diagram:: HomelinkError - :parts: 1 - .. autoexception:: RetryLimitError .. rubric:: Inheritance @@ -140,4 +161,4 @@ Variables .. code-block:: text - '2.4.0' + '2.4.5'
        • websocket_connect() (teslajsonpy.Connection method)
        • diff --git a/docs/html/index.html b/docs/html/index.html index c6b07030..08af99f8 100644 --- a/docs/html/index.html +++ b/docs/html/index.html @@ -42,11 +42,12 @@
        • teslajsonpy
          • Submodules
          • @@ -139,11 +140,12 @@

            API Referenceteslajsonpy
            • Submodules
            • diff --git a/docs/html/objects.inv b/docs/html/objects.inv index a6bc9ebf83b83dd25fde67141881ea9f4f9d1399..e1b2e216fdf6edbaba96a865fdac0e8e80ddbfcb 100644 GIT binary patch delta 1974 zcmV;n2TAy_6Pgf^dVibCj_WoOhWCC74KmwylHDNNgR=;b0F9gsW=YUeSz?+J6_Rqd zv&w7C>&=r)k&-OgmaqJKWkZ*b#aq>16zR!n`G+%2yU*JFPqisJP1z5bpRRvD`@gPy zt-tG^diiJevYOxfVw#3>uS^qAxt7jlKVBzYMyzvf97UVRSAV8RAeH1%93eY~SoN0- zS43_K`b4zQs(FL)HzX}t;msR0FsO4T8Vn*(!u7Yhbn3g>pz-Nex-)N#xQBv-yL$4hs&y<=!d&i{({7$ zzp1{yPbDN1Vm;H+y;09rcE+rv8#OPGsEsTHH(8&EfuPQ4iQ>W9kK>!^Y|hSS-%<+m z=O#CGtADx8zE~=#@k;7AkLe?)HmdvwiLd6J>^n!ur15t|S%+ZD{*Y_UGmZq;G-p#& z>V3-Z)2}vM#bJZ`5^^V%mK#ktZa*W1rY*1yO3U6B+yIg3y!4&JLE|h38yXnBLaGPx zh2ah(RhFDCRnnGJ;MgHhp4 zi>j6XYOS$JPW4-;BgV6yqt^7%8p~bYr8%hua&!Q31^ zM{H{7j^Om60FG1>6BAG-9>(uaJcuir;V^FK6qq_neh>e@$mc&zjDUkk(zGLuFzG`#B!1A!vMAED*C{tPFeiukr$EuIFaxUw39gkU*SD@v1xfu`@bFm= z+JR9x6vkMQxJ%(Om!p?u-uk z9*)WCGv?4BB)fvJdih4gpyYIS;+m!zgHN6+A`euYE}f9HwI;+FRLu+JS}pg4+D~_*am4C9w z`T`jzdR`YRRODZwMv%O)q$HOgZKLBK}&E{16Up>pL5)TpmaO<@n7C}^kh zux+6O4X6XTA!#?Dnj7X-#v*is!c530>jXYm<3ueiF>;@PaE>jFUK7J};-a8~z=uF= zV8pHMc<@9uTcDWSNhL{(!s)Pq0)J*4uIZd7vO#FQS44VrzBs-|cMazO%6A|I6M6;U zZ?tJjoKc(s_sS19r8)OSBOC6A9SLN>8j#Q)9+@WZzZSW!4PFg27!P!0{|``@P&0)| z-Wo8$I**Mut#mAO%g>PgxD+>%s9?3b^`b;&q?Y5$T=O~*wKg1G=URHUcz>Db>0Dk> zI6fns+4^^BNu+Ksn$Vo?u*`yGr!yWnk?uDDURiHXZV7+*{3c}GIuvtx1q48j@0*I)3kMk&;Rb@~%<9Z1I z*)^b?<#@Yoa!2eyU)P2m;E5Iddf0B1viB&WUak4qr|lt#h&6`?wcW>rX^?se73`6% ze2MPUxvEL%fmA2b(NKF!_N%ZJCW_w@AG9mgpismC(z zV(C|R96wU-Dd=K}*JQ@S9(CeeEbm67dZfs6)N!fte>=*an`32q{OVYlJ+`E)9&WS# z{g+iqjcN+|$eb^J(sDyOu*R$L zy3mI6T3>7C`OVcGvgID`smjSN++1CeHTRS=s-NuS;_9{xxeI?RTC(MPtE=+i&T=P_vgLcL+wSY0!cLrBwtR2( If7^>e^({U0CIA2c delta 2383 zcmV-V39$B>5U>-FdVieFlH)WGfbV_^Q*fQY-YAZ2v9MLUwNpExIJz{pCKhodBgvTw zCtics<4KS#|Hw{k>!)+t*;x9z_1Efd$(C4D=ug3DeM#%fPbXG16;6Lh<@o#e*5A8Q zo~mz6sfX{6s_j0##~-Pc?T@BGpZVZzAKT)p2ty*gK_-Au_(hq@AU{` z{+>~ab4eI|!c;IGJl__0K=W^j4lq1RXs8~3dL=S1WBb)mV0*J;g3QY)k_Z3`F-5`- z$4*GZrBM>YF@ISNgH-(nu^b1?4iyD1GLEQGEz~N`sc^G#gqjY_@`iIvWybMe4Hgnm z>)Qgd6MBOef+^;QOV)NjJiQ_(A~V5qa5T?QE*n(!y>tfwHG9SU4%{=VBm$l0y(v@c zM?!L;e3cm%&Yq7<80$Ool=+qLEtlk^76pEZo33hAb${a=W_eA0DC5}Cw4>8oEQwDw z4DkigObM=qj)ef!@D)ERZ}F)267ypeRdQBU zn8&P{4zWWufly_1)mjGw2FXq*L<=AmSg-F2A|P?Gw#MRQIpP_k$_8LKWIwPq;^-vc zxMnW_$1?gtz?CgHIfJWeLR3K{24ud~grj~#1%E*v8Uo$&lAW+JTSVCks)y`UGphV+ zI04EiO0D!U(~$`*)&v%J0KcLmR&-c0Yq#2lS75|8eE>ZpOjrsGZMSbYTsQE9s?g}H z!mTr*z-%~O&$uLc1q9SdW!Q~H6Fd74WT_O9(CnZbRw$fM7KvZl=nGKEc1~uUpNV0d zn}2Bcw+C^2P&QeuZp&RoRrQSWBM4u>heONgf}If13Kj6KmAI2@CGO)|iMzN~;vTM* zfcIH6k`!PDU);w-5%=;?#N9j;agVKE+;OiKzY+H)90vRWTKL; zBem~C#YXnYWwg3#MAV*bjZrxDgIYu7Cx3D%O`sE$xv>~P=NvZiMgN^ugNpG)ki&!z zyrIWW%s3%o-PRMJI&I*=Hc)$rm3F)e2w25p{I-!3eW)>_8XBk6ua8f`lR(u48h%Y` z!*-TO=2|ofWD03e^f>*-PFNox{KC1p0fXOiT(>RsCh_ni9bFk{q*@e62OAQj$A7Hs z+7`z7NOiue!VnO_zXcXb>A(SZuu!1lK5XAO5>+kil)w)ivc-&kQdi&pxWzC=xw+q z9S?*Ygf>ajT~daD>2uEl@d(f6E0+B7N#8<__tl@;gO1Ovh@EtE5YWuK#-0(W^nF4= zWRbctur9GA6LS;zh5>JwDZu4#0>2nwl06YGbpIdVhB#RFDEk zUhSPRStDVgAT!Y70n8Qk_b-D%jIoX_>XEazOT@J65j(Z?$;%}xF)A<@kqtD+n&TSN zA|vm9q*;w*2}`AC z<~K*eEZ-S4ehtrn()_=@JbxB<%XuGtp0Ec#PuRnrC+s256ZSyo346Hnggw}K!teA* z*(-f)jne1NFALr)es8Biq?2MpfTq_3**Pla*j>#uPM#YX_x8(#4-k`2c+s;Z%g$=A z)mMTtr;kHeO z`v{zI7UgYh6+R-bQB$dt`|)lcw6SZf&d+H{q?Uyg0U}IAZ%MZ=!T9u@4RDY*J&5*O zcATzC=s%z%hW*~N4}YfskG`B$y!v91Pc!?<9`a^q3SO(bt-30UP!!dvc72~doz&a> zNRQVJv;cEg=k}?gj}8-pyWoX`Hy_DUguCe5&a{`AwFzs2yDwM-%(kwk$2{w6EPSLW zU|w|yXQ7efIb*npy?LVLZ8lumIetRIcOd((AGi->SL5ol6@Olx*;RHCbSnYw=jaUe z*Tfy_5Pv|`^5{EVdUBP|ryaWD7X13IjF++OYp;nZu^N;@St}IeK-twjdakb~G3NT! zk=GJ;U+)VOKjHZ3VQ1kyMGtvLZ)wZ9TCasLgRDs&(o24AtlC_jbNkxj(~P7~gW8&F z{Na;Np3P|Md4IHHe!%G`&8i(HbLA-8r#r7GYvQHOMFupst>@89Q~t(Pc(34TsJ<;~ zA17+>B&vI_BB1}enjSr9zatVZu&ur-wf(#9PYgGujKv+PH;*K}Gajb?Lvp~B&;q}u zLKZ(&=vnP1iq-$xR!%?aKa;Uo!E#;5D)9%da@AX@WPi3A|NZl?NLAtHq*hdWQ~sMo zpMYM=t1l1z0|WeQy3I(oo^DioyvUN#X>GN|aYe=tH~JYu#)= zOdRsdF5G!KI$d~}Y$u*w#Ld#w>FsC9YQor2PM29uuYOLpv&W7i>|Aqt`&qJ`@O2S3 zOAn{FpMNDzw#I3?U1j{z#m9+HPS0Psl?zO_N+ltwF@KeAq}Ec`(n&y~DL})kJXPvg zIPpqN_-kh8G}6stiC26bpp6xnr<=tR=V;YmlRm^vSBoV6;a|XVp9hbrTiGNcI1JbF zI>t;rDteslajsonpy

            - teslajsonpy.connection -
            - teslajsonpy.const -
            - teslajsonpy.controller -
            - teslajsonpy.exceptions -
            - teslajsonpy.homeassistant -
            - teslajsonpy.homeassistant.alerts -
            - teslajsonpy.homeassistant.battery_sensor -
            - teslajsonpy.homeassistant.binary_sensor + teslajsonpy.car
            - teslajsonpy.homeassistant.charger -
            - teslajsonpy.homeassistant.climate -
            - teslajsonpy.homeassistant.gps -
            - teslajsonpy.homeassistant.heated_seats -
            - teslajsonpy.homeassistant.heated_steering_wheel -
            - teslajsonpy.homeassistant.homelink -
            - teslajsonpy.homeassistant.lock -
            - teslajsonpy.homeassistant.power + teslajsonpy.connection
            - teslajsonpy.homeassistant.sentry_mode + teslajsonpy.const
            - teslajsonpy.homeassistant.trunk + teslajsonpy.controller
            - teslajsonpy.homeassistant.vehicle + teslajsonpy.energy
            - teslajsonpy.homeassistant.vehicle_data + teslajsonpy.exceptions