├── .github └── workflows │ ├── jekyll-gh-pages.yml │ ├── pwsim-docker.yml │ ├── pylint.yml │ ├── simtest.yml │ └── test.yml ├── .gitignore ├── .pylintrc ├── API.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── RELEASE.md ├── _config.yml ├── api_test.py ├── dashboard └── README.md ├── docs ├── README.md ├── api.txt ├── flow.png ├── portal.png ├── vitals-example-failed-pw.json ├── vitals-example-latest.json └── vitals-example.json ├── example-cloud-mode.py ├── example.py ├── examples ├── network_route.py ├── vitals.py └── vitals │ ├── README.md │ ├── pull_vitals.py │ ├── tesla.proto │ └── tesla_pb2.py ├── proxy ├── API.md ├── Dockerfile ├── Dockerfile.beta ├── HELP.md ├── README.md ├── RELEASE.md ├── beta.txt ├── localhost.pem ├── requirements.txt ├── server.py ├── transform.py ├── upload-beta.sh └── web │ ├── LICENSE │ ├── bogus │ ├── api.auth.toggle.supported.json │ ├── api.customer.json │ ├── api.customer.registration.json │ ├── api.installer.json │ ├── api.meters.aggregates.json │ ├── api.meters.json │ ├── api.meters.readings.json │ ├── api.meters.site.json │ ├── api.meters.solar.json │ ├── api.networks.json │ ├── api.operation.json │ ├── api.powerwalls.json │ ├── api.site_info.grid_codes.json │ ├── api.site_info.json │ ├── api.site_info.site_name.json │ ├── api.sitemaster.json │ ├── api.solars.brands.json │ ├── api.solars.json │ ├── api.status.json │ ├── api.synchrometer.ct_voltage_references.json │ ├── api.system.networks.json │ ├── api.system.update.status.json │ ├── api.system_status.grid_faults.json │ ├── api.system_status.grid_status.json │ ├── api.system_status.grid_status.json-offline │ ├── api.system_status.grid_status.json-transition │ ├── api.system_status.json │ ├── api.system_status.soe.json │ └── api.troubleshooting.problems.json │ ├── box.png │ ├── example.html │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── index.html │ └── viz-static │ ├── 012955c70685614a5639d326f41890bd.png │ ├── 1.17c71172308436a079d1.js │ ├── 124f233cfa9945f861dcaca7acedd308.otf │ ├── 230aeae00823cd3b622d093948d9c433.png │ ├── 2bf15a1686c7a1bf7b577337a07d7049.otf │ ├── 39.17c71172308436a079d1.js │ ├── 40.17c71172308436a079d1.js │ ├── 448c34a56d699c29117adc64c43affeb.woff2 │ ├── 4e28cc8f2bdf3ba640331daa2c453341.png │ ├── 653969a51632a4df33358a39d7012f79.otf │ ├── 722c5f898bbca8b2eb3fce0287688326.otf │ ├── 86a6894da889a3db781418529403290f.otf │ ├── 89889688147bd7575d6327160d64e760.svg │ ├── 89aec2cc0b804667e95b1adc02e1ac4a.otf │ ├── a3b0d611359e6fa8356cd88aa9035268.otf │ ├── ac2944015a17576924af7c56d88751cb.otf │ ├── app.css │ ├── app.js │ ├── b8d72cb0ef934ba1fe847c692d9dfed1.otf │ ├── bceda3fae660177ae570735feec62811.otf │ ├── befdfda70624c396169873b05de57f8a.otf │ ├── black.js │ ├── cb0da8a8999c06735455bf5056a5cd78.png │ ├── clear.js │ ├── d859fee2eba0e67c75c4c92e719d0630.otf │ ├── dakboard.js │ ├── e19c20e966bde501f94e41cd0322dbe8.otf │ ├── ec6b35b07448e1624cb09323b5fb6e32.otf │ ├── ec89c09b066f57efc7687540c998845b.otf │ ├── eca1317ee8a99162d0d0e2df77330cec.otf │ ├── f4769f9bdb7466be65088239c12046d1.eot │ ├── grafana-dark.js │ ├── grafana.js │ ├── solar.js │ ├── vendor.js │ └── white.js ├── pwsimulator ├── Dockerfile ├── README.md ├── control.html ├── localhost.pem ├── stub.py ├── test.py └── test.sh ├── pypowerwall ├── __init__.py ├── __main__.py ├── api_lock.py ├── cloud │ ├── __init__.py │ ├── decorators.py │ ├── exceptions.py │ ├── mock_data.py │ ├── pypowerwall_cloud.py │ └── stubs.py ├── exceptions.py ├── fleetapi │ ├── __init__.py │ ├── __main__.py │ ├── decorators.py │ ├── exceptions.py │ ├── fleetapi.py │ ├── mock_data.py │ ├── pypowerwall_fleetapi.py │ └── stubs.py ├── local │ ├── __init__.py │ ├── exceptions.py │ ├── pypowerwall_local.py │ └── tesla_pb2.py ├── pypowerwall_base.py ├── regex.py ├── scan.py ├── tedapi │ ├── __init__.py │ ├── __main__.py │ ├── decorators.py │ ├── exceptions.py │ ├── mock_data.py │ ├── pypowerwall_tedapi.py │ ├── stubs.py │ ├── tedapi.proto │ └── tedapi_pb2.py └── tests │ ├── __init__.py │ └── tedapi │ ├── __init__.py │ └── test_init.py ├── pytest.ini ├── requirements.txt ├── setup.py ├── tesla.proto ├── test.py ├── test_requirements.txt ├── tools ├── README.md ├── cron.sh ├── fleetapi │ ├── README.md │ ├── create_pem_key.py │ ├── fleetapi.py │ ├── index.html │ ├── live.py │ ├── setup.py │ └── test.py ├── set-mode.py ├── set-reserve.py ├── tedapi │ ├── ComponentsQuery.py │ ├── PW3_Strings.py │ ├── PW3_Vitals.py │ ├── README.md │ ├── decode.py │ ├── status.py │ ├── tedapi.proto │ ├── tedapi_orig.py │ ├── tedapi_pb2.py │ ├── test_tedapi.py │ └── web.py └── tessolarcharge.py └── web └── index.html /.github/workflows/jekyll-gh-pages.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Jekyll site to GitHub Pages 2 | name: Deploy Jekyll with GitHub Pages dependencies preinstalled 3 | 4 | on: 5 | # Runs on pushes targeting the default branch 6 | push: 7 | branches: ["main"] 8 | 9 | # Allows you to run this workflow manually from the Actions tab 10 | workflow_dispatch: 11 | 12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 13 | permissions: 14 | contents: read 15 | pages: write 16 | id-token: write 17 | 18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 20 | concurrency: 21 | group: "pages" 22 | cancel-in-progress: false 23 | 24 | jobs: 25 | # Build job 26 | build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | - name: Setup Pages 32 | uses: actions/configure-pages@v5 33 | - name: Build with Jekyll 34 | uses: actions/jekyll-build-pages@v1 35 | with: 36 | source: ./ 37 | destination: ./_site 38 | - name: Upload artifact 39 | uses: actions/upload-pages-artifact@v3 40 | 41 | # Deployment job 42 | deploy: 43 | environment: 44 | name: github-pages 45 | url: ${{ steps.deployment.outputs.page_url }} 46 | runs-on: ubuntu-latest 47 | needs: build 48 | steps: 49 | - name: Deploy to GitHub Pages 50 | id: deployment 51 | uses: actions/deploy-pages@v4 52 | -------------------------------------------------------------------------------- /.github/workflows/pwsim-docker.yml: -------------------------------------------------------------------------------- 1 | name: pwsim-docker 2 | 3 | # Only trigger if a push is made to the pwsimulator folder 4 | on: 5 | push: 6 | paths: 7 | - 'pwsimulator/**' 8 | 9 | jobs: 10 | docker: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v4 15 | - name: Set up QEMU 16 | uses: docker/setup-qemu-action@v3 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v3 19 | - name: Login to Docker Hub 20 | uses: docker/login-action@v3 21 | with: 22 | username: ${{ secrets.DOCKERHUB_USERNAME }} 23 | password: ${{ secrets.DOCKERHUB_TOKEN }} 24 | - name: Build and push 25 | uses: docker/build-push-action@v5 26 | with: 27 | context: pwsimulator/ 28 | platforms: linux/amd64,linux/arm64,linux/arm/v7 29 | push: true 30 | tags: ${{ secrets.DOCKERHUB_USERNAME }}/pwsimulator:latest 31 | -------------------------------------------------------------------------------- /.github/workflows/pylint.yml: -------------------------------------------------------------------------------- 1 | name: Pylint 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | runs-on: ubuntu-latest 8 | strategy: 9 | matrix: 10 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 11 | steps: 12 | - uses: actions/checkout@v3 13 | - name: Set up Python ${{ matrix.python-version }} 14 | uses: actions/setup-python@v4 15 | with: 16 | python-version: ${{ matrix.python-version }} 17 | - name: Install dependencies 18 | run: | 19 | python -m pip install --upgrade pip 20 | pip install pylint 21 | pip install -r requirements.txt 22 | - name: Analyzing the code with pylint 23 | run: | 24 | pylint -E pypowerwall/*.py 25 | pylint -E proxy/*.py 26 | -------------------------------------------------------------------------------- /.github/workflows/simtest.yml: -------------------------------------------------------------------------------- 1 | name: simulator 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | tests: 10 | name: "Python ${{ matrix.python-version }}" 11 | runs-on: ubuntu-latest 12 | env: 13 | USING_COVERAGE: '3.8' 14 | 15 | strategy: 16 | matrix: 17 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 18 | 19 | services: 20 | simulator: 21 | image: ${{ github.repository_owner }}/pwsimulator 22 | ports: 23 | - 443:443 24 | 25 | steps: 26 | - uses: "actions/checkout@v2" 27 | - uses: "actions/setup-python@v2" 28 | with: 29 | python-version: "${{ matrix.python-version }}" 30 | - name: "Install dependencies" 31 | run: | 32 | set -xe 33 | python -VV 34 | python -m site 35 | python -m pip install --upgrade pip setuptools wheel 36 | pip install --upgrade requests protobuf teslapy 37 | 38 | - name: "Run test.py on ${{ matrix.python-version }}" 39 | run: "python example.py" 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | pull_request: 6 | workflow_dispatch: 7 | 8 | jobs: 9 | tests: 10 | name: "Python ${{ matrix.python-version }}" 11 | runs-on: ubuntu-latest 12 | env: 13 | USING_COVERAGE: '3.8' 14 | 15 | strategy: 16 | matrix: 17 | python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] 18 | 19 | steps: 20 | - uses: "actions/checkout@v2" 21 | - uses: "actions/setup-python@v2" 22 | with: 23 | python-version: "${{ matrix.python-version }}" 24 | - name: "Install dependencies" 25 | run: | 26 | set -xe 27 | python -VV 28 | python -m site 29 | python -m pip install --upgrade pip 30 | pip install --upgrade protobuf pytest requests setuptools teslapy wheel 31 | 32 | - name: "Run unit tests" 33 | run: "pytest" 34 | 35 | - name: "Run test.py on ${{ matrix.python-version }}" 36 | run: "python test.py" 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | 131 | # Local Test 132 | localtest.py 133 | upload.sh 134 | .powerwall 135 | .vscode/settings.json 136 | localtest.sh 137 | sandbox/ 138 | .DS_Store 139 | proxy/testproxy.sh 140 | proxy/uploadtest.sh 141 | tools/set-reserve.auth 142 | tools/set-reserve.conf 143 | tools/set-mode.auth 144 | tools/set-mode.conf 145 | tools/tedapi/request.bin 146 | tools/tedapi/app* 147 | .pypowerwall.auth 148 | .pypowerwall.site 149 | proxy/pypowerwall 150 | proxy/teslapy 151 | .fleetapi* 152 | .idea 153 | **/.cachefile 154 | .cachefile 155 | .pypowerwall.fleetapi 156 | .auth 157 | j 158 | config.json 159 | status.json 160 | tools/tedapi/firmware.raw 161 | tools/tedapi/components.json 162 | tools/tedapi/manypw3.json 163 | tools/tedapi/pw3.json 164 | tools/tedapi/test.py 165 | -------------------------------------------------------------------------------- /.pylintrc: -------------------------------------------------------------------------------- 1 | [MESSAGES CONTROL] 2 | disable=consider-iterating-dictionary, consider-swap-variables, consider-using-enumerate, cyclic-import, consider-using-max-builtin, no-else-continue, consider-using-min-builtin, consider-using-in, super-with-arguments, protected-access, import-outside-toplevel, multiple-statements, unidiomatic-typecheck, no-else-break, import-error, invalid-name, missing-docstring, no-else-return, no-member, too-many-lines, line-too-long, too-many-ancestors, too-many-arguments, too-many-branches, too-many-instance-attributes, too-many-locals, too-many-nested-blocks, too-many-return-statements, too-many-statements, too-few-public-methods, ungrouped-imports, use-dict-literal, superfluous-parens, fixme, consider-using-f-string, bare-except, broad-except, unused-variable, unspecified-encoding, redefined-builtin, consider-using-dict-items, redundant-u-string-prefix, useless-object-inheritance, wrong-import-position, logging-not-lazy, logging-fstring-interpolation, wildcard-import, logging-format-interpolation 3 | 4 | [SIMILARITIES] 5 | min-similarity-lines=8 6 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of this project, and in the interest of fostering an open and welcoming community, we pledge to respect all people who contribute through reporting issues, hosting discussions, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, disability, personal appearance, ethnicity, age, religion, or nationality. 6 | 7 | Examples of unacceptable behavior by participants include: 8 | - Personal attacks 9 | - Trolling or insulting/derogatory comments 10 | - Public or private harassment 11 | - Publishing others' private information, such as physical or electronic addresses, without explicit permission 12 | - Other unethical or unprofessional conduct 13 | 14 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 15 | 16 | By adopting this Code of Conduct, project maintainers commit themselves to fairly and consistently applying these principles to every aspect of managing this project. Project maintainers who do not follow or enforce the Code of Conduct may be permanently removed from the project team. 17 | 18 | This code of conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. 19 | 20 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer at the email address listed in the repository. 21 | 22 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 1.4. 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Jason Cox 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /api_test.py: -------------------------------------------------------------------------------- 1 | # Test Functions of the Powerwall API 2 | import os 3 | 4 | import pypowerwall 5 | 6 | # Optional: Turn on Debug Mode 7 | pypowerwall.set_debug(True) 8 | 9 | # Credentials for your Powerwall - Customer Login Data 10 | password = os.environ.get('PW_PASSWORD', 'password') 11 | email = os.environ.get('PW_EMAIL', 'email@example.com') 12 | host = os.environ.get('PW_HOST', 'localhost') # Change to the IP of your Powerwall 13 | timezone = os.environ.get('PW_TIMEZONE', 14 | 'America/Los_Angeles') # https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 15 | auth_path = os.environ.get('PW_AUTH_PATH', "") 16 | cachefile_path = os.environ.get('PW_CACHEFILE_PATH', ".cachefile") 17 | 18 | 19 | def test_battery_mode_change(pw): 20 | original_mode = pw.get_mode(force=True) 21 | if original_mode != 'backup': 22 | new_mode = 'backup' 23 | else: 24 | new_mode = 'self_consumption' 25 | 26 | resp = pw.set_mode(mode=new_mode) 27 | if resp and resp.get('set_operation', {}).get('result') == 'Updated': 28 | # if we got a valid response from API, let's assume it worked :) 29 | installed_mode = resp.get('set_operation', {}).get('real_mode') 30 | else: 31 | # TODO: may need to poll API until change is detected (in cloud mode) 32 | installed_mode = pw.get_mode(force=True) 33 | if installed_mode != new_mode: 34 | print(f"Set battery operation mode to {new_mode} failed.") 35 | # revert to original value just in case 36 | pw.set_mode(mode=original_mode) 37 | 38 | 39 | def test_battery_reserve_change(pw): 40 | original_reserve_level = pw.get_reserve(force=True) 41 | if original_reserve_level != 100: 42 | new_reserve_level = 100 43 | else: 44 | new_reserve_level = 50 45 | 46 | resp = pw.set_reserve(level=new_reserve_level) 47 | if resp and resp.get('set_backup_reserve_percent', {}).get('result') == 'Updated': 48 | # if we got a valid response from API, let's assume it worked :) 49 | installed_level = resp.get('set_backup_reserve_percent', {}).get('backup_reserve_percent') 50 | else: 51 | # TODO: may need to poll API until change is detected (in cloud mode) 52 | installed_level = pw.get_reserve(force=True) 53 | if installed_level != new_reserve_level: 54 | print(f"Set battery reserve level to {new_reserve_level}% failed.") 55 | # revert to original value just in case 56 | pw.set_reserve(level=original_reserve_level) 57 | 58 | 59 | def test_post_functions(pw): 60 | # test battery reserve and mode change 61 | print("Testing set_operation()...") 62 | test_battery_mode_change(pw) 63 | test_battery_reserve_change(pw) 64 | print("Post functions test complete.") 65 | 66 | 67 | def run(include_post_funcs=False): 68 | for h in [host, ""]: 69 | if h: 70 | print(f"LOCAL MODE: Connecting to Powerwall at {h}") 71 | else: 72 | print(f"CLOUD MODE: Connecting to Powerwall via Tesla API") 73 | print("---------------------------------------------------------") 74 | 75 | # Connect to Powerwall 76 | pw = pypowerwall.Powerwall(h, password, email, timezone, authpath=auth_path, cachefile=cachefile_path) 77 | 78 | # noinspection PyUnusedLocal 79 | aggregates = pw.poll('/api/meters/aggregates') 80 | # noinspection PyUnusedLocal 81 | coe = pw.poll('/api/system_status/soe') 82 | 83 | # Pull Sensor Power Data 84 | grid = pw.grid() 85 | solar = pw.solar() 86 | battery = pw.battery() 87 | home = pw.home() 88 | 89 | # Display Data 90 | battery_level = pw.level() 91 | combined_power_metrics = pw.power() 92 | print(f"Battery power level: \033[92m{battery_level:.0f}%\033[0m") 93 | print(f"Combined power metrics: \033[92m{combined_power_metrics}\033[0m") 94 | print("") 95 | 96 | # Display Power in kW 97 | print(f"Grid Power: \033[92m{float(grid) / 1000.0:.2f}kW\033[0m") 98 | print(f"Solar Power: \033[92m{float(solar) / 1000.0:.2f}kW\033[0m") 99 | print(f"Battery Power: \033[92m{float(battery) / 1000.0:.2f}kW\033[0m") 100 | print(f"Home Power: \033[92m{float(home) / 1000.0:.2f}kW\033[0m") 101 | print() 102 | 103 | # Raw JSON Payload Examples 104 | print(f"Grid raw: \033[92m{pw.grid(verbose=True)!r}\033[0m\n") 105 | print(f"Solar raw: \033[92m{pw.solar(verbose=True)!r}\033[0m\n") 106 | 107 | # Test each function 108 | print("Testing each function:") 109 | functions = [ 110 | pw.poll, 111 | pw.level, 112 | pw.power, 113 | pw.site, 114 | pw.solar, 115 | pw.battery, 116 | pw.load, 117 | pw.grid, 118 | pw.home, 119 | pw.vitals, 120 | pw.strings, 121 | pw.din, 122 | pw.uptime, 123 | pw.version, 124 | pw.status, 125 | pw.site_name, 126 | pw.temps, 127 | pw.alerts, 128 | pw.system_status, 129 | pw.battery_blocks, 130 | pw.grid_status, 131 | pw.is_connected, 132 | pw.get_reserve, 133 | pw.get_mode, 134 | pw.get_time_remaining 135 | ] 136 | for func in functions: 137 | print(f"{func.__name__}()") 138 | print(f"{func()}") 139 | 140 | if include_post_funcs: 141 | test_post_functions(pw) 142 | 143 | print("All functions tested.") 144 | print("") 145 | print("Testing all functions and printing result:") 146 | input("Press Enter to continue...") 147 | print("") 148 | 149 | for func in functions: 150 | print(f"{func.__name__}()") 151 | print("\033[92m", end="") 152 | print(func()) 153 | print("\033[0m") 154 | 155 | print("All functions tested.") 156 | print("") 157 | input("Press Enter to continue...") 158 | print("") 159 | 160 | print("All tests completed.") 161 | print("") 162 | 163 | 164 | if __name__ == "__main__": 165 | run(include_post_funcs=True) 166 | -------------------------------------------------------------------------------- /dashboard/README.md: -------------------------------------------------------------------------------- 1 | # Powerwall Dashboard 2 | 3 | Monitoring Dashboard for the Tesla Powerwall using Grafana, InfluxDB and Telegraf. See [jasonacox/Powerwall-Dashboard](https://github.com/jasonacox/Powerwall-Dashboard). 4 | 5 | ![Dashboard](https://user-images.githubusercontent.com/836718/144769680-78b8abf4-4336-4672-9483-896b0476ec44.png) 6 | ![Strings](https://user-images.githubusercontent.com/836718/146310511-7863e4bb-7e43-40b9-9790-65c1d6ce24ba.png) 7 | 8 | ## Installation and Setup 9 | 10 | See [jasonacox/Powerwall-Dashboard](https://github.com/jasonacox/Powerwall-Dashboard#requirements). 11 | 12 | ## Credits 13 | 14 | This is based on the great work by [mihailescu2m](https://github.com/mihailescu2m/powerwall_monitor) but has been modified to use pypowerwall as a proxy to the Powerwall and includes solar String graphs for Powerwall+ systems. 15 | -------------------------------------------------------------------------------- /docs/api.txt: -------------------------------------------------------------------------------- 1 | /api/auth/toggle/login 2 | /api/auth/toggle/start 3 | /api/auth/toggle/supported 4 | /api/autoconfig/cancel 5 | /api/autoconfig/retry 6 | /api/autoconfig/run 7 | /api/autoconfig/status 8 | /api/config 9 | /api/customer 10 | /api/customer/registration 11 | /api/customer/registration/legal 12 | /api/devices/vitals 13 | /api/logging 14 | /api/logout 15 | /api/meters 16 | /api/meters/${e}/ct_config 17 | /api/meters/${e}/cts 18 | /api/meters/${e}/invert_cts 19 | /api/meters/${o.serial}/cts 20 | /api/meters/detect_wired_meters 21 | /api/meters/inverter_meter_readings 22 | /api/meters/readings 23 | /api/meters/status 24 | /api/networks 25 | /api/networks/${e}/disconnect 26 | /api/networks/client_protocols 27 | /api/networks/enable_${e} 28 | /api/networks/request_scan_wifi 29 | /api/operation 30 | /api/password/generate 31 | /api/powerwalls 32 | /api/powerwalls/enable_inverter_solar_meter_readings 33 | /api/powerwalls/phase_detection 34 | /api/powerwalls/phase_usages 35 | /api/powerwalls/pvi_power_status 36 | /api/powerwalls/scan 37 | /api/powerwalls/status 38 | /api/powerwalls/update 39 | /api/site_info 40 | /api/site_info/extra_programs 41 | /api/site_info/grid_code 42 | /api/site_info/grid_regions 43 | /api/site_info/offgrid 44 | /api/site_info/site_name 45 | /api/site_info/timezone 46 | /api/sitemaster/run 47 | /api/sitemaster/run_for_commissioning 48 | /api/sitemaster/stop 49 | /api/solar_powerwall/reset 50 | /api/status 51 | /api/synchrometer/ct_voltage_references 52 | /api/synchrometer/ct_voltage_references/options 53 | /api/system/networks/conn_tests 54 | /api/system/networks/ping_test 55 | /api/system/testing 56 | /api/system/testing/PINV_TEST 57 | /api/system/update 58 | /api/system/update/urgency 59 | /api/system/update?force=true 60 | /api/system_status/grid_faults 61 | /api/system_status/grid_status 62 | /api/troubleshooting/problems 63 | /api/v2/islanding/mode 64 | -------------------------------------------------------------------------------- /docs/flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/docs/flow.png -------------------------------------------------------------------------------- /docs/portal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/docs/portal.png -------------------------------------------------------------------------------- /docs/vitals-example-failed-pw.json: -------------------------------------------------------------------------------- 1 | { 2 | "STSTSM--1152100-14-E--CNxxx": { 3 | "STSTSM-Location": "Gateway", 4 | "alerts": [ 5 | "GridCodesWrite", 6 | "FWUpdateSucceeded", 7 | "BatteryFault" 8 | ], 9 | "firmwareVersion": "2022-01-31-g9853d3db5", 10 | "lastCommunicationTime": 1645918731, 11 | "manufacturer": "TESLA", 12 | "partNumber": "1152100-14-E", 13 | "serialNumber": "CNxxx", 14 | "teslaEnergyEcuAttributes": { 15 | "ecuType": 207 16 | } 17 | }, 18 | "TEPINV--1081100-01-U--T21C0007854": { 19 | "componentParentDin": "TETHC--3012170-05-B--TG121094002C9V", 20 | "firmwareVersion": "b0ec24329c08e4", 21 | "lastCommunicationTime": 1645911911, 22 | "manufacturer": "TESLA", 23 | "partNumber": "1081100-01-U", 24 | "serialNumber": "Txxx", 25 | "teslaEnergyEcuAttributes": { 26 | "ecuType": 253 27 | } 28 | }, 29 | "TEPOD--1081100-01-U--T21C0007854": { 30 | "alerts": [ 31 | "POD_f029_HW_CMA_OV", 32 | "POD_w024_HW_Fault_Asserted", 33 | "POD_w029_HW_CMA_OV", 34 | "POD_w031_SW_Brick_OV", 35 | "POD_w044_SW_Brick_UV_Warning", 36 | "POD_w045_SW_Brick_OV_Warning", 37 | "POD_w048_SW_Cell_Voltage_Sens", 38 | "POD_w058_SW_App_Boot", 39 | "POD_w063_SW_SOC_Imbalance", 40 | "POD_w067_SW_Not_Enough_Energy_Precharge", 41 | "POD_w090_SW_SOC_Imbalance_Limit_Charge", 42 | "POD_w093_SW_Charge_Request", 43 | "POD_w105_SW_EOD", 44 | "POD_w110_SW_EOC" 45 | ], 46 | "componentParentDin": "TETHC--3012170-05-B--TG121094002C9V", 47 | "firmwareVersion": "b0ec24329c08e4", 48 | "lastCommunicationTime": 1645911911, 49 | "manufacturer": "TESLA", 50 | "partNumber": "1081100-01-U", 51 | "serialNumber": "Txxx", 52 | "teslaEnergyEcuAttributes": { 53 | "ecuType": 226 54 | } 55 | }, 56 | "TESLA--JBL20314C4D370": { 57 | "componentParentDin": "STSTSM--1152100-14-E--CNxxx", 58 | "lastCommunicationTime": 1645918731, 59 | "manufacturer": "TESLA", 60 | "meterAttributes": { 61 | "meterLocation": [ 62 | 1, 63 | 4 64 | ] 65 | }, 66 | "serialNumber": "JBLxxx" 67 | }, 68 | "TESYNC--1449782-04-D--JBLxxx": { 69 | "ISLAND_FreqL1_Load": 50.03, 70 | "ISLAND_FreqL1_Main": 50.03, 71 | "ISLAND_FreqL2_Load": 50.03, 72 | "ISLAND_FreqL2_Main": 50.04, 73 | "ISLAND_FreqL3_Load": 50.019999999999996, 74 | "ISLAND_FreqL3_Main": 50.019999999999996, 75 | "ISLAND_GridConnected": true, 76 | "ISLAND_GridState": "ISLAND_GridState_Grid_Compliant", 77 | "ISLAND_L1L2PhaseDelta": -256.0, 78 | "ISLAND_L1L3PhaseDelta": -120.5, 79 | "ISLAND_L1MicrogridOk": true, 80 | "ISLAND_L2L3PhaseDelta": 119.0, 81 | "ISLAND_L2MicrogridOk": true, 82 | "ISLAND_L3MicrogridOk": true, 83 | "ISLAND_PhaseL1_Main_Load": -1.0, 84 | "ISLAND_PhaseL2_Main_Load": -1.0, 85 | "ISLAND_PhaseL3_Main_Load": -1.0, 86 | "ISLAND_ReadyForSynchronization": true, 87 | "ISLAND_VL1N_Load": 247.5, 88 | "ISLAND_VL1N_Main": 246.5, 89 | "ISLAND_VL2N_Load": 248.0, 90 | "ISLAND_VL2N_Main": 248.0, 91 | "ISLAND_VL3N_Load": 245.0, 92 | "ISLAND_VL3N_Main": 245.5, 93 | "METER_X_CTA_I": 5.901, 94 | "METER_X_CTA_InstReactivePower": 304.0, 95 | "METER_X_CTA_InstRealPower": 1406.0, 96 | "METER_X_CTB_I": 2.5315, 97 | "METER_X_CTB_InstReactivePower": -154.0, 98 | "METER_X_CTB_InstRealPower": -600.0, 99 | "METER_X_CTC_I": 2.5585, 100 | "METER_X_CTC_InstReactivePower": -278.0, 101 | "METER_X_CTC_InstRealPower": -552.0, 102 | "METER_X_LifetimeEnergyExport": 6195476.0, 103 | "METER_X_LifetimeEnergyImport": 5631703.0, 104 | "METER_X_VL1N": 246.04, 105 | "METER_X_VL2N": 247.11, 106 | "METER_X_VL3N": 244.66, 107 | "METER_Y_CTA_I": 2.91, 108 | "METER_Y_CTA_InstReactivePower": 129.0, 109 | "METER_Y_CTA_InstRealPower": 704.0, 110 | "METER_Y_CTB_I": 2.5395, 111 | "METER_Y_CTB_InstReactivePower": 110.0, 112 | "METER_Y_CTB_InstRealPower": 614.0, 113 | "METER_Y_CTC_I": 2.571, 114 | "METER_Y_CTC_InstReactivePower": 110.0, 115 | "METER_Y_CTC_InstRealPower": 616.0, 116 | "METER_Y_LifetimeEnergyExport": 16680.0, 117 | "METER_Y_LifetimeEnergyImport": 7861507.0, 118 | "METER_Y_VL1N": 246.70000000000002, 119 | "METER_Y_VL2N": 246.78, 120 | "METER_Y_VL3N": 244.32, 121 | "SYNC-Phase1Usage": "Backup", 122 | "SYNC-Phase2Usage": "NonBackup", 123 | "SYNC-Phase3Usage": "NonBackup", 124 | "SYNC_ExternallyPowered": false, 125 | "SYNC_SiteSwitchEnabled": false, 126 | "alerts": [ 127 | "SYNC_a001_SW_App_Boot" 128 | ], 129 | "componentParentDin": "STSTSM--1152100-14-E--CN3xxx", 130 | "firmwareVersion": "b0ec24329c08e4", 131 | "lastCommunicationTime": 1645918731, 132 | "manufacturer": "TESLA", 133 | "partNumber": "1449782-04-D", 134 | "serialNumber": "JBLxxx", 135 | "teslaEnergyEcuAttributes": { 136 | "ecuType": 259 137 | } 138 | }, 139 | "TETHC--3012170-05-B--TG121094002C9V": { 140 | "THC_AmbientTemp": 21.5, 141 | "THC_State": "THC_STATE_AUTONOMOUSCONTROL", 142 | "alerts": [ 143 | "THC_w061_CAN_TX_FIFO_Overflow", 144 | "THC_w155_Backup_Genealogy_Updated" 145 | ], 146 | "componentParentDin": "STSTSM--1152100-14-E--CNxxx", 147 | "firmwareVersion": "b0ec24329c08e4", 148 | "lastCommunicationTime": 1645918730, 149 | "manufacturer": "TESLA", 150 | "partNumber": "3012170-05-B", 151 | "serialNumber": "TGxxx", 152 | "teslaEnergyEcuAttributes": { 153 | "ecuType": 224 154 | } 155 | } 156 | } -------------------------------------------------------------------------------- /example-cloud-mode.py: -------------------------------------------------------------------------------- 1 | # Example test for pypowerwall 2 | import os 3 | import pypowerwall 4 | 5 | if __name__ == "__main__": 6 | # Optional: Turn on Debug Mode 7 | pypowerwall.set_debug(True) 8 | 9 | # Credentials for your Powerwall - Customer Login Data 10 | # Set appropriate env vars or change the defaults 11 | email = os.environ.get('PW_EMAIL', 'email@example.com') 12 | timezone = os.environ.get('PW_TIMEZONE', 'America/Los_Angeles') # Change to your local timezone/tz 13 | auth_path = os.environ.get('PW_AUTH_PATH', "") 14 | 15 | # Connect to Powerwall 16 | pw = pypowerwall.Powerwall("", "", email, timezone, authpath=auth_path, cloudmode=True) 17 | 18 | # Display Metric Examples 19 | print("Battery power level: %0.0f%%" % pw.level()) 20 | print("Power response: %r" % pw.power()) 21 | print("Grid Power: %0.2fkW" % (float(pw.grid()) / 1000.0)) 22 | print("Solar Power: %0.2fkW" % (float(pw.solar()) / 1000.0)) 23 | print("Battery Power: %0.2fkW" % (float(pw.battery()) / 1000.0)) 24 | print("Home Power: %0.2fkW" % (float(pw.home()) / 1000.0)) 25 | 26 | # Raw JSON Data Examples 27 | print("Status: %s" % pw.status()) 28 | print("Grid raw: %r" % pw.grid(verbose=True)) 29 | print("Solar raw: %r" % pw.solar(verbose=True)) 30 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | # Example test for pypowerwall 2 | import os 3 | import pypowerwall 4 | 5 | if __name__ == "__main__": 6 | # Optional: Turn on Debug Mode 7 | pypowerwall.set_debug(True) 8 | 9 | # Credentials for your Powerwall - Customer Login Data 10 | # Set appropriate env vars or change the defaults 11 | password = os.environ.get('PW_PASSWORD', 'password') 12 | email = os.environ.get('PW_EMAIL', 'email@example.com') 13 | host = os.environ.get('PW_HOST', 'localhost') # Change to the IP of your Powerwall 14 | timezone = os.environ.get('PW_TIMEZONE', 'America/Los_Angeles') # Change to your local timezone/tz 15 | 16 | # Connect to Powerwall 17 | pw = pypowerwall.Powerwall(host, password, email, timezone, cloudmode=False) 18 | 19 | # Display Metric Examples 20 | print("Battery power level: %0.0f%%" % pw.level()) 21 | print("Power response: %r" % pw.power()) 22 | print("Grid Power: %0.2fkW" % (float(pw.grid()) / 1000.0)) 23 | print("Solar Power: %0.2fkW" % (float(pw.solar()) / 1000.0)) 24 | print("Battery Power: %0.2fkW" % (float(pw.battery()) / 1000.0)) 25 | print("Home Power: %0.2fkW" % (float(pw.home()) / 1000.0)) 26 | 27 | # Raw JSON Data Examples 28 | print("Status: %s" % pw.status()) 29 | print("Grid raw: %r" % pw.grid(verbose=True)) 30 | print("Solar raw: %r" % pw.solar(verbose=True)) 31 | print("Strings raw: %r" % pw.strings(verbose=True, jsonformat=True)) 32 | -------------------------------------------------------------------------------- /examples/network_route.py: -------------------------------------------------------------------------------- 1 | # This file contains two examples of how to use the `route` command from Linux in Python. 2 | # 1. manage_ip_route_pyroute: Recommended approach. Less error-prone due to use of encapsulated pyroute2 framework. 3 | # 2. manage_ip_route_subprocess: Simpler, straightforward approach that utilizes Python subprocces. 4 | 5 | import socket 6 | import subprocess 7 | from enum import Enum, auto 8 | from typing import Optional 9 | 10 | from pyroute2 import IPRoute, NetlinkError 11 | 12 | 13 | class Tense(Enum): 14 | """ String/tense variation on the route operations. 15 | """ 16 | BASE = auto() 17 | PRESENT = auto() 18 | PAST = auto() 19 | 20 | 21 | class RouteOperation(Enum): 22 | """Whether to add or remove a route. 23 | """ 24 | ADD = { 25 | Tense.BASE: "add", 26 | Tense.PRESENT: "adding", 27 | Tense.PAST: "added" 28 | } 29 | DELETE = { 30 | Tense.BASE: "del", 31 | Tense.PRESENT: "deleting", 32 | Tense.PAST: "deleted" 33 | } 34 | 35 | def get_action(self, tense: Tense) -> str: 36 | """Retrieve string representation appropriate to each RouteOperation tense. 37 | 38 | Args: 39 | tense (Tense): Tense for each operation. 40 | 41 | Returns: 42 | str: String representation of operation tense. 43 | """ 44 | return self.value.get(tense, "Tense Missing") 45 | 46 | 47 | def manage_ip_route_pyroute(operation: RouteOperation, destination: str, gateway: str, interface: Optional[str] = None, interactive: bool = False) -> None: 48 | """ Manages an IP route using pyroute2's IPRoute, utilizing onlink to ensure the route works. 49 | For instance, if you want to map all requests that go from a CIDR range of 192.168.91.0/24 => 192.168.1.250, 50 | use this to add/delete such a route. This can also be configured on your router. 51 | 52 | The calling process must be run as root (sudo). 53 | 54 | Args: 55 | operation (RouteOperation): RouteOperation.ADD or RouteOperation.DELETE, corresponding to desired operation for network route. 56 | destination (str): The network or IP address in IPv4 CIDR notation (e.g., "192.168.1.0/24") 57 | gateway (str): The IP address of the Tesla Gateway/Powerwall (e.g., "192.168.1.250") 58 | interface (str, optional): The optional network interface (e.g., "eth0"). If not provided, the route is managed without specifying an interface. Defaults to None. 59 | interactive (bool, optional): Whether messages should be printed. Defaults to False. 60 | 61 | Example usage: 62 | manage_ip_route_pyroute(RouteOperation.ADD, "192.168.1.0/24", "192.168.1.1") 63 | manage_ip_route_pyroute(RouteOperation.DELETE, "192.168.1.0/24", "192.168.1.1", "eth0") 64 | """ 65 | 66 | route_params = { 67 | "family": socket.AF_INET6 if ":" in destination else socket.AF_INET, 68 | "dst": destination, 69 | "gateway": gateway 70 | } 71 | 72 | if operation == RouteOperation.ADD: 73 | route_params["flags"] = ["onlink"] 74 | 75 | with IPRoute() as ip: 76 | try: 77 | # Lookup interface index if interface is specified 78 | idxs = ip.link_lookup(ifname=interface) if interface else None 79 | if not idxs: 80 | print(f"Interface '{interface}' not found.") 81 | return 82 | route_params["oif"] = idxs[0] 83 | # Perform the route operation 84 | ip.route(operation.get_action(Tense.BASE), **route_params) 85 | if interactive: 86 | print(f"Route {operation.get_action(Tense.PAST)}: {destination} via {gateway}" + (f" dev {interface}" if interface else "") + (f" {','.join(route_params['flags'])}" if 'flags' in route_params else "")) 87 | except NetlinkError as e: 88 | print(f"Network specific error occurred {operation.get_action(Tense.PRESENT)} route: {e}") 89 | except Exception as e: 90 | print(f"An unexpected error occurred: {e}") 91 | 92 | 93 | def manage_ip_route_subprocess(operation: RouteOperation, destination: str, gateway: str, interface: Optional[str] = None, interactive: bool = False) -> None: 94 | """ Manages an IP route using the 'ip' command, utilizing onlink to ensure the route works. 95 | For instance, if you want to map all requests that go from a CIDR range of 192.168.91.0/24 => 192.168.1.250, 96 | use this to add/delete such a route. This can also be configured on your router. 97 | 98 | Args: 99 | operation (RouteOperation): RouteOperation.ADD or RouteOperation.DELETE, corresponding to desired operation for network route. 100 | destination (str): The network or IP address in IPv4 CIDR notation (e.g., "192.168.1.0/24") 101 | gateway (str): The IP address of the Tesla Gateway/Powerwall (e.g., "192.168.1.250") 102 | interface (str, optional): The optional network interface (e.g., "eth0"). If not provided, the route is managed without specifying an interface. Defaults to None. 103 | interactive (bool, optional): Whether messages should be printed. Defaults to False. 104 | 105 | Example usage: 106 | manage_ip_route_subprocess(RouteOperation.ADD, "192.168.1.0/24", "192.168.1.1") 107 | manage_ip_route_subprocess(RouteOperation.DELETE, "192.168.1.0/24", "192.168.1.1", "eth0") 108 | """ 109 | command = ["sudo", "ip", "route", operation.get_action(Tense.BASE), destination, "via", gateway] 110 | 111 | if interface: 112 | command.extend(["dev", interface]) 113 | 114 | if operation == RouteOperation.ADD: 115 | command.append("onlink") 116 | 117 | try: 118 | subprocess.run(command, check=True) 119 | if interactive: 120 | print(f"Route {operation.get_action(Tense.PAST)}: {destination} via {gateway}" + (f" dev {interface}" if interface else "") + (f" onlink" if 'onlink' in command else "")) 121 | except subprocess.CalledProcessError as e: 122 | print(f"Error adding route: {e}") 123 | -------------------------------------------------------------------------------- /examples/vitals.py: -------------------------------------------------------------------------------- 1 | # pyPowerWall Vitals 2 | # -*- coding: utf-8 -*- 3 | """ 4 | This script pulls the Powerwall Vitals API 5 | 6 | Author: Jason A. Cox 7 | For more information see https://github.com/jasonacox/pypowerwall 8 | 9 | * /api/devices/vitals produces a protobuf binary payload 10 | """ 11 | 12 | import pypowerwall 13 | 14 | # Update with your details 15 | password='password' 16 | email='email@example.com' 17 | host = "10.0.1.23" 18 | timezone = "America/LosAngeles" 19 | 20 | # Make sure binary polling allowed 21 | if pypowerwall.version_tuple < (0,0,3): 22 | print("\n*** WARNING: Minimum pypowerwall version 0.0.3 required for proper function! ***\n\n") 23 | 24 | # Connect to Powerwall 25 | pw = pypowerwall.Powerwall(host,password,email,timezone) 26 | 27 | # Display Vitals 28 | print("Vitals: %r\n" % pw.vitals()) 29 | 30 | 31 | 32 | # Below is an alternative manual way to parse the protobuf payload 33 | 34 | """ 35 | import struct 36 | 37 | # Pull vitals payload - binary format in protobuf 38 | stream = pw.poll('/api/devices/vitals') 39 | streamsize = len(stream) 40 | print("Size of stream = %d" % streamsize) 41 | 42 | # Walk through payload 43 | index = 0 44 | skip = False 45 | skipdata = "" 46 | while(index < streamsize): 47 | key = "" 48 | value = "" 49 | meta = "" 50 | # check for kv signal 51 | if(streamsize-index > 3 and stream[index] == ord('\x12') and stream[index+2] == ord('\x0a')): 52 | # print skip data 53 | if skip: 54 | print(" > Skipped: %s" % skipdata) 55 | skip = False 56 | skipdata = "" 57 | # Parse payload 58 | meta=stream[index+1] 59 | index += 3 60 | # grab key starting with size value 61 | size = stream[index] 62 | index += 1 63 | if(size > 0): 64 | key = stream[index:index+size].decode() 65 | index += size 66 | delimiter = stream[index] 67 | index += 1 68 | if(delimiter == ord('!')): 69 | # numerical value 70 | # DOUBLE 71 | v = stream[index:index+8] 72 | v = struct.unpack(' 0): 83 | value = stream[index:index+size].decode() 84 | index += size 85 | if(delimiter == ord('0')): 86 | # boolean value 87 | if(stream[index] == 1): 88 | value = "TRUE" 89 | else: 90 | value = "FALSE" 91 | index += 1 92 | # Print it 93 | print("[%d] %s: %s" % (meta,key,value)) 94 | continue 95 | 96 | skip = True 97 | if(chr(stream[index]).isalnum()): 98 | skipdata += " %c" % chr(stream[index]) 99 | else: 100 | skipdata += " 0x%02d" % stream[index] 101 | index += 1 102 | 103 | # end while 104 | 105 | """ -------------------------------------------------------------------------------- /examples/vitals/README.md: -------------------------------------------------------------------------------- 1 | # Tesla Powerwall Vitals Data 2 | 3 | This script, [pull_vitals.py](pull_vitals.py) pulls the Powerwall Vitals API and returns a JSON result. 4 | 5 | ## Notes 6 | 7 | * Work in Progress 8 | * API Endpoint on Powerwall: /api/devices/vitals (protobuf binary payload) 9 | * Output from this script is a list of vital data points from all Tesla Energy Powerwall devices and a resulting combined JSON payload. 10 | 11 | ## Requirements 12 | 13 | Tesla is using a protobuf response for this API. This requires the necessary protobuf definition and libraries: 14 | 15 | Install the protobuf python module: 16 | 17 | ```bash 18 | # Install protobuf 19 | pip install protobuf 20 | ``` 21 | 22 | The script uses the generated file, [tesla_pb2.py](tesla_pb2.py). This is generated from the `protoc` compiler. If you wish to compile the tesla.proto definition file yourself, install protobuf of your systems (e.g. `brew install protobuf`) and run: 23 | 24 | ```bash 25 | # Run protobuf compiler to build definition python code 26 | protoc --python_out=. tesla.proto 27 | ``` 28 | 29 | ## Credits 30 | 31 | * Protobuf definition [tesla.proto](tesla.proto) thanks to @brianhealey. See https://github.com/vloschiavo/powerwall2/issues/51#issuecomment-923574346 -------------------------------------------------------------------------------- /examples/vitals/pull_vitals.py: -------------------------------------------------------------------------------- 1 | # pyPowerWall Vitals 2 | # -*- coding: utf-8 -*- 3 | """ 4 | This script pulls the Powerwall Vitals API 5 | 6 | Author: Jason A. Cox 7 | For more information see https://github.com/jasonacox/pypowerwall 8 | 9 | * Work in Progress 10 | * API Endpoing on Powerwall: /api/devices/vitals 11 | * Result is a protobuf binary payload 12 | 13 | Requires protobuf: 14 | pip install protobuf 15 | tesla_pb2.py # tesla protobuf definition file built using: 16 | protoc --python_out=. tesla.proto 17 | 18 | Credits: 19 | Protobuf definition (tesla.proto) thanks to @brianhealey 20 | 21 | Date: 27 Nov 2021 22 | """ 23 | 24 | import pypowerwall 25 | import tesla_pb2 26 | import json 27 | 28 | # Update with your details 29 | password='password' 30 | email='email@example.com' 31 | host = "10.0.1.23" 32 | timezone = "America/LosAngeles" 33 | 34 | # Make sure binary polling allowed 35 | if pypowerwall.version_tuple < (0,0,3): 36 | print("\n*** WARNING: Minimum pypowerwall version 0.0.3 required for proper function! ***\n\n") 37 | 38 | # Connect to Powerwall 39 | pw = pypowerwall.Powerwall(host,password,email,timezone) 40 | 41 | # Pull vitals payload - binary format in protobuf 42 | stream = pw.poll('/api/devices/vitals') 43 | streamsize = len(stream) 44 | print("Size of stream = %d" % streamsize) 45 | 46 | # Protobuf payload processing 47 | pw = tesla_pb2.DevicesWithVitals() 48 | pw.ParseFromString(stream) 49 | num = len(pw.devices) 50 | print("There are %d devices found." % num) 51 | 52 | # List Devices 53 | x = 0 54 | output = {} 55 | while(x < num): 56 | device = pw.devices[x].device.device 57 | parent = str(device.componentParentDin.value) 58 | vitals = pw.devices[x].vitals 59 | alerts = pw.devices[x].alerts 60 | 61 | name = str(device.din.value) 62 | print("Device %d: %s " % (x, name)) 63 | 64 | # e.STSTSM = "STSTSM", 65 | # e.POD = "TEPOD", 66 | # e.PINV = "TEPINV", 67 | # e.PVAC = "PVAC", 68 | # e.PVS = "PVS", 69 | # e.SYNC = "TESYNC", 70 | # e.MSA = "TEMSA", 71 | # e.NEURIO = "NEURIO", 72 | # e.ACPW = "ACPW", 73 | # e.PVI = "PVI", 74 | # e.SPW = "SPW" 75 | 76 | if name.startswith("TETHC--"): 77 | print(" - Inverter") 78 | 79 | if device.HasField("partNumber"): 80 | print(" - Part Number: %s" % device.partNumber.value) 81 | if device.HasField("serialNumber"): 82 | print(" - Serial Number: %s" % device.serialNumber.value) 83 | if device.HasField("manufacturer"): 84 | print(" - Manufacturer: %s" % device.manufacturer.value) 85 | if device.HasField("siteLabel"): 86 | print(" - Site Label: %s" % device.siteLabel.value) 87 | if device.HasField("componentParentDin"): 88 | print(" - Parent DIN: %s" % device.componentParentDin.value) 89 | if device.HasField("firmwareVersion"): 90 | print(" - Firmware Version: %s" % device.firmwareVersion.value) 91 | if device.HasField("firstCommunicationTime"): 92 | print(" - First Communicated At: %s" % device.firstCommunicationTime.ToDatetime()) 93 | if device.HasField("lastCommunicationTime"): 94 | print(" - Last Communicated At: %s" % device.lastCommunicationTime.ToDatetime()) 95 | # if device.HasField("connectionParameters"): 96 | # print(" - Connection Parameters: %s" % device.connectionParameters) 97 | 98 | if device.HasField("deviceAttributes"): 99 | attributes = device.deviceAttributes 100 | if attributes.HasField("teslaEnergyEcuAttributes"): 101 | print(" - Ecu:") 102 | print(" - type: %i" % attributes.teslaEnergyEcuAttributes.ecuType) 103 | if attributes.HasField("generatorAttributes"): 104 | print(" - Generator:") 105 | print(" - nameplateRealPowerW: %i" % attributes.generatorAttributes.nameplateRealPowerW) 106 | print(" - nameplateApparentPowerVa: %i" % attributes.generatorAttributes.nameplateApparentPowerVa) 107 | if attributes.HasField("pvInverterAttributes"): 108 | print(" - Inverter:") 109 | print(" - nameplateRealPowerW: %i" % attributes.pvInverterAttributes.nameplateRealPowerW) 110 | if attributes.HasField("meterAttributes"): 111 | print(" - Meter:") 112 | for location in attributes.meterAttributes.meterLocation: 113 | print(" - location: %i" % location) 114 | a = 0 115 | 116 | while (a < len(alerts)): 117 | if (a == 0): 118 | print(" - Alerts:") 119 | 120 | print(" - ALERT_%i = %s" % (a, alerts[a]) ) 121 | a += 1 122 | 123 | print(" - Vitals:") 124 | 125 | for y in pw.devices[x].vitals: 126 | vital_name = str(y.name) 127 | if (y.HasField('intValue')): 128 | print(" - %s = %i" % (y.name, y.intValue)) 129 | vital_value = y.intValue 130 | if(y.HasField('boolValue')): 131 | print(" - %s = %r" % (y.name,y.boolValue)) 132 | vital_value = y.boolValue 133 | if(y.HasField('stringValue')): 134 | print(" - %s = '%s'" % (y.name,y.stringValue)) 135 | vital_value = y.stringValue 136 | if(y.HasField('floatValue')): 137 | print(" - %s = '%f'" % (y.name,y.floatValue)) 138 | vital_value = y.floatValue 139 | # Record in output dictionary 140 | if name not in output.keys(): 141 | output[name] = {} 142 | output[name]['Parent'] = parent 143 | output[name][vital_name] = vital_value 144 | 145 | if name in output.keys() and len(alerts) > 0: 146 | output[name]["ALERT_Count"] = len(pw.devices[x].alerts) 147 | 148 | x += 1 149 | 150 | json_out = json.dumps(output, indent=4, sort_keys=True) 151 | print("Resulting vitals:\n", json_out) 152 | -------------------------------------------------------------------------------- /proxy/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine 2 | WORKDIR /app 3 | COPY requirements.txt /app/requirements.txt 4 | RUN pip3 install -r requirements.txt 5 | COPY . . 6 | CMD ["python3", "server.py"] 7 | EXPOSE 8675 8 | -------------------------------------------------------------------------------- /proxy/Dockerfile.beta: -------------------------------------------------------------------------------- 1 | FROM python:3.10-alpine 2 | WORKDIR /app 3 | COPY beta.txt /app/requirements.txt 4 | RUN pip3 install -r requirements.txt 5 | COPY . . 6 | CMD ["python3", "server.py"] 7 | EXPOSE 8675 8 | -------------------------------------------------------------------------------- /proxy/HELP.md: -------------------------------------------------------------------------------- 1 | # pyPowerwall Proxy Help 2 | 3 | Besides providing authentication and payload caching, the Proxy exposes several APIs that aggregating Powerwall data into simple JSON output for convenient processing. It also provides several read-only pass through Powerwall API calls. See [README](https://github.com/jasonacox/pypowerwall/blob/main/proxy/README.md) for setup instructions. 4 | 5 | Please see [API.md](./API.md) for all proxy endpoints and usage. 6 | 7 | ## Release Notes 8 | 9 | Release notes are in the [RELEASE.md](https://github.com/jasonacox/pypowerwall/blob/main/proxy/RELEASE.md) file. 10 | -------------------------------------------------------------------------------- /proxy/beta.txt: -------------------------------------------------------------------------------- 1 | bs4==0.0.2 2 | requests 3 | protobuf 4 | teslapy 5 | 6 | -------------------------------------------------------------------------------- /proxy/localhost.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDIrws/RUeO1Rni 3 | ir2ri+DXsCOiprIgLCF8Clrhd4Zyeeuvqjx6pcEKD/4cL42PqbuGErB7tKXr7vhS 4 | nghXHtAEOf1EDW7tpU40LvTXYceInoK0L+pS0Em3qeXcGp+7qVl3GczoUwBxqWSN 5 | jYzdsUiltiZVhNyLRYXmz/J1DXIOKAyUPmQTzHwS+c2kfVmpobBcgfwTBZW2PQ1H 6 | oZ0Iwq5rvwMScFySCnNn55uX+xuLc96T8gScTajb3BEbAbPykrSvk9CJW2MgdsAo 7 | 7gk0w1LPtyZuv6wNa+TLPiZ/pZOLBzuiOI7AzoyCalsGGuplSnL4R7bBkKzKdS8j 8 | yW60PNjrAgMBAAECggEBAJzs+/+Cvhz7mF0knoI5RB2FF6iFbz5nI9vqAPzTySdV 9 | HS5lERva52Nl9A+4Q5r2X7PMg4KIVUJzwGxiNSVi68iSS/BeDML6A3gcy8psJGo5 10 | gP1DhpkxVKOw0BRYIVXObC4M18VHuk4m5oEmEeP9UFB8aedvmEGzoKxHKVHMrMZR 11 | uiuvhRYlg4MrJ0hngdoO7jqfg7F9tcw66qwoj0n88/aI9AxFzoYXB2Z/2y1yMeop 12 | Ctgqrvl7gMLHB+7jzcJuDnYJzdHJEo3Rgk7yaeLdjCyz66hUXskEe5wPRSj08qCT 13 | 2Ps+RaduUP6C9KxiAUTZjj56iAHDkocCX6nLomt98PkCgYEA6o7kk+YN1l/iXV5P 14 | E1DuQrpDxlj2ZezAU+3xl3rokqzE/ISD+A3jdMfBraqs4SkTrcTMd2srZbu3AEGd 15 | QBWd1KmR1zYUjqOg0Emo17sC3WoX8xOrjFwdjaoXSi0xD86PHEbi1pOwftnnR+hW 16 | Y0IuTqKIuW1BvTQikoy7sj+x/U0CgYEA2wds6/rWpFZg2pqPMc1x3ztRmwD44rzz 17 | PFg/bD36phTiGsooqdAo58n1YCaOYJt/ny1DPRMb6CXOPfF05O2bcA+OwBrZWU0f 18 | yd/vofS4w/Qe3yHAR7qZ5JJrHeu1e2a3L9I6vBbRaV1r0zkmL1p8J0CJYaAzZlNG 19 | VaeBzvuL8xcCgYAXxK0S85/5VjQJBBJ9QZkzN87AXalyQKBooNb3Y6QHoOxBLmh1 20 | DWs8HTXaFE56boAo/qU9gKWgJHpx0zRNFyOsNhaqOTeyEJCuKpiqa6/poeOVZSvg 21 | CEGSZmb/xD6RfHvyAJjh54td/1S5a6i9XCp3G29BYvnjY1IRiaNHd77gjQKBgEhN 22 | YTVc7nH9WaeQEej8yrRIHp4uafpfKWQoNXeD1jPw/NqfFWFJJ9esIWYGFEXrzus6 23 | w9Frd3Dg2f40sMPJc+BAIn1j34/NF8tKMw6hfESjV3WM7K5A+QAtHVMZNiVwONR+ 24 | b4kbdzFy918YpHRJSGaktTUW7yC+KJ+p1f3/p6ktAoGBAOXYAxIacvw2A9jfVXhX 25 | L6fQmN9BWzFbuJ6NSdO2Jqpbqc/BUZh8My9BX0CNcNaahqE4ohA+l/4mPO0EU7Oz 26 | 8AzU7jm42PD1AW5SWdmTJnUsF++pv75quGz4U+sm9UI9IWvw0J8OUs0ydFFh93Dw 27 | Ooeq2H0cXKBS9mGQZ8sdidVK 28 | -----END PRIVATE KEY----- 29 | -----BEGIN CERTIFICATE----- 30 | MIIC3DCCAcSgAwIBAgIJANA/tohyxHNLMA0GCSqGSIb3DQEBCwUAMBYxFDASBgNV 31 | BAMTC3B5cG93ZXJ3YWxsMB4XDTIyMDUyMTA2NDQyN1oXDTMyMDUxODA2NDQyN1ow 32 | FjEUMBIGA1UEAxMLcHlwb3dlcndhbGwwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw 33 | ggEKAoIBAQDIrws/RUeO1Rniir2ri+DXsCOiprIgLCF8Clrhd4Zyeeuvqjx6pcEK 34 | D/4cL42PqbuGErB7tKXr7vhSnghXHtAEOf1EDW7tpU40LvTXYceInoK0L+pS0Em3 35 | qeXcGp+7qVl3GczoUwBxqWSNjYzdsUiltiZVhNyLRYXmz/J1DXIOKAyUPmQTzHwS 36 | +c2kfVmpobBcgfwTBZW2PQ1HoZ0Iwq5rvwMScFySCnNn55uX+xuLc96T8gScTajb 37 | 3BEbAbPykrSvk9CJW2MgdsAo7gk0w1LPtyZuv6wNa+TLPiZ/pZOLBzuiOI7AzoyC 38 | alsGGuplSnL4R7bBkKzKdS8jyW60PNjrAgMBAAGjLTArMBQGA1UdEQQNMAuCCWxv 39 | Y2FsaG9zdDATBgNVHSUEDDAKBggrBgEFBQcDATANBgkqhkiG9w0BAQsFAAOCAQEA 40 | xnTkShGWa+/o8886H8HAcHR6AtuMSn7H6wn2Pz5vHttFJsd5qy2Mzcrwvz7Fv8Wg 41 | iQkbsAeL730QExkc0+Un3bpj4MvMA/OhUzZlzM1OQCJSiCzpInC2sg7vgbbLbs/z 42 | Lrd4CMVrRpHnZ2vWxyTqDefzTkh5W5J0tIvPTxHwNuz0gaoBZWXxFYFl7mu8dE2Y 43 | yv0SUOrUkUDFhqbMmj+0rsV87ZHRcJ601IzoL2+/8XXO4lkYGgHMKhgmfchr3hDT 44 | QL8BwsJFPdS0BLypv0qU4MEnqgZxwkGeQ1083u2HfQjU9T6pYjOGEoP3LsgfLqZu 45 | WDL7pfRDmI3wTlyY5udCfA== 46 | -----END CERTIFICATE----- 47 | -------------------------------------------------------------------------------- /proxy/requirements.txt: -------------------------------------------------------------------------------- 1 | pypowerwall==0.13.0 2 | bs4==0.0.2 3 | -------------------------------------------------------------------------------- /proxy/transform.py: -------------------------------------------------------------------------------- 1 | import os 2 | import logging 3 | 4 | from bs4 import BeautifulSoup as Soup 5 | 6 | logging.basicConfig( 7 | format='%(asctime)s [%(name)s] [%(levelname)s] %(message)s', datefmt='%m/%d/%Y %I:%M:%S %p') 8 | logger = logging.getLogger(os.path.basename(__file__)) 9 | if os.environ.get("LOG_LEVEL", "").lower() == "debug": 10 | logger.setLevel(logging.DEBUG) 11 | else: 12 | logger.setLevel(logging.INFO) 13 | 14 | 15 | def get_static(web_root, fpath): 16 | if fpath.split('?')[0] == "/": 17 | fpath = "index.html" 18 | if fpath.startswith("/"): 19 | fpath = fpath[1:] 20 | freq = os.path.join(web_root, fpath) 21 | if os.path.exists(freq): 22 | if freq.lower().endswith(".js"): 23 | ftype = "application/javascript" 24 | elif freq.lower().endswith(".css"): 25 | ftype = "text/css" 26 | elif freq.lower().endswith(".png"): 27 | ftype = "image/png" 28 | elif freq.lower().endswith(".html"): 29 | ftype = "text/html" 30 | elif freq.lower().endswith(".otf"): 31 | ftype = "font/opentype" 32 | elif freq.lower().endswith(".woff"): 33 | ftype = "font/woff" 34 | elif freq.lower().endswith(".woff2"): 35 | ftype = "font/woff2" 36 | elif freq.lower().endswith(".ttf"): 37 | ftype = "font/ttf" 38 | elif freq.lower().endswith(".svg"): 39 | ftype = "image/svg+xml" 40 | elif freq.lower().endswith(".eot"): 41 | ftype = "application/vnd.ms-fontobject" 42 | elif freq.lower().endswith(".json"): 43 | ftype = "application/json" 44 | elif freq.lower().endswith(".xml"): 45 | ftype = "application/xml" 46 | else: 47 | ftype = "text/plain" 48 | 49 | with open(freq, 'rb') as f: 50 | return f.read(), ftype 51 | 52 | return None, None 53 | 54 | 55 | def inject_js(htmlsrc, *args): 56 | soup = Soup(htmlsrc, 'html.parser') 57 | 58 | for fpath in args: 59 | logger.debug("Inserting Javascript file: {}".format(fpath)) 60 | 61 | script = soup.new_tag('script') 62 | script['type'] = 'text/javascript' 63 | script['src'] = fpath 64 | soup.body.append(script) 65 | 66 | return str(soup) 67 | -------------------------------------------------------------------------------- /proxy/upload-beta.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo "Build and Push jasonacox/pypowerwall to Docker Hub" 3 | echo "" 4 | 5 | last_path=$(basename $PWD) 6 | if [ "$last_path" == "proxy" ]; then 7 | # Remove test link 8 | rm -rf pypowerwall 9 | cp -r ../pypowerwall . 10 | 11 | # Determine version 12 | PROXY=`grep "BUILD = " server.py | cut -d\" -f2` 13 | PYPOWERWALL=`echo -n "import pypowerwall 14 | print(pypowerwall.version)" | (cd ..; python3)` 15 | VER="${PYPOWERWALL}${PROXY}-beta${1}" 16 | 17 | # Check with user before proceeding 18 | echo "Build and push jasonacox/pypowerwall:${VER} to Docker Hub?" 19 | read -p "Press [Enter] to continue or Ctrl-C to cancel..." 20 | 21 | # Build jasonacox/pypowerwall:x.y.z 22 | echo "* BUILD jasonacox/pypowerwall:${VER}" 23 | docker buildx build -f Dockerfile.beta --no-cache --platform linux/amd64,linux/arm64,linux/arm/v7 --push -t jasonacox/pypowerwall:${VER} . 24 | echo "" 25 | 26 | # Verify 27 | echo "* VERIFY jasonacox/pypowerwall:${VER}" 28 | docker buildx imagetools inspect jasonacox/pypowerwall:${VER} | grep Platform 29 | echo "" 30 | echo "* VERIFY jasonacox/pypowerwall:latest" 31 | docker buildx imagetools inspect jasonacox/pypowerwall | grep Platform 32 | echo "" 33 | 34 | # Restore link for testing 35 | rm -rf pypowerwall 36 | ln -s ../pypowerwall pypowerwall 37 | 38 | else 39 | # Exit script if last_path is not "proxy" 40 | echo "Current directory is not 'proxy'." 41 | exit 0 42 | fi 43 | -------------------------------------------------------------------------------- /proxy/web/LICENSE: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Lodash 4 | * Copyright JS Foundation and other contributors 5 | * Released under MIT license 6 | * Based on Underscore.js 1.8.3 7 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 8 | */ 9 | 10 | /*! 11 | Copyright (c) 2017 Jed Watson. 12 | Licensed under the MIT License (MIT), see 13 | http://jedwatson.github.io/classnames 14 | */ 15 | 16 | /*! 17 | * jQuery JavaScript Library v2.2.4 18 | * http://jquery.com/ 19 | * 20 | * Includes Sizzle.js 21 | * http://sizzlejs.com/ 22 | * 23 | * Copyright jQuery Foundation and other contributors 24 | * Released under the MIT license 25 | * http://jquery.org/license 26 | * 27 | * Date: 2016-05-20T17:23Z 28 | */ 29 | 30 | /*! 31 | * Sizzle CSS Selector Engine v2.2.1 32 | * http://sizzlejs.com/ 33 | * 34 | * Copyright jQuery Foundation and other contributors 35 | * Released under the MIT license 36 | * http://jquery.org/license 37 | * 38 | * Date: 2015-10-17 39 | */ 40 | 41 | /* 42 | object-assign 43 | (c) Sindre Sorhus 44 | @license MIT 45 | */ 46 | 47 | /*! 48 | Copyright (c) 2018 Jed Watson. 49 | Licensed under the MIT License (MIT), see 50 | http://jedwatson.github.io/react-select 51 | */ 52 | 53 | /*! 54 | Copyright (c) 2016 Jed Watson. 55 | Licensed under the MIT License (MIT), see 56 | http://jedwatson.github.io/classnames 57 | */ 58 | 59 | /*! 60 | * Bootstrap v3.4.1 (https://getbootstrap.com/) 61 | * Copyright 2011-2019 Twitter, Inc. 62 | * Licensed under the MIT license 63 | */ 64 | 65 | /** @license React v16.13.1 66 | * react.production.min.js 67 | * 68 | * Copyright (c) Facebook, Inc. and its affiliates. 69 | * 70 | * This source code is licensed under the MIT license found in the 71 | * LICENSE file in the root directory of this source tree. 72 | */ 73 | 74 | /** @license React v16.13.1 75 | * react-dom.production.min.js 76 | * 77 | * Copyright (c) Facebook, Inc. and its affiliates. 78 | * 79 | * This source code is licensed under the MIT license found in the 80 | * LICENSE file in the root directory of this source tree. 81 | */ 82 | 83 | /** @license React v0.19.1 84 | * scheduler.production.min.js 85 | * 86 | * Copyright (c) Facebook, Inc. and its affiliates. 87 | * 88 | * This source code is licensed under the MIT license found in the 89 | * LICENSE file in the root directory of this source tree. 90 | */ 91 | 92 | /** @license React v16.10.1 93 | * react-is.production.min.js 94 | * 95 | * Copyright (c) Facebook, Inc. and its affiliates. 96 | * 97 | * This source code is licensed under the MIT license found in the 98 | * LICENSE file in the root directory of this source tree. 99 | */ 100 | 101 | /*! ***************************************************************************** 102 | Copyright (c) Microsoft Corporation. All rights reserved. 103 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 104 | this file except in compliance with the License. You may obtain a copy of the 105 | License at http://www.apache.org/licenses/LICENSE-2.0 106 | 107 | THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 108 | KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED 109 | WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, 110 | MERCHANTABLITY OR NON-INFRINGEMENT. 111 | 112 | See the Apache Version 2.0 License for specific language governing permissions 113 | and limitations under the License. 114 | ***************************************************************************** */ -------------------------------------------------------------------------------- /proxy/web/bogus/api.auth.toggle.supported.json: -------------------------------------------------------------------------------- 1 | {"toggle_auth_supported":true} 2 | -------------------------------------------------------------------------------- /proxy/web/bogus/api.customer.json: -------------------------------------------------------------------------------- 1 | {"registered":true} -------------------------------------------------------------------------------- /proxy/web/bogus/api.customer.registration.json: -------------------------------------------------------------------------------- 1 | {"privacy_notice":null,"limited_warranty":null,"grid_services":null,"marketing":null,"registered":true,"timed_out_registration":false} -------------------------------------------------------------------------------- /proxy/web/bogus/api.installer.json: -------------------------------------------------------------------------------- 1 | {"company":"Tesla","customer_id":"","phone":"","email":"","location":"","mounting":"","wiring":"","backup_configuration":"Whole Home","solar_installation":"New","solar_installation_type":"PV Panel","run_sitemaster":true,"verified_config":true,"installation_types":["Residential"]} 2 | -------------------------------------------------------------------------------- /proxy/web/bogus/api.meters.aggregates.json: -------------------------------------------------------------------------------- 1 | {"site":{"last_communication_time":"2023-12-16T08:33:19.496043714-08:00","instant_power":27,"instant_reactive_power":-223,"instant_apparent_power":224.62858233092243,"frequency":0,"energy_exported":4319958.270189472,"energy_imported":6800365.841005325,"instant_average_voltage":211.11967293457045,"instant_average_current":4.986000000000001,"i_a_current":0,"i_b_current":0,"i_c_current":0,"last_phase_voltage_communication_time":"0001-01-01T00:00:00Z","last_phase_power_communication_time":"0001-01-01T00:00:00Z","last_phase_energy_communication_time":"0001-01-01T00:00:00Z","timeout":1500000000,"num_meters_aggregated":1,"instant_total_current":4.986000000000001},"battery":{"last_communication_time":"2023-12-16T08:33:19.470811519-08:00","instant_power":-990,"instant_reactive_power":10,"instant_apparent_power":990.050503762308,"frequency":60.019000000000005,"energy_exported":12319540,"energy_imported":13853641,"instant_average_voltage":243.95,"instant_average_current":22.3,"i_a_current":0,"i_b_current":0,"i_c_current":0,"last_phase_voltage_communication_time":"0001-01-01T00:00:00Z","last_phase_power_communication_time":"0001-01-01T00:00:00Z","last_phase_energy_communication_time":"0001-01-01T00:00:00Z","timeout":1500000000,"num_meters_aggregated":2,"instant_total_current":22.3},"load":{"last_communication_time":"2023-12-16T08:33:19.470811519-08:00","instant_power":866.25,"instant_reactive_power":-202.75,"instant_apparent_power":889.6609607035705,"frequency":0,"energy_exported":0,"energy_imported":27290482.570815854,"instant_average_voltage":211.11967293457045,"instant_average_current":4.103123067401045,"i_a_current":0,"i_b_current":0,"i_c_current":0,"last_phase_voltage_communication_time":"0001-01-01T00:00:00Z","last_phase_power_communication_time":"0001-01-01T00:00:00Z","last_phase_energy_communication_time":"0001-01-01T00:00:00Z","timeout":1500000000,"instant_total_current":4.103123067401045},"solar":{"last_communication_time":"2023-12-16T08:33:19.478393964-08:00","instant_power":1840,"instant_reactive_power":0,"instant_apparent_power":1840,"frequency":60.016000000000005,"energy_exported":26344176,"energy_imported":0,"instant_average_voltage":243.5,"instant_average_current":7.553366174055829,"i_a_current":0,"i_b_current":0,"i_c_current":0,"last_phase_voltage_communication_time":"0001-01-01T00:00:00Z","last_phase_power_communication_time":"0001-01-01T00:00:00Z","last_phase_energy_communication_time":"0001-01-01T00:00:00Z","timeout":1000000000,"num_meters_aggregated":1,"instant_total_current":7.553366174055829}} -------------------------------------------------------------------------------- /proxy/web/bogus/api.meters.json: -------------------------------------------------------------------------------- 1 | [{"serial":"VAH1234AB1234","short_id":"73533","type":"neurio_w2_tcp","connected":true,"cts":[{"type":"solarRGM","valid":[true,false,false,false],"inverted":[false,false,false,false],"real_power_scale_factor":2}],"ip_address":"PWRview-73533","mac":"01-23-45-56-78-90"},{"serial":"JBL12345Y1F012synchrometerY","short_id":"1232100-00-E--TG123456789EGG","type":"synchrometerY"},{"serial":"JBL12345Y1F012synchrometerX","short_id":"1232100-00-E--TG123456789EGG","type":"synchrometerX","cts":[{"type":"site","valid":[true,true,false,false],"inverted":[false,false,false,false]}]}] 2 | -------------------------------------------------------------------------------- /proxy/web/bogus/api.meters.readings.json: -------------------------------------------------------------------------------- 1 | TIMEOUT! -------------------------------------------------------------------------------- /proxy/web/bogus/api.meters.site.json: -------------------------------------------------------------------------------- 1 | [{"id":0,"location":"site","type":"synchrometerX","cts":[true,true,false,false],"inverted":[false,false,false,false],"connection":{"short_id":"1232100-00-E--TG123456789E4G","device_serial":"JBL12345Y1F012synchrometerX","https_conf":{}},"Cached_readings":{"last_communication_time":"2023-12-16T11:48:34.135766872-08:00","instant_power":2495,"instant_reactive_power":-212,"instant_apparent_power":2503.9906149983867,"frequency":0,"energy_exported":4507438.170261594,"energy_imported":6995047.554439916,"instant_average_voltage":210.8945063295865,"instant_average_current":20.984,"i_a_current":13.3045,"i_b_current":7.6795,"i_c_current":0,"last_phase_voltage_communication_time":"2023-12-16T11:48:34.035339849-08:00","v_l1n":121.72,"v_l2n":121.78,"last_phase_power_communication_time":"2023-12-16T11:48:34.135766872-08:00","real_power_a":1584,"real_power_b":911,"reactive_power_a":-129,"reactive_power_b":-83,"last_phase_energy_communication_time":"0001-01-01T00:00:00Z","serial_number":"JBL12345Y1F012","version":"fa0c1ad02efda3","timeout":1500000000,"instant_total_current":20.984}}] 2 | -------------------------------------------------------------------------------- /proxy/web/bogus/api.meters.solar.json: -------------------------------------------------------------------------------- 1 | null -------------------------------------------------------------------------------- /proxy/web/bogus/api.networks.json: -------------------------------------------------------------------------------- 1 | [{"network_name":"ethernet_tesla_internal_default","interface":"EthType","enabled":true,"dhcp":true,"extra_ips":[{"ip":"192.168.90.2","netmask":24}],"active":true,"primary":true,"lastTeslaConnected":true,"lastInternetConnected":true,"iface_network_info":{"network_name":"ethernet_tesla_internal_default","ip_networks":[{"IP":"","Mask":"////AA=="}],"gateway":"","interface":"EthType","state":"DeviceStateReady","state_reason":"DeviceStateReasonNone","signal_strength":0,"hw_address":""}},{"network_name":"gsm_tesla_internal_default","interface":"GsmType","enabled":true,"dhcp":null,"active":true,"primary":false,"lastTeslaConnected":false,"lastInternetConnected":false,"iface_network_info":{"network_name":"gsm_tesla_internal_default","ip_networks":[{"IP":"","Mask":"/////w=="}],"gateway":"","interface":"GsmType","state":"DeviceStateReady","state_reason":"DeviceStateReasonNone","signal_strength":71,"hw_address":""}}] -------------------------------------------------------------------------------- /proxy/web/bogus/api.operation.json: -------------------------------------------------------------------------------- 1 | {"real_mode":"self_consumption","backup_reserve_percent":81,"freq_shift_load_shed_soe":65,"freq_shift_load_shed_delta_f":-0.32} -------------------------------------------------------------------------------- /proxy/web/bogus/api.powerwalls.json: -------------------------------------------------------------------------------- 1 | {"enumerating":false,"updating":false,"checking_if_offgrid":false,"running_phase_detection":false,"phase_detection_last_error":"no phase information","bubble_shedding":false,"on_grid_check_error":"on grid check not run","grid_qualifying":false,"grid_code_validating":false,"phase_detection_not_available":true,"powerwalls":[{"Type":"","PackagePartNumber":"2012170-25-E","PackageSerialNumber":"TG1234567890G1","type":"SolarPowerwall","grid_state":"Grid_Uncompliant","grid_reconnection_time_seconds":0,"under_phase_detection":false,"updating":false,"commissioning_diagnostic":{"name":"Commissioning","category":"InternalComms","disruptive":false,"inputs":null,"checks":[{"name":"CAN connectivity","status":"fail","start_time":"2023-12-16T08:34:17.3068631-08:00","end_time":"2023-12-16T08:34:17.3068696-08:00","message":"Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.","results":{},"debug":{},"checks":null},{"name":"Enable switch","status":"fail","start_time":"2023-12-16T08:34:17.306875474-08:00","end_time":"2023-12-16T08:34:17.306880724-08:00","message":"Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.","results":{},"debug":{},"checks":null},{"name":"Internal communications","status":"fail","start_time":"2023-12-16T08:34:17.306886099-08:00","end_time":"2023-12-16T08:34:17.306891223-08:00","message":"Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.","results":{},"debug":{},"checks":null},{"name":"Firmware up-to-date","status":"fail","start_time":"2023-12-16T08:34:17.306896598-08:00","end_time":"2023-12-16T08:34:17.306901723-08:00","message":"Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.","results":{},"debug":{},"checks":null}],"alert":false},"update_diagnostic":{"name":"Firmware Update","category":"InternalComms","disruptive":true,"inputs":null,"checks":[{"name":"Solar Inverter firmware","status":"not_run","start_time":null,"end_time":null,"progress":0,"results":null,"debug":null,"checks":null},{"name":"Solar Safety firmware","status":"not_run","start_time":null,"end_time":null,"progress":0,"results":null,"debug":null,"checks":null},{"name":"Grid code","status":"not_run","start_time":null,"end_time":null,"progress":0,"results":null,"debug":null,"checks":null},{"name":"Powerwall firmware","status":"not_run","start_time":null,"end_time":null,"progress":0,"results":null,"debug":null,"checks":null},{"name":"Battery firmware","status":"not_run","start_time":null,"end_time":null,"progress":0,"results":null,"debug":null,"checks":null},{"name":"Inverter firmware","status":"not_run","start_time":null,"end_time":null,"progress":0,"results":null,"debug":null,"checks":null},{"name":"Grid code","status":"not_run","start_time":null,"end_time":null,"progress":0,"results":null,"debug":null,"checks":null}],"alert":false},"bc_type":null,"in_config":true},{"Type":"","PackagePartNumber":"3012170-05-B","PackageSerialNumber":"TG1234567890G1","type":"ACPW","grid_state":"Grid_Uncompliant","grid_reconnection_time_seconds":0,"under_phase_detection":false,"updating":false,"commissioning_diagnostic":{"name":"Commissioning","category":"InternalComms","disruptive":false,"inputs":null,"checks":[{"name":"CAN connectivity","status":"fail","start_time":"2023-12-16T08:34:17.320856307-08:00","end_time":"2023-12-16T08:34:17.320940302-08:00","message":"Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.","results":{},"debug":{},"checks":null},{"name":"Enable switch","status":"fail","start_time":"2023-12-16T08:34:17.320949301-08:00","end_time":"2023-12-16T08:34:17.320955301-08:00","message":"Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.","results":{},"debug":{},"checks":null},{"name":"Internal communications","status":"fail","start_time":"2023-12-16T08:34:17.320960676-08:00","end_time":"2023-12-16T08:34:17.320966176-08:00","message":"Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.","results":{},"debug":{},"checks":null},{"name":"Firmware up-to-date","status":"fail","start_time":"2023-12-16T08:34:17.32097155-08:00","end_time":"2023-12-16T08:34:17.3209768-08:00","message":"Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.","results":{},"debug":{},"checks":null}],"alert":false},"update_diagnostic":{"name":"Firmware Update","category":"InternalComms","disruptive":true,"inputs":null,"checks":[{"name":"Powerwall firmware","status":"not_run","start_time":null,"end_time":null,"progress":0,"results":null,"debug":null,"checks":null},{"name":"Battery firmware","status":"not_run","start_time":null,"end_time":null,"progress":0,"results":null,"debug":null,"checks":null},{"name":"Inverter firmware","status":"not_run","start_time":null,"end_time":null,"progress":0,"results":null,"debug":null,"checks":null},{"name":"Grid code","status":"not_run","start_time":null,"end_time":null,"progress":0,"results":null,"debug":null,"checks":null}],"alert":false},"bc_type":null,"in_config":true}],"gateway_din":"1232100-00-E--TG1234567890G1","sync":{"updating":false,"commissioning_diagnostic":{"name":"Commissioning","category":"InternalComms","disruptive":false,"inputs":null,"checks":[{"name":"CAN connectivity","status":"fail","start_time":"2023-12-16T08:34:17.321101293-08:00","end_time":"2023-12-16T08:34:17.321107918-08:00","message":"Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.","results":{},"debug":{},"checks":null},{"name":"Firmware up-to-date","status":"fail","start_time":"2023-12-16T08:34:17.321113792-08:00","end_time":"2023-12-16T08:34:17.321118917-08:00","message":"Cannot perform this action with site controller running. From landing page, either \"STOP SYSTEM\" or \"RUN WIZARD\" to proceed.","results":{},"debug":{},"checks":null}],"alert":false},"update_diagnostic":{"name":"Firmware Update","category":"InternalComms","disruptive":true,"inputs":null,"checks":[{"name":"Synchronizer firmware","status":"not_run","start_time":null,"end_time":null,"progress":0,"results":null,"debug":null,"checks":null},{"name":"Islanding configuration","status":"not_run","start_time":null,"end_time":null,"progress":0,"results":null,"debug":null,"checks":null},{"name":"Grid code","status":"not_run","start_time":null,"end_time":null,"progress":0,"results":null,"debug":null,"checks":null}],"alert":false}},"msa":null,"states":null} 2 | -------------------------------------------------------------------------------- /proxy/web/bogus/api.site_info.grid_codes.json: -------------------------------------------------------------------------------- 1 | TIMEOUT! -------------------------------------------------------------------------------- /proxy/web/bogus/api.site_info.json: -------------------------------------------------------------------------------- 1 | {"max_system_energy_kWh":27,"max_system_power_kW":10.8,"site_name":"Tesla Energy Gateway","timezone":"America/Los_Angeles","max_site_meter_power_kW":1000000000,"min_site_meter_power_kW":-1000000000,"nominal_system_energy_kWh":27,"nominal_system_power_kW":10.8,"panel_max_current":100,"grid_code":{"grid_code":"60Hz_240V_s_UL1741SA:2019_California","grid_voltage_setting":240,"grid_freq_setting":60,"grid_phase_setting":"Split","country":"United States","state":"California","utility":"Southern California Edison"}} -------------------------------------------------------------------------------- /proxy/web/bogus/api.site_info.site_name.json: -------------------------------------------------------------------------------- 1 | {"site_name":"Tesla Energy Gateway","timezone":"America/Los_Angeles"} 2 | -------------------------------------------------------------------------------- /proxy/web/bogus/api.sitemaster.json: -------------------------------------------------------------------------------- 1 | {"status":"StatusUp","running":true,"connected_to_tesla":true,"power_supply_mode":false,"can_reboot":"Yes"} -------------------------------------------------------------------------------- /proxy/web/bogus/api.solars.brands.json: -------------------------------------------------------------------------------- 1 | ["ABB","Ablerex Electronics","Advanced Energy Industries","Advanced Solar Photonics","AE Solar Energy","AEconversion Gmbh","AEG Power Solutions","Aero-Sharp","Afore New Energy Technology Shanghai Co","Agepower Limit","Alpha ESS Co","Alpha Technologies","Altenergy Power System","American Electric Technologies","AMETEK Solidstate Control","Andalay Solar","Apparent","Asian Power Devices","AU Optronics","Auxin Solar","Ballard Power Systems","Beacon Power","Beijing Hua Xin Liu He Investment (Australia) Pty","Beijing Kinglong New Energy","Bergey Windpower","Beyond Building Group","Beyond Building Systems","BYD Auto Industry Company Limited","Canadian Solar","Carlo Gavazzi","CFM Equipment Distributors","Changzhou Nesl Solartech","Chiconypower","Chilicon","Chilicon Power","Chint Power Systems America","Chint Solar Zhejiang","Concept by US","Connect Renewable Energy","Danfoss","Danfoss Solar","Darfon Electronics","DASS tech","Delta Energy Systems","Destin Power","Diehl AKO Stiftung","Diehl AKO Stiftung \u0026 KG","Direct Grid Technologies","Dow Chemical","DYNAPOWER COMPANY","E-Village Solar","EAST GROUP CO LTD","Eaton","Eguana Technologies","Elettronica Santerno","Eltek","Emerson Network Power","Enecsys","Energy Storage Australia Pty","EnluxSolar","Enphase Energy","Eoplly New Energy Technology","EPC Power","ET Solar Industry","ETM Electromatic","Exeltech","Flextronics Industrial","Flextronics International USA","Fronius","FSP Group","GAF","GE Energy","Gefran","Geoprotek","Global Mainstream Dynamic Energy Technology","Green Power Technologies","GreenVolts","GridPoint","Growatt","Gsmart Ningbo Energy Storage Technology Co","Guangzhou Sanjing Electric Co","Hangzhou Sunny Energy Science and Technology Co","Hansol Technics","Hanwha Q CELLS \u0026 Advanced Materials Corporation","Heart Transverter","Helios","HiQ Solar","HiSEL Power","Home Director","Hoymiles Converter Technology","Huawei Technologies","Huawei Technologies Co","HYOSUNG","i-Energy Corporation","Ideal Power","Ideal Power Converters","IMEON ENERGY","Ingeteam","Involar","INVOLAR","INVT Solar Technology Shenzhen Co","iPower","IST Energy","Jema Energy","Jiangsu GoodWe Power Supply Technology Co","Jiangsu Weiheng Intelligent Technology Co","Jiangsu Zeversolar New Energy","Jiangsu Zeversolar New Energy Co","Jiangyin Hareon Power","Jinko Solar","KACO","Kehua Hengsheng Co","Kostal Solar Electric","LeadSolar Energy","Leatec Fine Ceramics","LG Electronics","Lixma Tech","Mage Solar","Mage Solar USA","Mariah Power","MIL-Systems","Ming Shen Energy Technology","Mohr Power","Motech Industries","NeoVolta","Nextronex Energy Systems","Nidec ASI","Ningbo Ginlong Technologies","Ningbo Ginlong Technologies Co","Northern Electric","ONE SUN MEXICO DE C.V.","Open Energy","OPTI International","OPTI-Solar","OutBack Power Technologies","Panasonic Corporation Eco Solutions Company","Perfect Galaxy","Petra Solar","Petra Systems","Phoenixtec Power","Phono Solar Technology","Pika Energy","Power Electronics","Power-One","Powercom","PowerWave Energy Pty","Princeton Power Systems","PurpleRubik New Energy Technology Co","PV Powered","Redback Technologies Limited","RedEarth Energy Storage Pty","REFU Elektronik","Renac Power Technology Co","Renergy","Renesola Zhejiang","Renovo Power Systems","Resonix","Rhombus Energy Solutions","Ritek Corporation","Sainty Solar","Samil Power","SanRex","SANYO","Sapphire Solar Pty","Satcon Technology","SatCon Technology","Schneider","Schneider Inverters USA","Schuco USA","Selectronic Australia","Senec GmbH","Shanghai Sermatec Energy Technology Co","Shanghai Trannergy Power Electronics Co","Sharp","Shenzhen BYD","Shenzhen Growatt","Shenzhen Growatt Co","Shenzhen INVT Electric Co","SHENZHEN KSTAR NEW ENERGY COMPANY LIMITED","Shenzhen Litto New Energy Co","Shenzhen Sinexcel Electric","Shenzhen Sinexcel Electric Co","Shenzhen SOFARSOLAR Co","Siemens Industry","Silicon Energy","Sineng Electric Co","SMA","Sol-Ark","Solar Juice Pty","Solar Liberty","Solar Power","Solarbine","SolarBridge Technologies","SolarCity","SolarEdge Technologies","Solargate","Solaria Corporation","Solarmax","SolarWorld","SolaX Power Co","SolaX Power Network Technology (Zhe jiang)","SolaX Power Network Technology Zhejiang Co","Solectria Renewables","Solis","Sonnen GmbH","Sonnetek","Southwest Windpower","Sparq Systems","Sputnik Engineering","STARFISH HERO CO","Sungrow Power Supply","Sungrow Power Supply Co","Sunna Tech","SunPower","SunPower (Original Mfr.Fronius)","Sunset","Sustainable Energy Technologies","Sustainable Solar Services","Suzhou Hypontech Co","Suzhou Solarwii Micro Grid Technology Co","Sysgration","Tabuchi Electric","Talesun Solar","Tesla","The Trustee for Soltaro Unit Trust","TMEIC","TOPPER SUN Energy Tech","Toshiba International","Trannergy","Trina Energy Storage Solutions (Jiangsu)","Trina Energy Storage Solutions Jiangsu Co","Trina Solar Co","Ubiquiti Networks International","United Renewable Energy Co","Westinghouse Solar","Windterra Systems","Xantrex Technology","Xiamen Kehua Hengsheng","Xiamen Kehua Hengsheng Co","Xslent Energy Technologies","Yaskawa Solectria Solar","Yes! Solar","Zhongli Talesun Solar","ZIGOR","シャープ (Sharp)","パナソニック (Panasonic)","三菱電機 (Mitsubishi)","京セラ (Kyocera)","東芝 (Toshiba)","長州産業 (Choshu Sangyou)","カナディアン ソーラー -------------------------------------------------------------------------------- /proxy/web/bogus/api.solars.json: -------------------------------------------------------------------------------- 1 | [{"brand":"Tesla","model":"Solar Inverter 7.6","power_rating_watts":7600}] 2 | -------------------------------------------------------------------------------- /proxy/web/bogus/api.status.json: -------------------------------------------------------------------------------- 1 | {"din":"1232100-00-E--TG1234567890G1","start_time":"2023-10-13 04:01:45 +0800","up_time_seconds":"1541h38m20.998412744s","is_new":false,"version":"23.28.2 27626f98","git_hash":"27626f98a66cad5c665bbe1d4d788cdb3e94fd33","commission_count":0,"device_type":"teg","teg_type":"unknown","sync_type":"v2.1","cellular_disabled":false,"can_reboot":true} -------------------------------------------------------------------------------- /proxy/web/bogus/api.synchrometer.ct_voltage_references.json: -------------------------------------------------------------------------------- 1 | {"ct1":"Phase1","ct2":"Phase2","ct3":"Phase1"} 2 | -------------------------------------------------------------------------------- /proxy/web/bogus/api.system.networks.json: -------------------------------------------------------------------------------- 1 | TIMEOUT! -------------------------------------------------------------------------------- /proxy/web/bogus/api.system.update.status.json: -------------------------------------------------------------------------------- 1 | {"state":"/update_succeeded","info":{"status":["nonactionable"]},"current_time":1702756114429,"last_status_time":1702753309227,"version":"23.28.2 27626f98","offline_updating":false,"offline_update_error":"","estimated_bytes_per_second":null} -------------------------------------------------------------------------------- /proxy/web/bogus/api.system_status.grid_faults.json: -------------------------------------------------------------------------------- 1 | [] -------------------------------------------------------------------------------- /proxy/web/bogus/api.system_status.grid_status.json: -------------------------------------------------------------------------------- 1 | {"grid_status":"SystemGridConnected","grid_services_active":false} -------------------------------------------------------------------------------- /proxy/web/bogus/api.system_status.grid_status.json-offline: -------------------------------------------------------------------------------- 1 | {"grid_status":"SystemIslandedActive","grid_services_active":true} -------------------------------------------------------------------------------- /proxy/web/bogus/api.system_status.grid_status.json-transition: -------------------------------------------------------------------------------- 1 | {"grid_status":"SystemTransitionToGrid","grid_services_active":false} -------------------------------------------------------------------------------- /proxy/web/bogus/api.system_status.json: -------------------------------------------------------------------------------- 1 | {"command_source":"Configuration","battery_target_power":-3866.6666666666665,"battery_target_reactive_power":0,"nominal_full_pack_energy":25995,"nominal_energy_remaining":16693,"max_power_energy_remaining":0,"max_power_energy_to_be_charged":0,"max_charge_power":10800,"max_discharge_power":10800,"max_apparent_power":10800,"instantaneous_max_discharge_power":24000,"instantaneous_max_charge_power":14000,"instantaneous_max_apparent_power":11520,"hardware_capability_charge_power":0,"hardware_capability_discharge_power":0,"grid_services_power":-0,"system_island_state":"SystemGridConnected","available_blocks":2,"available_charger_blocks":0,"battery_blocks":[{"Type":"","PackagePartNumber":"2012170-25-E","PackageSerialNumber":"TG123456789012","disabled_reasons":[],"pinv_state":"PINV_GridFollowing","pinv_grid_state":"Grid_Compliant","nominal_energy_remaining":8528,"nominal_full_pack_energy":13305,"p_out":-1990,"q_out":20,"v_out":243.70000000000002,"f_out":60.007999999999996,"i_out":40.900000000000006,"energy_charged":7036162,"energy_discharged":6249327,"off_grid":false,"vf_mode":false,"wobble_detected":false,"charge_power_clamped":false,"backup_ready":true,"OpSeqState":"Active","version":"27626f98a66cad"},{"Type":"","PackagePartNumber":"3012170-05-B","PackageSerialNumber":"TG123456789012","disabled_reasons":[],"pinv_state":"PINV_GridFollowing","pinv_grid_state":"Grid_Compliant","nominal_energy_remaining":8165,"nominal_full_pack_energy":12690,"p_out":-1890.0000000000002,"q_out":20,"v_out":243.70000000000002,"f_out":60.007999999999996,"i_out":39.400000000000006,"energy_charged":6835113,"energy_discharged":6076726,"off_grid":false,"vf_mode":false,"wobble_detected":false,"charge_power_clamped":false,"backup_ready":true,"OpSeqState":"Active","version":"27626f98a66cad"}],"ffr_power_availability_high":11600,"ffr_power_availability_low":11600,"load_charge_constraint":0,"max_sustained_ramp_rate":2700000,"grid_faults":[],"can_reboot":"Yes","smart_inv_delta_p":0,"smart_inv_delta_q":0,"last_toggle_timestamp":"2023-10-13T04:08:05.957195-07:00","solar_real_power_limit":3909.4999669342515,"score":10000,"blocks_controlled":2,"primary":true,"auxiliary_load":0,"all_enable_lines_high":true,"inverter_nominal_usable_power":11600,"expected_energy_remaining":0} 2 | -------------------------------------------------------------------------------- /proxy/web/bogus/api.system_status.soe.json: -------------------------------------------------------------------------------- 1 | {"percentage": 20.109166592431226} -------------------------------------------------------------------------------- /proxy/web/bogus/api.troubleshooting.problems.json: -------------------------------------------------------------------------------- 1 | {"problems":[]} -------------------------------------------------------------------------------- /proxy/web/box.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/box.png -------------------------------------------------------------------------------- /proxy/web/example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | pyPowerwall Proxy iFrame Example 5 | 6 | 7 | 8 |

Tesla Powerwall Power Flows

9 |

iFrame Example

10 | 11 | 14 |

Firmware

15 | 16 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /proxy/web/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/favicon-16x16.png -------------------------------------------------------------------------------- /proxy/web/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/favicon-32x32.png -------------------------------------------------------------------------------- /proxy/web/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/favicon.ico -------------------------------------------------------------------------------- /proxy/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tesla Energy - Setup 5 | 6 | 7 | 8 | 9 | 13 | 40 | 41 | 42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 | -------------------------------------------------------------------------------- /proxy/web/viz-static/012955c70685614a5639d326f41890bd.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/012955c70685614a5639d326f41890bd.png -------------------------------------------------------------------------------- /proxy/web/viz-static/124f233cfa9945f861dcaca7acedd308.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/124f233cfa9945f861dcaca7acedd308.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/230aeae00823cd3b622d093948d9c433.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/230aeae00823cd3b622d093948d9c433.png -------------------------------------------------------------------------------- /proxy/web/viz-static/2bf15a1686c7a1bf7b577337a07d7049.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/2bf15a1686c7a1bf7b577337a07d7049.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/40.17c71172308436a079d1.js: -------------------------------------------------------------------------------- 1 | (window.webpackJsonp = window.webpackJsonp || []).push([[40], { 1062: function (n, w) {} }]); 2 | -------------------------------------------------------------------------------- /proxy/web/viz-static/448c34a56d699c29117adc64c43affeb.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/448c34a56d699c29117adc64c43affeb.woff2 -------------------------------------------------------------------------------- /proxy/web/viz-static/4e28cc8f2bdf3ba640331daa2c453341.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/4e28cc8f2bdf3ba640331daa2c453341.png -------------------------------------------------------------------------------- /proxy/web/viz-static/653969a51632a4df33358a39d7012f79.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/653969a51632a4df33358a39d7012f79.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/722c5f898bbca8b2eb3fce0287688326.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/722c5f898bbca8b2eb3fce0287688326.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/86a6894da889a3db781418529403290f.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/86a6894da889a3db781418529403290f.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/89aec2cc0b804667e95b1adc02e1ac4a.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/89aec2cc0b804667e95b1adc02e1ac4a.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/a3b0d611359e6fa8356cd88aa9035268.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/a3b0d611359e6fa8356cd88aa9035268.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/ac2944015a17576924af7c56d88751cb.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/ac2944015a17576924af7c56d88751cb.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/b8d72cb0ef934ba1fe847c692d9dfed1.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/b8d72cb0ef934ba1fe847c692d9dfed1.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/bceda3fae660177ae570735feec62811.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/bceda3fae660177ae570735feec62811.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/befdfda70624c396169873b05de57f8a.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/befdfda70624c396169873b05de57f8a.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/black.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Clear IndexedDB to prevent auth hangup in the proxied Powerwall web app. 4 | try { 5 | window.indexedDB.databases().then((dbs) => { 6 | dbs.forEach(db => { window.indexedDB.deleteDatabase(db.name) }); 7 | }); 8 | } catch (error) { 9 | document.write("Browser blocking indexedDB - Turn off incognito mode."); 10 | } 11 | 12 | function injectScriptAndUse() { 13 | return new Promise((resolve, reject) => { 14 | var body = document.getElementsByTagName("body")[0]; 15 | var script = document.createElement("script"); 16 | script.src = "//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"; 17 | script.onload = function () { 18 | resolve(); 19 | }; 20 | body.appendChild(script); 21 | }); 22 | } 23 | 24 | injectScriptAndUse().then(() => { 25 | console.log("Applying Black customization"); 26 | triggerOnMutation(formatPowerwallForBlack); 27 | }); 28 | 29 | function triggerOnMutation(cb) { 30 | // Create an observer instance 31 | var observer = new MutationObserver(function (mutations) { 32 | mutations.forEach(function (mutation) { 33 | var newNodes = mutation.addedNodes; // DOM NodeList 34 | if (newNodes !== null) { // If there are new nodes added 35 | if (cb) cb(); 36 | } 37 | }); 38 | }); 39 | 40 | // Configuration of the observer: 41 | var config = { 42 | attributes: true, 43 | childList: true, 44 | subtree: true, 45 | }; 46 | 47 | var target = $("#root")[0]; 48 | 49 | // Pass in the target node, as well as the observer options 50 | observer.observe(target, config); 51 | } 52 | 53 | function formatPowerwallForBlack() { 54 | // Hide elements. 55 | $('.overview-menu, #logout, .footer, .compact-btn-row, .toast-list, .power-flow-header, .btn').hide(); 56 | 57 | // Set alignment 58 | $('.core-layout__viewport').css({ 59 | padding: 0, 60 | margin: 0, 61 | }); 62 | 63 | $('.power-flow-header').css({ 64 | "padding-top": 0, 65 | }); 66 | 67 | $('.power-flow-grid').css({ 68 | width: "100%", 69 | left: 0, 70 | right: 0, 71 | margin: 0, 72 | "padding-top": 0, 73 | "position": "fixed", 74 | }); 75 | 76 | $('.app').css({ 77 | "overflow-y": "hidden", 78 | }); 79 | 80 | // Set colors 81 | $('body').css({ 82 | "background-color": "black", 83 | }); 84 | 85 | $('.power-flow-grid.active').css({ 86 | "background-color": "#000", 87 | }); 88 | } -------------------------------------------------------------------------------- /proxy/web/viz-static/cb0da8a8999c06735455bf5056a5cd78.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/cb0da8a8999c06735455bf5056a5cd78.png -------------------------------------------------------------------------------- /proxy/web/viz-static/clear.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Clear IndexedDB to prevent auth hangup in the proxied Powerwall web app. 4 | try { 5 | window.indexedDB.databases().then((dbs) => { 6 | dbs.forEach(db => { window.indexedDB.deleteDatabase(db.name) }); 7 | }); 8 | } catch (error) { 9 | document.write("Browser blocking indexedDB - Turn off incognito mode."); 10 | } 11 | 12 | function injectScriptAndUse() { 13 | return new Promise((resolve, reject) => { 14 | var body = document.getElementsByTagName("body")[0]; 15 | var script = document.createElement("script"); 16 | script.src = "//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"; 17 | script.onload = function () { 18 | resolve(); 19 | }; 20 | body.appendChild(script); 21 | }); 22 | } 23 | 24 | injectScriptAndUse().then(() => { 25 | console.log("Applying Clear customization"); 26 | triggerOnMutation(formatPowerwallForClear); 27 | }); 28 | 29 | function triggerOnMutation(cb) { 30 | // Create an observer instance 31 | var observer = new MutationObserver(function (mutations) { 32 | mutations.forEach(function (mutation) { 33 | var newNodes = mutation.addedNodes; // DOM NodeList 34 | if (newNodes !== null) { // If there are new nodes added 35 | if (cb) cb(); 36 | } 37 | }); 38 | }); 39 | 40 | // Configuration of the observer: 41 | var config = { 42 | attributes: true, 43 | childList: true, 44 | subtree: true, 45 | }; 46 | 47 | var target = $("#root")[0]; 48 | 49 | // Pass in the target node, as well as the observer options 50 | observer.observe(target, config); 51 | } 52 | 53 | function formatPowerwallForClear() { 54 | // Hide elements. 55 | $('.overview-menu, #logout, .footer, .compact-btn-row, .toast-list, .power-flow-header, .btn').hide(); 56 | 57 | // Set alignment 58 | $('.core-layout__viewport').css({ 59 | padding: 0, 60 | margin: 0, 61 | }); 62 | 63 | $('.power-flow-header').css({ 64 | "padding-top": 0, 65 | }); 66 | 67 | $('.power-flow-grid').css({ 68 | width: "100%", 69 | left: 0, 70 | right: 0, 71 | margin: 0, 72 | "padding-top": 0, 73 | "position": "fixed", 74 | }); 75 | 76 | $('.app').css({ 77 | "overflow-y": "hidden", 78 | }); 79 | 80 | // Set colors 81 | $('body').css({ 82 | "background-color": "transparent", 83 | }); 84 | 85 | $('.power-flow-grid.active').css({ 86 | "background-color": "transparent", 87 | }); 88 | } 89 | -------------------------------------------------------------------------------- /proxy/web/viz-static/d859fee2eba0e67c75c4c92e719d0630.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/d859fee2eba0e67c75c4c92e719d0630.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/dakboard.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Clear IndexedDB to prevent auth hangup in the proxied Powerwall web app. 4 | try { 5 | window.indexedDB.databases().then((dbs) => { 6 | dbs.forEach(db => { window.indexedDB.deleteDatabase(db.name) }); 7 | }); 8 | } catch (error) { 9 | document.write("Browser blocking indexedDB - Turn off incognito mode."); 10 | } 11 | 12 | function injectScriptAndUse() { 13 | return new Promise((resolve, reject) => { 14 | var body = document.getElementsByTagName("body")[0]; 15 | var script = document.createElement("script"); 16 | script.src = "//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"; 17 | script.onload = function () { 18 | resolve(); 19 | }; 20 | body.appendChild(script); 21 | }); 22 | } 23 | 24 | injectScriptAndUse().then(() => { 25 | console.log("Applying Dakboard customization"); 26 | triggerOnMutation(formatPowerwallForDakboard); 27 | }); 28 | 29 | function triggerOnMutation(cb) { 30 | // Create an observer instance 31 | var observer = new MutationObserver(function (mutations) { 32 | mutations.forEach(function (mutation) { 33 | var newNodes = mutation.addedNodes; // DOM NodeList 34 | if (newNodes !== null) { // If there are new nodes added 35 | if (cb) cb(); 36 | } 37 | }); 38 | }); 39 | 40 | // Configuration of the observer: 41 | var config = { 42 | attributes: true, 43 | childList: true, 44 | subtree: true, 45 | }; 46 | 47 | var target = $("#root")[0]; 48 | 49 | // Pass in the target node, as well as the observer options 50 | observer.observe(target, config); 51 | } 52 | 53 | function formatPowerwallForDakboard() { 54 | // Hide elements. 55 | $('.overview-menu, #logout, .footer, .compact-btn-row, .toast-list, .power-flow-header, .btn').hide(); 56 | 57 | // Set alignment 58 | $('.core-layout__viewport').css({ 59 | padding: 0, 60 | margin: 0, 61 | }); 62 | 63 | $('.power-flow-header').css({ 64 | "padding-top": 0, 65 | }); 66 | 67 | $('.power-flow-grid').css({ 68 | width: "100%", 69 | left: 0, 70 | right: 0, 71 | margin: 0, 72 | "padding-top": 0, 73 | "position": "fixed", 74 | }); 75 | 76 | $('.app').css({ 77 | "overflow-y": "hidden", 78 | }); 79 | 80 | // Set colors 81 | $('body').css({ 82 | "background-color": "black", 83 | }); 84 | 85 | $('.power-flow-grid.active').css({ 86 | "background-color": "#000", 87 | }); 88 | } -------------------------------------------------------------------------------- /proxy/web/viz-static/e19c20e966bde501f94e41cd0322dbe8.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/e19c20e966bde501f94e41cd0322dbe8.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/ec6b35b07448e1624cb09323b5fb6e32.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/ec6b35b07448e1624cb09323b5fb6e32.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/ec89c09b066f57efc7687540c998845b.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/ec89c09b066f57efc7687540c998845b.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/eca1317ee8a99162d0d0e2df77330cec.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/eca1317ee8a99162d0d0e2df77330cec.otf -------------------------------------------------------------------------------- /proxy/web/viz-static/f4769f9bdb7466be65088239c12046d1.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/proxy/web/viz-static/f4769f9bdb7466be65088239c12046d1.eot -------------------------------------------------------------------------------- /proxy/web/viz-static/grafana-dark.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Grafana Dark Theme 4 | // Clear IndexedDB to prevent auth hangup in the proxied Powerwall web app. 5 | try { 6 | window.indexedDB.databases().then((dbs) => { 7 | dbs.forEach(db => { window.indexedDB.deleteDatabase(db.name) }); 8 | }); 9 | } catch (error) { 10 | document.write("Browser blocking indexedDB - Turn off incognito mode."); 11 | } 12 | 13 | function injectScriptAndUse() { 14 | return new Promise((resolve, reject) => { 15 | var body = document.getElementsByTagName("body")[0]; 16 | var script = document.createElement("script"); 17 | script.src = "//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"; 18 | script.onload = function () { 19 | resolve(); 20 | }; 21 | body.appendChild(script); 22 | }); 23 | } 24 | 25 | injectScriptAndUse().then(() => { 26 | console.log("Applying Grafana customization"); 27 | triggerOnMutation(formatPowerwallForGrafana); 28 | }); 29 | 30 | function triggerOnMutation(cb) { 31 | // Create an observer instance 32 | var observer = new MutationObserver(function (mutations) { 33 | mutations.forEach(function (mutation) { 34 | var newNodes = mutation.addedNodes; // DOM NodeList 35 | if (newNodes !== null) { // If there are new nodes added 36 | if (cb) cb(); 37 | } 38 | }); 39 | }); 40 | 41 | // Configuration of the observer: 42 | var config = { 43 | attributes: true, 44 | childList: true, 45 | subtree: true, 46 | }; 47 | 48 | var target = $("#root")[0]; 49 | 50 | // Pass in the target node, as well as the observer options 51 | observer.observe(target, config); 52 | } 53 | 54 | function formatPowerwallForGrafana() { 55 | // Hide elements. 56 | $('.overview-menu, #logout, .footer, .compact-btn-row, .toast-list, .power-flow-header, .btn').hide(); 57 | 58 | // Set alignment 59 | $('.core-layout__viewport').css({ 60 | padding: 0, 61 | margin: 0, 62 | }); 63 | 64 | $('.power-flow-header').css({ 65 | "padding-top": 0, 66 | }); 67 | 68 | $('.power-flow-grid').css({ 69 | width: "100%", 70 | left: 0, 71 | right: 0, 72 | margin: 0, 73 | "padding-top": 0, 74 | "position": "fixed", 75 | }); 76 | 77 | $('.app').css({ 78 | "overflow-y": "hidden", 79 | }); 80 | 81 | // Set colors 82 | $('body').css({ 83 | "background-color": "#111217", 84 | }); 85 | 86 | $('.power-flow-grid.active').css({ 87 | "background-color": "#111217", 88 | }); 89 | } -------------------------------------------------------------------------------- /proxy/web/viz-static/grafana.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Clear IndexedDB to prevent auth hangup in the proxied Powerwall web app. 4 | try { 5 | window.indexedDB.databases().then((dbs) => { 6 | dbs.forEach(db => { window.indexedDB.deleteDatabase(db.name) }); 7 | }); 8 | } catch (error) { 9 | document.write("Browser blocking indexedDB - Turn off incognito mode."); 10 | } 11 | 12 | function injectScriptAndUse() { 13 | return new Promise((resolve, reject) => { 14 | var body = document.getElementsByTagName("body")[0]; 15 | var script = document.createElement("script"); 16 | script.src = "//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"; 17 | script.onload = function () { 18 | resolve(); 19 | }; 20 | body.appendChild(script); 21 | }); 22 | } 23 | 24 | injectScriptAndUse().then(() => { 25 | console.log("Applying Grafana customization"); 26 | triggerOnMutation(formatPowerwallForGrafana); 27 | }); 28 | 29 | function triggerOnMutation(cb) { 30 | // Create an observer instance 31 | var observer = new MutationObserver(function (mutations) { 32 | mutations.forEach(function (mutation) { 33 | var newNodes = mutation.addedNodes; // DOM NodeList 34 | if (newNodes !== null) { // If there are new nodes added 35 | if (cb) cb(); 36 | } 37 | }); 38 | }); 39 | 40 | // Configuration of the observer: 41 | var config = { 42 | attributes: true, 43 | childList: true, 44 | subtree: true, 45 | }; 46 | 47 | var target = $("#root")[0]; 48 | 49 | // Pass in the target node, as well as the observer options 50 | observer.observe(target, config); 51 | } 52 | 53 | function formatPowerwallForGrafana() { 54 | // Hide elements. 55 | $('.overview-menu, #logout, .footer, .compact-btn-row, .toast-list, .power-flow-header, .btn').hide(); 56 | 57 | // Set alignment 58 | $('.core-layout__viewport').css({ 59 | padding: 0, 60 | margin: 0, 61 | }); 62 | 63 | $('.power-flow-header').css({ 64 | "padding-top": 0, 65 | }); 66 | 67 | $('.power-flow-grid').css({ 68 | width: "100%", 69 | left: 0, 70 | right: 0, 71 | margin: 0, 72 | "padding-top": 0, 73 | "position": "fixed", 74 | }); 75 | 76 | $('.app').css({ 77 | "overflow-y": "hidden", 78 | }); 79 | 80 | // Set colors 81 | $('body').css({ 82 | "background-color": "#161719", 83 | }); 84 | 85 | $('.power-flow-grid.active').css({ 86 | "background-color": "#161719", 87 | }); 88 | } -------------------------------------------------------------------------------- /proxy/web/viz-static/solar.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Clear IndexedDB to prevent auth hangup in the proxied Powerwall web app. 4 | try { 5 | window.indexedDB.databases().then((dbs) => { 6 | dbs.forEach(db => { window.indexedDB.deleteDatabase(db.name) }); 7 | }); 8 | } catch (error) { 9 | document.write("Browser blocking indexedDB - Turn off incognito mode."); 10 | } 11 | 12 | function injectScriptAndUse() { 13 | return new Promise((resolve, reject) => { 14 | var body = document.getElementsByTagName("body")[0]; 15 | var script = document.createElement("script"); 16 | script.src = "//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"; 17 | script.onload = function () { 18 | resolve(); 19 | }; 20 | body.appendChild(script); 21 | }); 22 | } 23 | 24 | injectScriptAndUse().then(() => { 25 | console.log("Applying SolarOnly customization"); 26 | triggerOnMutation(formatPowerwallForSolar); 27 | }); 28 | 29 | function triggerOnMutation(cb) { 30 | // Create an observer instance 31 | var observer = new MutationObserver(function (mutations) { 32 | mutations.forEach(function (mutation) { 33 | var newNodes = mutation.addedNodes; // DOM NodeList 34 | if (newNodes !== null) { // If there are new nodes added 35 | if (cb) cb(); 36 | } 37 | }); 38 | }); 39 | 40 | // Configuration of the observer: 41 | var config = { 42 | attributes: true, 43 | childList: true, 44 | subtree: true, 45 | }; 46 | 47 | var target = $("#root")[0]; 48 | 49 | // Pass in the target node, as well as the observer options 50 | observer.observe(target, config); 51 | } 52 | 53 | function formatPowerwallForSolar() { 54 | // Hide elements. 55 | $('.overview-menu, #logout, .footer, .compact-btn-row, .toast-list, .power-flow-header, .btn, .powerwall-soe, .soe-label').hide(); 56 | 57 | // Hide Powerwall image 58 | var imgElement = document.querySelector('[data-testid="b3372156-8a9e-4d17-9721-fcc5891d1074"]'); 59 | if (imgElement) { 60 | imgElement.style.display = 'none'; 61 | } 62 | // Hide the Powerwall text 63 | const divs = document.querySelectorAll('[data-testid="ec7d6a6d-b6d2-411c-a535-c052c00baf62"]'); 64 | divs.forEach(div => { 65 | if (div.style.width === '120px' && div.style.top === '200.5px' && div.style.left === '0px' && div.style.right === '0px') { 66 | const paragraph = div.querySelector('p[data-testid="4c6aadb3-7661-4d7f-b1ff-d5a0571fac60"]'); 67 | if (paragraph) { 68 | paragraph.style.display = 'none'; 69 | } 70 | } 71 | }); 72 | 73 | // Set alignment 74 | $('.core-layout__viewport').css({ 75 | padding: 0, 76 | margin: 0, 77 | }); 78 | 79 | $('.power-flow-header').css({ 80 | "padding-top": 0, 81 | }); 82 | 83 | $('.power-flow-grid').css({ 84 | width: "100%", 85 | left: 0, 86 | right: 0, 87 | margin: 0, 88 | "padding-top": 0, 89 | "position": "fixed", 90 | }); 91 | 92 | $('.app').css({ 93 | "overflow-y": "hidden", 94 | }); 95 | 96 | // Set colors 97 | $('body').css({ 98 | "background-color": "transparent", 99 | }); 100 | 101 | $('.power-flow-grid.active').css({ 102 | "background-color": "transparent", 103 | }); 104 | } 105 | -------------------------------------------------------------------------------- /proxy/web/viz-static/white.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | // Clear IndexedDB to prevent auth hangup in the proxied Powerwall web app. 4 | try { 5 | window.indexedDB.databases().then((dbs) => { 6 | dbs.forEach(db => { window.indexedDB.deleteDatabase(db.name) }); 7 | }); 8 | } catch (error) { 9 | document.write("Browser blocking indexedDB - Turn off incognito mode."); 10 | } 11 | 12 | function injectScriptAndUse() { 13 | return new Promise((resolve, reject) => { 14 | var body = document.getElementsByTagName("body")[0]; 15 | var script = document.createElement("script"); 16 | script.src = "//ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"; 17 | script.onload = function () { 18 | resolve(); 19 | }; 20 | body.appendChild(script); 21 | }); 22 | } 23 | 24 | injectScriptAndUse().then(() => { 25 | console.log("Applying White customization"); 26 | triggerOnMutation(formatPowerwallForWhite); 27 | }); 28 | 29 | function triggerOnMutation(cb) { 30 | // Create an observer instance 31 | var observer = new MutationObserver(function (mutations) { 32 | mutations.forEach(function (mutation) { 33 | var newNodes = mutation.addedNodes; // DOM NodeList 34 | if (newNodes !== null) { // If there are new nodes added 35 | if (cb) cb(); 36 | } 37 | }); 38 | }); 39 | 40 | // Configuration of the observer: 41 | var config = { 42 | attributes: true, 43 | childList: true, 44 | subtree: true, 45 | }; 46 | 47 | var target = $("#root")[0]; 48 | 49 | // Pass in the target node, as well as the observer options 50 | observer.observe(target, config); 51 | } 52 | 53 | function formatPowerwallForWhite() { 54 | // Hide elements. 55 | $('.overview-menu, #logout, .footer, .compact-btn-row, .toast-list, .power-flow-header, .btn').hide(); 56 | 57 | // Set alignment 58 | $('.core-layout__viewport').css({ 59 | padding: 0, 60 | margin: 0, 61 | }); 62 | 63 | $('.power-flow-header').css({ 64 | "padding-top": 0, 65 | }); 66 | 67 | $('.power-flow-grid').css({ 68 | width: "100%", 69 | left: 0, 70 | right: 0, 71 | margin: 0, 72 | "padding-top": 0, 73 | "position": "fixed", 74 | }); 75 | 76 | $('.app').css({ 77 | "overflow-y": "hidden", 78 | }); 79 | 80 | // Set colors 81 | $('body').css({ 82 | "background-color": "white", 83 | }); 84 | 85 | $('.power-flow-grid.active').css({ 86 | "background-color": "#ffffff", 87 | }); 88 | } -------------------------------------------------------------------------------- /pwsimulator/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.7-alpine 2 | WORKDIR /app 3 | RUN pip3 install pypowerwall 4 | COPY . . 5 | CMD ["python3", "stub.py"] 6 | EXPOSE 443 -------------------------------------------------------------------------------- /pwsimulator/README.md: -------------------------------------------------------------------------------- 1 | # pyPowerwall Simulator 2 | 3 | ![Docker Pulls](https://img.shields.io/docker/pulls/jasonacox/pwsimulator) 4 | 5 | You can use pyPowerwall simulator to mimic the responses from the Powerwall Gateway. This is useful for testing purposes. 6 | 7 | ## Quick Start 8 | 9 | 1. Run the Docker Container to listen on port 443 (https) - pulls from Docker Hub 10 | 11 | ```bash 12 | docker run \ 13 | -d \ 14 | -p 443:443 \ 15 | --name pwsimulator \ 16 | --restart unless-stopped \ 17 | jasonacox/pwsimulator 18 | ``` 19 | 20 | 2. Test using the [test.py](test.py) script set to use localhost as the Powerwall 21 | 22 | ```bash 23 | python3 test.py 24 | ``` 25 | 26 | 3. Test using the pypowerwall proxy against the simulator: 27 | 28 | ```bash 29 | # Launch Proxy 30 | cd .. 31 | PW_HOST=localhost \ 32 | PW_PASSWORD=password \ 33 | PW_EMAIL=me@example.com \ 34 | PW_DEBUG=yes python3 proxy/server.py 35 | 36 | # Open http://localhost:8675/example.html 37 | ``` 38 | 39 | 4. Change simulated values using [https://localhost/test/](https://localhost/test/). 40 | 41 | ## Build Your Own 42 | 43 | 1. Build the Docker Container 44 | 45 | ```bash 46 | docker build -t pwsimulator:latest . 47 | ``` 48 | 49 | 2. Setup the Docker Container to listen on port 443 (https) 50 | 51 | ```bash 52 | docker run \ 53 | -d \ 54 | -p 443:443 \ 55 | --name pwsimulator \ 56 | --restart unless-stopped \ 57 | pwsimulator 58 | ``` 59 | 60 | 3. Test the Proxy 61 | 62 | ```bash 63 | bash test.sh 64 | ``` 65 | 66 | ## Troubleshooting Help 67 | 68 | Check the logs: 69 | 70 | ```bash 71 | # See the logs 72 | docker logs pwsimulator 73 | ``` 74 | 75 | If you see python errors, make sure you entered your credentials correctly in the `stub.py` file. If you didn't, edit that file and restart docker: 76 | 77 | ```bash 78 | # Stop the server 79 | docker stop pypowerwall 80 | 81 | # Start the server 82 | docker start pypowerwall 83 | ``` 84 | 85 | ## Test Commands 86 | 87 | ### Battery 88 | Full: `curl -k https://localhost/test/battery-percentage/100.0` 89 | Empty: `curl -k https://localhost/test/battery-percentage/0.0` 90 | 91 | ### Grid 92 | Toggle Grid Connection: `curl -k https://localhost/test/toggle-grid` 93 | 94 | ### Solar 95 | Zero solar: `curl -k https://localhost/test/solar-power/0` 96 | Some solar: `curl -k https://localhost/test/solar-power/1450` 97 | 98 | ### Scenarios 99 | This script includes some sample scenarios to cover common use cases 100 | 101 | ```sh 102 | # Flow Scenarios 103 | curl -k http://localhost/test/scenario/battery-exporting 104 | curl -k http://localhost/test/scenario/solar-exporting 105 | curl -k http://localhost/test/scenario/solar-powered 106 | curl -k http://localhost/test/scenario/grid-powered 107 | curl -k http://localhost/test/scenario/self-powered 108 | curl -k http://localhost/test/scenario/battery-powered 109 | curl -k http://localhost/test/scenario/grid-charging 110 | curl -k http://localhost/test/scenario/solar-charging 111 | 112 | # Outages 113 | curl -k http://localhost/test/scenario/sunny-day-outage 114 | curl -k http://localhost/test/scenario/cloudy-day-outage 115 | curl -k http://localhost/test/scenario/nighttime-outage 116 | ``` 117 | 118 | ## Powerwall Scenario Simulator 119 | 120 | Thanks to @mccahan, there is an external UI that can be used to manage the Simulator while also watching the Power Flow Animation udpates. This can be installed with: 121 | 122 | ```bash 123 | docker run --rm -p 3000:3000 mccahan/pypowerwall-simulator-control:latest 124 | 125 | # Open http://localhost:3000 126 | ``` 127 | 128 | image 129 | -------------------------------------------------------------------------------- /pwsimulator/control.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Powerwall Simulator Control 7 | 15 | 16 | 17 |

Powerwall Simulator Control

18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 |

Outages

29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /pwsimulator/localhost.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC/7rKoz3FNDbZq 3 | ZiA9/WE8baZzFUn5c5a5EGksOWXM6AABnQcT1D9zDEC1EoZBOLkFeQDNc9dYvlSq 4 | tpwjdU2Cmu0yE7sPMZr37dxiZzYmShNO8TK5+dEaNHqMNCb+/Mp4ejUGiy6VLg/7 5 | ZNJYGleXGCRoeOUUp/eslanHG/+eJ8mRrYsNGbxhcmVw2kL/MZHsFkqhE6LwyFzH 6 | z7tDKEJ7vrFNFWYmdY8kzBAlWvonryi2foSv/EJSko5tIB5ItCBONdplindj1iqn 7 | DxAEOEKoX5sks9IJDNZNYqE6yUaWWOMvoNMCBNMzwoYbzp+xYAUEqeCef6oq5HvJ 8 | MFvQwJ1jAgMBAAECggEAWCF1/hfK8dPDGFYupehMFuBOlveGkTmnUvEbKghtG0rI 9 | ffUpw8wpGl3c9Ig+B4LRSpcGbFCt6Hz1CbJyPcHmRnt3QRBYS6C+hOu+z5yO+8P1 10 | 28DbRxRUFNFWGDz2Tw2qtul+obOVP0D4a7oe7/+giqu2RBaHMhXyn1fVrqzHkknr 11 | R+lEw3RoF6d42pZaZoBwoqlhKWUNnv8JYhJ3dwrwgz4OcqUghfO03H/ojDcfeeRA 12 | DEXSZ3g7aq9kCfo6li76gKjNS32jyGMPqZvEFQ+ieiHa4IplvvV7gAhVaDIjhfbD 13 | jjGcY5UxETm6MrcTjb4/LO9lttl+KXxyUBoiagwTIQKBgQD+Fga/B7PMRbf07GUu 14 | +O/Xtg+a5m5ncmkI+5+WAEvBiY4J1UFqL2g7syCZEFU6lcNeJuGTpLF35TXIPhb2 15 | 55yskFhINVsL2zjiPNo09O3kIvkrYeYpRuxlmc+xw6KzxjFGZ+kr/Y7bLvuLaUTY 16 | QmsmS1j2i6ChhRGl62kFVt/rcwKBgQDBYNDgClKA3LzpJUeZoKR3bjbnyItCsKsf 17 | AtADJwXrW6vqB3I6tAYUYVJE7roXDAYYZS5yiNul8zZrKiRYhT3LsJEIyAJxAN7A 18 | hNrqCr3fRqQ2y4McOpD7wqiBXSPnmeLqDsh7lEh30t5g2o+lhIgpBatwniMOK0AG 19 | I393cpDqUQKBgBTJ1I94n9tMsPpXBQhM0cLCYJB53fqUv8c+XxY0u/3/tSFU019O 20 | taZ8x2P+lBqQ+kUPxVEBHowCst3JRrO4y3bK8I4n32Ue8I6CEBBZIcWSnvRPtDcY 21 | WGsPpJCUwxEWXB24jrlMq41/UJFdcYaIGKAvdf3l+qR8cK80jYkkTHnFAoGAFnOg 22 | LLLmG1IFmOgcJQkSr08bjtOo34hVhdc0wXjxZf8Rua09pPUr04ftHlW9Q1nBB2oP 23 | 93pOTjkrs0uERsbJgvGwFR3rsqUQN7f1FsFChD7WaAo+4bMnDCPwnnxq48PpJZWr 24 | zzVLKKZR7VrS3LvDd0fSPNQPa+C3oT7T2NFzY9ECgYEAs4vkNd06pYTFn2VSJidX 25 | KomOukEvwmFSBNy+kTM6k6+BtWT4P10SdCVK8AJ3GE0WBnr/sGTprPuNWWUvh5nM 26 | Q6EZhdVsdTU136K5XtMF/fIEOy0bPD248HnkNoTD4Cx7grYgzPjEDfPamlS6DueF 27 | n3NzaJal3RTJFVKVuDsBIy4= 28 | -----END PRIVATE KEY----- 29 | -----BEGIN CERTIFICATE----- 30 | MIIDoDCCAogCCQD1MZCfbmLddDANBgkqhkiG9w0BAQsFADCBkTELMAkGA1UEBhMC 31 | dXMxEzARBgNVBAgMCkNhbGlmb3JuaWExFDASBgNVBAcMC0xvcyBBbmdlbGVzMRQw 32 | EgYDVQQKDAtweVBvd2Vyd2FsbDEMMAoGA1UECwwDT1NTMRIwEAYDVQQDDAlwb3dl 33 | cndhbGwxHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20wHhcNMjExMDE3 34 | MTQzNjM2WhcNMzExMDE1MTQzNjM2WjCBkTELMAkGA1UEBhMCdXMxEzARBgNVBAgM 35 | CkNhbGlmb3JuaWExFDASBgNVBAcMC0xvcyBBbmdlbGVzMRQwEgYDVQQKDAtweVBv 36 | d2Vyd2FsbDEMMAoGA1UECwwDT1NTMRIwEAYDVQQDDAlwb3dlcndhbGwxHzAdBgkq 37 | hkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB 38 | DwAwggEKAoIBAQC/7rKoz3FNDbZqZiA9/WE8baZzFUn5c5a5EGksOWXM6AABnQcT 39 | 1D9zDEC1EoZBOLkFeQDNc9dYvlSqtpwjdU2Cmu0yE7sPMZr37dxiZzYmShNO8TK5 40 | +dEaNHqMNCb+/Mp4ejUGiy6VLg/7ZNJYGleXGCRoeOUUp/eslanHG/+eJ8mRrYsN 41 | GbxhcmVw2kL/MZHsFkqhE6LwyFzHz7tDKEJ7vrFNFWYmdY8kzBAlWvonryi2foSv 42 | /EJSko5tIB5ItCBONdplindj1iqnDxAEOEKoX5sks9IJDNZNYqE6yUaWWOMvoNMC 43 | BNMzwoYbzp+xYAUEqeCef6oq5HvJMFvQwJ1jAgMBAAEwDQYJKoZIhvcNAQELBQAD 44 | ggEBAElk4rr/XaRfDu2S+Ln+xFTAj9Koloewk4CF8r1ko+GjxnZCka60rjJCrlGR 45 | AOXW05fatZtjpAIVBQNMC9pYQx5LPVHVA6NTp10JOJHofbAuOKqJsq/IbNByxHff 46 | KdfHkqBJpvHi2KmPLtEKQ3oYO2WTwArHa171DQleJSucSJHh+k4ln9GysZIq84Nx 47 | iVXKYe2ruhH66GeRk2iQHrx6/MhUCMTvtrVG7FwrRAc9nVdK2hyTO7I5PPILo0ux 48 | hVlT2raSCc9PM+ZmxwJk58fbDw0MsJ8GOYRKdT1Yt0ajVk8on1Zm6VD4f8I1z1/v 49 | 80naN9iMXp7/VR3TwMTXEkT2lDo= 50 | -----END CERTIFICATE----- 51 | -------------------------------------------------------------------------------- /pwsimulator/test.py: -------------------------------------------------------------------------------- 1 | # Example test for pypowerwall 2 | 3 | import pypowerwall 4 | 5 | # Optional: Turn on Debug Mode 6 | pypowerwall.set_debug(True) 7 | 8 | # Credentials for your Powerwall - Customer Login Data 9 | password='password' 10 | email='email@example.com' 11 | host = "localhost" # IP of your Powerwall 12 | timezone = "America/Los_Angeles" # Local timezone/tz 13 | 14 | # Connect to Powerwall 15 | pw = pypowerwall.Powerwall(host,password,email,timezone) 16 | 17 | # Display Metric Examples 18 | print("Battery power level: %0.0f%%" % pw.level()) 19 | print("Power response: %r" % pw.power()) 20 | print("Grid Power: %0.2fkW" % (float(pw.grid())/1000.0)) 21 | print("Solar Power: %0.2fkW" % (float(pw.solar())/1000.0)) 22 | print("Battery Power: %0.2fkW" % (float(pw.battery())/1000.0)) 23 | print("Home Power: %0.2fkW" % (float(pw.home())/1000.0)) 24 | 25 | # Raw JSON Data Examples 26 | print("Grid raw: %r" % pw.grid(verbose=True)) 27 | print("Solar raw: %r" % pw.solar(verbose=True)) 28 | 29 | -------------------------------------------------------------------------------- /pwsimulator/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Test Script for Powerwall Simulator 4 | # 5 | 6 | # Valid Login Request 7 | echo "------------------------------------------------" 8 | echo "Trying valid request for login: " 9 | echo "------------------------------------------------" 10 | curl 'https://localhost/api/login/Basic' \ 11 | --data-raw $'{"username":"customer","password":"password","email":"test@example.com","clientInfo":{"timezone":"America/Los_Angeles"}}' \ 12 | --compressed \ 13 | --insecure 14 | 15 | # Invalid Request - should 403 16 | echo " " 17 | echo " " 18 | echo "------------------------------------------------" 19 | echo "Trying invalid request for aggregates: " 20 | echo "------------------------------------------------" 21 | curl -i 'https://localhost/api/meters/aggregates' \ 22 | -H 'cookie: AuthCookie=rx5NqL9CHlaR6XCJeM_pah-gs9PLrvP7w8pW81w-JHm0nlxroEGD0rZY1FqiDvD_KIW1VWdSNIXd9ETZG-0P8Q==; UserRecord=eyJlbWFpbCI6Imphc29uQGphc29uYWNveC5jb20iLCJmaXJzdG5hbWUiOiJUZXNsYSIsImxhc3RuYW1lIjoiRW5lcmd5Iiwicm9sZXMiOlsiSG9tZV9Pd25lciJdLCJ0b2tlbiI6InJ4NU5xTDlDSGxhUjZYQ0plTV9wYWgtZ3M5UExydlA3dzhwVzgxdy1KSG0wbmx4cm9FR0QwclpZMUZxaUR2RF9LSVcxVldkU05JWGQ5RVRaRy0wUDhRPT0iLCJwcm92aWRlciI6IkJhc2ljIiwibG9naW5UaW1lIjoiMjAyMS0xMC0xNlQwMDoyOTozOS45NDc0NTk3NjMtMDc6MDAifQ==' \ 23 | --compressed \ 24 | --insecure 25 | 26 | # Valid Request - should 200 27 | echo " " 28 | echo " " 29 | echo "------------------------------------------------" 30 | echo "Trying valid request for aggregates: " 31 | echo "------------------------------------------------" 32 | curl -i 'https://localhost/api/meters/aggregates' \ 33 | -H 'cookie: AuthCookie=1234567890qwertyuiopasdfghjklZXcvbnm1234567890Qwertyuiopasdfghjklzxcvbnm1234567890qwer==; UserRecord=1234567890qwertyuiopasdfghjklZXcvbnm1234567890Qwertyuiopasdfghjklzxcvbnm1234567890qwer1234567890qwertyuiopasdfghjklZXcvbnm1234567890Qwertyuiopasdfghjklzxcvbnm1234567890qwer1234567890qwertyuiopasdfghjklZXcvbnm1234567890Qwertyuiopasdfghjklzxcvbnm1234567890qwer1234567890qwertyuiopasdfghjklZXcvbnm1234567890Qwertyuiopasdfghjklzxcvbnm1234567890qwer123456==' \ 34 | --compressed \ 35 | --insecure 36 | 37 | echo " " 38 | echo " " 39 | echo "------------------------------------------------" 40 | -------------------------------------------------------------------------------- /pypowerwall/api_lock.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import random 3 | import threading 4 | import time 5 | from collections import defaultdict 6 | from contextlib import contextmanager 7 | from typing import DefaultDict 8 | 9 | log = logging.getLogger(__name__) 10 | 11 | def acquire_with_exponential_backoff( 12 | lock: threading.Lock, 13 | timeout: float, 14 | initial_delay: float = 0.1, 15 | factor: int = 2, 16 | max_delay: int = 2, 17 | jitter: float = 0.1 18 | ) -> bool: 19 | """ 20 | Attempts to acquire a lock using exponential backoff with jitter. 21 | 22 | This function repeatedly attempts to acquire the given lock without blocking. 23 | If the lock is not immediately available, it waits for a delay period that increases 24 | exponentially with each attempt, plus a random jitter to reduce contention. The process 25 | continues until the lock is acquired or the total elapsed time exceeds the specified timeout. 26 | 27 | Args: 28 | lock (threading.Lock): The lock instance to acquire. 29 | timeout (float): The total time (in seconds) to keep trying to acquire the lock. 30 | initial_delay (float, optional): The initial delay (in seconds) before retrying after a failed attempt. Defaults to 0.1. 31 | factor (int, optional): The multiplier for the delay after each failed attempt. Defaults to 2. 32 | max_delay (int, optional): The maximum delay (in seconds) between retries. Defaults to 2. 33 | jitter (float, optional): The maximum additional random delay (in seconds) added to each sleep interval. Defaults to 0.1. 34 | 35 | Returns: 36 | bool: True if the lock was acquired within the timeout period, otherwise False. 37 | """ 38 | start_time = time.perf_counter() 39 | delay = initial_delay 40 | 41 | # Continue trying until the elapsed time exceeds the timeout 42 | elapsed = time.perf_counter() - start_time 43 | while elapsed < timeout: 44 | if lock.acquire(blocking=False): 45 | return True 46 | remaining_time = timeout - elapsed 47 | # Ensure we don't sleep past the timeout and add a bit of random jitter 48 | sleep_time = min(delay, remaining_time) + random.uniform(0, jitter) 49 | time.sleep(sleep_time) 50 | delay = min(delay * factor, max_delay) 51 | log.info(f"Timeout for {lock}") 52 | elapsed = time.perf_counter() - start_time 53 | 54 | return False 55 | 56 | 57 | @contextmanager 58 | def acquire_lock_with_backoff(func, timeout, **backoff_kwargs): 59 | """ 60 | Context manager for acquiring a lock using exponential backoff with jitter. 61 | Raises TimeoutError if the lock is not acquired in the given timeout. 62 | """ 63 | lock: threading.Lock = func.api_lock 64 | if not acquire_with_exponential_backoff(lock, timeout, **backoff_kwargs): 65 | raise TimeoutError("Unable to acquire lock within the specified timeout.") 66 | try: 67 | yield 68 | finally: 69 | lock.release() 70 | -------------------------------------------------------------------------------- /pypowerwall/cloud/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/pypowerwall/cloud/__init__.py -------------------------------------------------------------------------------- /pypowerwall/cloud/decorators.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | 4 | log = logging.getLogger('pypowerwall.cloud.pypowerwall_cloud') 5 | WARNED_ONCE = {} 6 | 7 | 8 | def not_implemented_mock_data(func): 9 | @functools.wraps(func) 10 | def wrapper(*args, **kwargs): 11 | if not WARNED_ONCE.get(func.__name__): 12 | log.warning(f"This API [{func.__name__}] is using mock data in cloud mode. This message will be " 13 | "printed only once at the warning level.") 14 | WARNED_ONCE[func.__name__] = 1 15 | else: 16 | log.debug(f"This API [{func.__name__}] is using mock data in cloud mode.") 17 | return func(*args, **kwargs) 18 | 19 | return wrapper 20 | -------------------------------------------------------------------------------- /pypowerwall/cloud/exceptions.py: -------------------------------------------------------------------------------- 1 | class PyPowerwallCloudNoTeslaAuthFile(Exception): 2 | pass 3 | 4 | 5 | class PyPowerwallCloudTeslaNotConnected(Exception): 6 | pass 7 | 8 | 9 | class PyPowerwallCloudNotImplemented(Exception): 10 | pass 11 | 12 | 13 | class PyPowerwallCloudInvalidPayload(Exception): 14 | pass 15 | -------------------------------------------------------------------------------- /pypowerwall/cloud/stubs.py: -------------------------------------------------------------------------------- 1 | API_METERS_AGGREGATES_STUB = { 2 | "site": { 3 | "last_communication_time": None, 4 | "instant_power": None, 5 | "instant_reactive_power": 0, 6 | "instant_apparent_power": 0, 7 | "frequency": 0, 8 | "energy_exported": 0, 9 | "energy_imported": 0, 10 | "instant_average_voltage": 0, 11 | "instant_average_current": 0, 12 | "i_a_current": 0, 13 | "i_b_current": 0, 14 | "i_c_current": 0, 15 | "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z", 16 | "last_phase_power_communication_time": "0001-01-01T00:00:00Z", 17 | "last_phase_energy_communication_time": "0001-01-01T00:00:00Z", 18 | "timeout": 1500000000, 19 | "num_meters_aggregated": 1, 20 | "instant_total_current": None 21 | }, 22 | "battery": { 23 | "last_communication_time": None, 24 | "instant_power": None, 25 | "instant_reactive_power": 0, 26 | "instant_apparent_power": 0, 27 | "frequency": 0, 28 | "energy_exported": 0, 29 | "energy_imported": 0, 30 | "instant_average_voltage": 0, 31 | "instant_average_current": 0, 32 | "i_a_current": 0, 33 | "i_b_current": 0, 34 | "i_c_current": 0, 35 | "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z", 36 | "last_phase_power_communication_time": "0001-01-01T00:00:00Z", 37 | "last_phase_energy_communication_time": "0001-01-01T00:00:00Z", 38 | "timeout": 1500000000, 39 | "num_meters_aggregated": None, 40 | "instant_total_current": 0 41 | }, 42 | "load": { 43 | "last_communication_time": None, 44 | "instant_power": None, 45 | "instant_reactive_power": 0, 46 | "instant_apparent_power": 0, 47 | "frequency": 0, 48 | "energy_exported": 0, 49 | "energy_imported": 0, 50 | "instant_average_voltage": 0, 51 | "instant_average_current": 0, 52 | "i_a_current": 0, 53 | "i_b_current": 0, 54 | "i_c_current": 0, 55 | "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z", 56 | "last_phase_power_communication_time": "0001-01-01T00:00:00Z", 57 | "last_phase_energy_communication_time": "0001-01-01T00:00:00Z", 58 | "timeout": 1500000000, 59 | "instant_total_current": 0 60 | }, 61 | "solar": { 62 | "last_communication_time": None, 63 | "instant_power": None, 64 | "instant_reactive_power": 0, 65 | "instant_apparent_power": 0, 66 | "frequency": 0, 67 | "energy_exported": 0, 68 | "energy_imported": 0, 69 | "instant_average_voltage": 0, 70 | "instant_average_current": 0, 71 | "i_a_current": 0, 72 | "i_b_current": 0, 73 | "i_c_current": 0, 74 | "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z", 75 | "last_phase_power_communication_time": "0001-01-01T00:00:00Z", 76 | "last_phase_energy_communication_time": "0001-01-01T00:00:00Z", 77 | "timeout": 1000000000, 78 | "num_meters_aggregated": None, 79 | "instant_total_current": 0 80 | } 81 | } 82 | 83 | API_SYSTEM_STATUS_STUB = { # TODO: Fill in 0 values 84 | "command_source": "Configuration", 85 | "battery_target_power": 0, 86 | "battery_target_reactive_power": 0, 87 | "nominal_full_pack_energy": None, 88 | "nominal_energy_remaining": None, 89 | "max_power_energy_remaining": 0, # TODO: Calculate 90 | "max_power_energy_to_be_charged": 0, # TODO: Calculate 91 | "max_charge_power": None, 92 | "max_discharge_power": None, 93 | "max_apparent_power": None, 94 | "instantaneous_max_discharge_power": 0, 95 | "instantaneous_max_charge_power": 0, 96 | "instantaneous_max_apparent_power": 0, 97 | "hardware_capability_charge_power": 0, 98 | "hardware_capability_discharge_power": 0, 99 | "grid_services_power": None, 100 | "system_island_state": None, 101 | "available_blocks": None, 102 | "available_charger_blocks": 0, 103 | "battery_blocks": [], # TODO: Populate with battery blocks 104 | "ffr_power_availability_high": 0, 105 | "ffr_power_availability_low": 0, 106 | "load_charge_constraint": 0, 107 | "max_sustained_ramp_rate": 0, 108 | "grid_faults": [], # TODO: Populate with grid faults 109 | "can_reboot": "Yes", 110 | "smart_inv_delta_p": 0, 111 | "smart_inv_delta_q": 0, 112 | "last_toggle_timestamp": "2023-10-13T04:08:05.957195-07:00", 113 | "solar_real_power_limit": None, 114 | "score": 10000, 115 | "blocks_controlled": None, 116 | "primary": True, 117 | "auxiliary_load": 0, 118 | "all_enable_lines_high": True, 119 | "inverter_nominal_usable_power": 0, 120 | "expected_energy_remaining": 0 121 | } 122 | -------------------------------------------------------------------------------- /pypowerwall/exceptions.py: -------------------------------------------------------------------------------- 1 | class PyPowerwallInvalidConfigurationParameter(Exception): 2 | pass 3 | 4 | 5 | class InvalidBatteryReserveLevelException(Exception): 6 | pass 7 | -------------------------------------------------------------------------------- /pypowerwall/fleetapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/pypowerwall/fleetapi/__init__.py -------------------------------------------------------------------------------- /pypowerwall/fleetapi/__main__.py: -------------------------------------------------------------------------------- 1 | # pyPowerWall Module - FleetAPI Command Line Interface 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Command Line Interface for Tesla FleetAPI to read and control Powerwall 5 | status and settings. This module is a command line interface to the 6 | FleetAPI class in the pypowerwall module. 7 | 8 | Author: Jason A. Cox 9 | For more information see https://github.com/jasonacox/pypowerwall 10 | 11 | Command Line Interface for FleetAPI: 12 | python3 -m pypowerwall.fleetapi 13 | 14 | """ 15 | 16 | # Import Libraries 17 | import sys 18 | import json 19 | import argparse 20 | from .fleetapi import FleetAPI, CONFIGFILE 21 | 22 | # Display help if no arguments 23 | if len(sys.argv) == 1: 24 | print("Tesla FleetAPI - Command Line Interface\n") 25 | print(f"Usage: {sys.argv[0]} command [arguments] [-h] [--debug] [--config CONFIG] [--site SITE] [--json]\n") 26 | print("Commands:") 27 | print(" setup Setup FleetAPI for your site") 28 | print(" sites List available sites") 29 | print(" status Report current power status for your site") 30 | print(" info Display information about your site") 31 | print(" getmode Get current operational mode setting") 32 | print(" getreserve Get current battery reserve level setting") 33 | print(" setmode Set operatinoal mode (self_consumption or autonomous)") 34 | print(" setreserve Set battery reserve level (prcentage or 'current')\n") 35 | print("options:") 36 | print(" --debug Enable debug mode") 37 | print(" --config CONFIG Specify alternate config file (default: .fleetapi.config)") 38 | print(" --site SITE Specify site_id") 39 | print(" --json Output in JSON format") 40 | sys.exit(0) 41 | 42 | # Parse command line arguments 43 | parser = argparse.ArgumentParser(description='Tesla FleetAPI - Command Line Interface') 44 | parser.add_argument("command", choices=["setup", "sites", "status", "info", "getmode", "getreserve", 45 | "setmode", "setreserve"], help="Select command to execute") 46 | parser.add_argument("argument", nargs="?", default=None, help="Argument for setmode or setreserve command") 47 | parser.add_argument("--debug", action="store_true", help="Enable debug mode") 48 | parser.add_argument("--config", help="Specify alternate config file") 49 | parser.add_argument("--site", help="Specify site_id") 50 | parser.add_argument("--json", action="store_true", help="Output in JSON format") 51 | 52 | # Adding descriptions for each command 53 | parser.add_help = False # Disabling default help message 54 | args = parser.parse_args() 55 | 56 | settings_file = CONFIGFILE 57 | if args.config: 58 | # Use alternate config file if specified 59 | settings_file = args.config 60 | 61 | # Create FleetAPI object 62 | settings_debug = False 63 | settings_site = None 64 | if args.debug: 65 | settings_debug = True 66 | if args.site: 67 | settings_site = args.site 68 | 69 | # Create FleetAPI object 70 | fleet = FleetAPI(configfile=settings_file, debug=settings_debug, site_id=settings_site) 71 | 72 | # Load Configuration 73 | if not fleet.load_config(): 74 | print(f" Configuration file not found: {settings_file}") 75 | if args.command != "setup": 76 | print(" Run setup to access Tesla FleetAPI.") 77 | sys.exit(1) 78 | else: 79 | fleet.setup() 80 | if not fleet.load_config(): 81 | print(" Setup failed, exiting...") 82 | sys.exit(1) 83 | sys.exit(0) 84 | 85 | # Command: Run Setup 86 | if args.command == "setup": 87 | fleet.setup() 88 | sys.exit(0) 89 | 90 | # Command: List Sites 91 | if args.command == "sites": 92 | sites = fleet.getsites() 93 | if args.json: 94 | print(json.dumps(sites, indent=4)) 95 | else: 96 | for site in sites: 97 | print(f" {site['energy_site_id']} - {site['site_name']}") 98 | sys.exit(0) 99 | 100 | # Command: Status 101 | if args.command == "status": 102 | status = fleet.get_live_status() 103 | if args.json: 104 | print(json.dumps(status, indent=4)) 105 | else: 106 | for key in status: 107 | print(f" {key}: {status[key]}") 108 | sys.exit(0) 109 | 110 | # Command: Site Info 111 | if args.command == "info": 112 | info = fleet.get_site_info() 113 | if args.json: 114 | print(json.dumps(info, indent=4)) 115 | else: 116 | for key in info: 117 | print(f" {key}: {info[key]}") 118 | sys.exit(0) 119 | 120 | # Command: Get Operating Mode 121 | if args.command == "getmode": 122 | mode = fleet.get_operating_mode() 123 | if args.json: 124 | print(json.dumps({"mode": mode}, indent=4)) 125 | else: 126 | print(f"{mode}") 127 | sys.exit(0) 128 | 129 | # Command: Get Battery Reserve 130 | if args.command == "getreserve": 131 | reserve = fleet.get_battery_reserve() 132 | if args.json: 133 | print(json.dumps({"reserve": reserve}, indent=4)) 134 | else: 135 | print(f"{reserve}") 136 | sys.exit(0) 137 | 138 | # Command: Set Operating Mode 139 | if args.command == "setmode": 140 | if args.argument: 141 | # autonomous or self_consumption 142 | if args.argument in ["self", "self_consumption"]: 143 | print(fleet.set_operating_mode("self_consumption")) 144 | elif args.argument in ["auto", "time", "autonomous"]: 145 | print(fleet.set_operating_mode("autonomous")) 146 | else: 147 | print("Invalid mode, must be 'self' or 'auto'") 148 | sys.exit(1) 149 | else: 150 | print("No mode specified, exiting...") 151 | sys.exit(0) 152 | 153 | # Command: Set Battery Reserve 154 | if args.command == "setreserve": 155 | if args.argument: 156 | if args.argument.isdigit(): 157 | val = int(args.argument) 158 | if val < 0 or val > 100: 159 | print(f"Invalid reserve level {val}, must be 0-100") 160 | sys.exit(1) 161 | elif args.argument == "current": 162 | val = fleet.battery_level() 163 | else: 164 | print("Invalid reserve level, must be 0-100 or 'current' to set to current level.") 165 | sys.exit(1) 166 | print(fleet.set_battery_reserve(int(val))) 167 | else: 168 | print("No reserve level specified, exiting...") 169 | sys.exit(0) 170 | 171 | print("No command specified, exiting...") 172 | sys.exit(1) 173 | -------------------------------------------------------------------------------- /pypowerwall/fleetapi/decorators.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | 4 | log = logging.getLogger('pypowerwall.fleetapi.pypowerwall_fleetapi') 5 | WARNED_ONCE = {} 6 | 7 | 8 | def not_implemented_mock_data(func): 9 | @functools.wraps(func) 10 | def wrapper(*args, **kwargs): 11 | if not WARNED_ONCE.get(func.__name__): 12 | log.warning(f"This API [{func.__name__}] is using mock data in fleetapi mode. This message will be " 13 | "printed only once at the warning level.") 14 | WARNED_ONCE[func.__name__] = 1 15 | else: 16 | log.debug(f"This API [{func.__name__}] is using mock data in fleetapi mode.") 17 | return func(*args, **kwargs) 18 | 19 | return wrapper 20 | -------------------------------------------------------------------------------- /pypowerwall/fleetapi/exceptions.py: -------------------------------------------------------------------------------- 1 | class PyPowerwallFleetAPINoTeslaAuthFile(Exception): 2 | pass 3 | 4 | 5 | class PyPowerwallFleetAPITeslaNotConnected(Exception): 6 | pass 7 | 8 | 9 | class PyPowerwallFleetAPINotImplemented(Exception): 10 | pass 11 | 12 | 13 | class PyPowerwallFleetAPIInvalidPayload(Exception): 14 | pass 15 | -------------------------------------------------------------------------------- /pypowerwall/fleetapi/stubs.py: -------------------------------------------------------------------------------- 1 | API_METERS_AGGREGATES_STUB = { 2 | "site": { 3 | "last_communication_time": None, 4 | "instant_power": None, 5 | "instant_reactive_power": 0, 6 | "instant_apparent_power": 0, 7 | "frequency": 0, 8 | "energy_exported": 0, 9 | "energy_imported": 0, 10 | "instant_average_voltage": 0, 11 | "instant_average_current": 0, 12 | "i_a_current": 0, 13 | "i_b_current": 0, 14 | "i_c_current": 0, 15 | "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z", 16 | "last_phase_power_communication_time": "0001-01-01T00:00:00Z", 17 | "last_phase_energy_communication_time": "0001-01-01T00:00:00Z", 18 | "timeout": 1500000000, 19 | "num_meters_aggregated": 1, 20 | "instant_total_current": None 21 | }, 22 | "battery": { 23 | "last_communication_time": None, 24 | "instant_power": None, 25 | "instant_reactive_power": 0, 26 | "instant_apparent_power": 0, 27 | "frequency": 0, 28 | "energy_exported": 0, 29 | "energy_imported": 0, 30 | "instant_average_voltage": 0, 31 | "instant_average_current": 0, 32 | "i_a_current": 0, 33 | "i_b_current": 0, 34 | "i_c_current": 0, 35 | "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z", 36 | "last_phase_power_communication_time": "0001-01-01T00:00:00Z", 37 | "last_phase_energy_communication_time": "0001-01-01T00:00:00Z", 38 | "timeout": 1500000000, 39 | "num_meters_aggregated": None, 40 | "instant_total_current": 0 41 | }, 42 | "load": { 43 | "last_communication_time": None, 44 | "instant_power": None, 45 | "instant_reactive_power": 0, 46 | "instant_apparent_power": 0, 47 | "frequency": 0, 48 | "energy_exported": 0, 49 | "energy_imported": 0, 50 | "instant_average_voltage": 0, 51 | "instant_average_current": 0, 52 | "i_a_current": 0, 53 | "i_b_current": 0, 54 | "i_c_current": 0, 55 | "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z", 56 | "last_phase_power_communication_time": "0001-01-01T00:00:00Z", 57 | "last_phase_energy_communication_time": "0001-01-01T00:00:00Z", 58 | "timeout": 1500000000, 59 | "instant_total_current": 0 60 | }, 61 | "solar": { 62 | "last_communication_time": None, 63 | "instant_power": None, 64 | "instant_reactive_power": 0, 65 | "instant_apparent_power": 0, 66 | "frequency": 0, 67 | "energy_exported": 0, 68 | "energy_imported": 0, 69 | "instant_average_voltage": 0, 70 | "instant_average_current": 0, 71 | "i_a_current": 0, 72 | "i_b_current": 0, 73 | "i_c_current": 0, 74 | "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z", 75 | "last_phase_power_communication_time": "0001-01-01T00:00:00Z", 76 | "last_phase_energy_communication_time": "0001-01-01T00:00:00Z", 77 | "timeout": 1000000000, 78 | "num_meters_aggregated": None, 79 | "instant_total_current": 0 80 | } 81 | } 82 | 83 | API_SYSTEM_STATUS_STUB = { # TODO: Fill in 0 values 84 | "command_source": "Configuration", 85 | "battery_target_power": 0, 86 | "battery_target_reactive_power": 0, 87 | "nominal_full_pack_energy": None, 88 | "nominal_energy_remaining": None, 89 | "max_power_energy_remaining": 0, # TODO: Calculate 90 | "max_power_energy_to_be_charged": 0, # TODO: Calculate 91 | "max_charge_power": None, 92 | "max_discharge_power": None, 93 | "max_apparent_power": None, 94 | "instantaneous_max_discharge_power": 0, 95 | "instantaneous_max_charge_power": 0, 96 | "instantaneous_max_apparent_power": 0, 97 | "hardware_capability_charge_power": 0, 98 | "hardware_capability_discharge_power": 0, 99 | "grid_services_power": None, 100 | "system_island_state": None, 101 | "available_blocks": None, 102 | "available_charger_blocks": 0, 103 | "battery_blocks": [], # TODO: Populate with battery blocks 104 | "ffr_power_availability_high": 0, 105 | "ffr_power_availability_low": 0, 106 | "load_charge_constraint": 0, 107 | "max_sustained_ramp_rate": 0, 108 | "grid_faults": [], # TODO: Populate with grid faults 109 | "can_reboot": "Yes", 110 | "smart_inv_delta_p": 0, 111 | "smart_inv_delta_q": 0, 112 | "last_toggle_timestamp": "2023-10-13T04:08:05.957195-07:00", 113 | "solar_real_power_limit": None, 114 | "score": 10000, 115 | "blocks_controlled": None, 116 | "primary": True, 117 | "auxiliary_load": 0, 118 | "all_enable_lines_high": True, 119 | "inverter_nominal_usable_power": 0, 120 | "expected_energy_remaining": 0 121 | } 122 | -------------------------------------------------------------------------------- /pypowerwall/local/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/pypowerwall/local/__init__.py -------------------------------------------------------------------------------- /pypowerwall/local/exceptions.py: -------------------------------------------------------------------------------- 1 | class LoginError(Exception): 2 | pass 3 | 4 | 5 | class PowerwallConnectionError(Exception): 6 | pass 7 | -------------------------------------------------------------------------------- /pypowerwall/pypowerwall_base.py: -------------------------------------------------------------------------------- 1 | import abc 2 | import logging 3 | from typing import Optional, Any, Union 4 | 5 | log = logging.getLogger(__name__) 6 | 7 | # Define which write API calls should invalidate which read API cache keys 8 | WRITE_OP_READ_OP_CACHE_MAP = { 9 | '/api/operation': ['/api/operation', 'SITE_CONFIG'] # local and cloud mode respectively 10 | } 11 | 12 | 13 | def parse_version(version: str) -> Optional[int]: 14 | if version is None or not isinstance(version, str): 15 | return None 16 | 17 | val = version.split(" ")[0] 18 | val = ''.join(i for i in val if i.isdigit() or i in './\\') 19 | while len(val.split('.')) < 3: 20 | val = val + ".0" 21 | line = [int(x, 10) for x in val.split('.')] 22 | line.reverse() 23 | vint = sum(x * (100 ** i) for i, x in enumerate(line)) 24 | return vint 25 | 26 | 27 | class PyPowerwallBase: 28 | 29 | def __init__(self, email: str): 30 | super().__init__() 31 | self.pwcache = {} # holds the cached data for api 32 | self.auth = None 33 | self.token = None # caches bearer token 34 | self.email = email 35 | 36 | @abc.abstractmethod 37 | def authenticate(self): 38 | raise NotImplementedError 39 | 40 | @abc.abstractmethod 41 | def close_session(self): 42 | raise NotImplementedError 43 | 44 | @abc.abstractmethod 45 | def poll(self, api: str, force: bool = False, 46 | recursive: bool = False, raw: bool = False) -> Optional[Union[dict, list, str, bytes]]: 47 | raise NotImplementedError 48 | 49 | @abc.abstractmethod 50 | def post(self, api: str, payload: Optional[dict], din: Optional[str], 51 | recursive: bool = False, raw: bool = False) -> Optional[Union[dict, list, str, bytes]]: 52 | raise NotImplementedError 53 | 54 | @abc.abstractmethod 55 | def vitals(self) -> Optional[dict]: 56 | raise NotImplementedError 57 | 58 | @abc.abstractmethod 59 | def get_time_remaining(self) -> Optional[float]: 60 | raise NotImplementedError 61 | 62 | # pylint: disable=inconsistent-return-statements 63 | def fetchpower(self, sensor, verbose=False) -> Any: 64 | if verbose: 65 | payload: dict = self.poll('/api/meters/aggregates') 66 | if payload and sensor in payload: 67 | return payload[sensor] 68 | else: 69 | return None 70 | r = self.power() 71 | if r and sensor in r: 72 | return r[sensor] 73 | 74 | def power(self) -> dict: 75 | site = solar = battery = load = 0.0 76 | payload: dict = self.poll('/api/meters/aggregates') 77 | try: 78 | site = payload['site']['instant_power'] 79 | solar = payload['solar']['instant_power'] 80 | battery = payload['battery']['instant_power'] 81 | load = payload['load']['instant_power'] 82 | except Exception as e: 83 | log.debug(f"ERROR unable to parse payload '{payload}': {e}") 84 | return {'site': site, 'solar': solar, 'battery': battery, 'load': load} 85 | 86 | def _invalidate_cache(self, api: str): 87 | cache_keys = WRITE_OP_READ_OP_CACHE_MAP.get(api, []) 88 | for cache_key in cache_keys: 89 | self.pwcache[cache_key] = None 90 | -------------------------------------------------------------------------------- /pypowerwall/regex.py: -------------------------------------------------------------------------------- 1 | import re 2 | 3 | IPV4_6_REGEX = re.compile(r'((^\s*((([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}([0-9]|[1-9][0-9]|1[0-9]{' 4 | r'2}|2[0-4][0-9]|25[0-5]))\s*$)|(^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([' 5 | r'0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[' 6 | r'0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,' 7 | r'2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([' 8 | r'0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[' 9 | r'0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,' 10 | r'4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[' 11 | r'1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[' 12 | r'0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[' 13 | r'0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,' 14 | r'6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[' 15 | r'0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,' 16 | r'5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(' 17 | r'%.+)?\s*$))') 18 | 19 | HOST_REGEX = re.compile(r'^(?=.{1,255}$)[0-9A-Za-z](?:(?:[0-9A-Za-z]|-){0,61}[0-9A-Za-z])?(?:\.[0-9A-Za-z](?:(?:[' 20 | r'0-9A-Za-z]|-){0,61}[0-9A-Za-z])?)*\.?$') 21 | 22 | EMAIL_REGEX = re.compile(r'^\S+@\S+\.\S+$') 23 | -------------------------------------------------------------------------------- /pypowerwall/tedapi/__main__.py: -------------------------------------------------------------------------------- 1 | # pyPowerwall - Tesla TEDAPI Class Main 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Tesla TEADAPI Class - Command Line Test 5 | 6 | This script tests the TEDAPI class by connecting to a Tesla Powerwall Gateway 7 | """ 8 | 9 | def run_tedapi_test(auto=False, debug=False): 10 | # Imports 11 | from pypowerwall.tedapi import TEDAPI, GW_IP 12 | from pypowerwall import __version__ 13 | import json 14 | import sys 15 | import argparse 16 | import requests 17 | import logging 18 | 19 | # Print header 20 | print(f"pyPowerwall - Powerwall Gateway TEDAPI Reader [v{__version__}]") 21 | 22 | # Setup Logging 23 | log = logging.getLogger(__name__) 24 | 25 | def set_debug(toggle=True, color=True): 26 | """Enable verbose logging""" 27 | if toggle: 28 | if color: 29 | logging.basicConfig(format='\x1b[31;1m%(levelname)s:%(message)s\x1b[0m', level=logging.DEBUG) 30 | else: 31 | logging.basicConfig(format='%(levelname)s:%(message)s', level=logging.DEBUG) 32 | log.setLevel(logging.DEBUG) 33 | log.debug('pyPowerwall TEDAPI version %s', __version__) 34 | log.debug('Python %s on %s', sys.version, sys.platform) 35 | else: 36 | log.setLevel(logging.NOTSET) 37 | 38 | # Load arguments if invoked from pypowerwall 39 | if auto: 40 | argv = ['pypowerwall'] 41 | if debug: 42 | argv.append('--debug') 43 | sys.argv = argv 44 | 45 | # Check for arguments using argparse 46 | parser = argparse.ArgumentParser(description='Tesla Powerwall Gateway TEDAPI Reader') 47 | parser.add_argument('gw_pwd', nargs='?', help='Powerwall Gateway Password') 48 | parser.add_argument('--gw_ip', default=GW_IP, help='Powerwall Gateway IP Address') 49 | parser.add_argument('--debug', action='store_true', help='Enable Debug Output') 50 | # Parse arguments 51 | args = parser.parse_args() 52 | if args.gw_pwd: 53 | gw_pwd = args.gw_pwd 54 | else: 55 | gw_pwd = None 56 | if args.debug: 57 | set_debug(True) 58 | GW_IP = args.gw_ip 59 | 60 | # Check that GW_IP is listening to port 443 61 | url = f'https://{GW_IP}' 62 | log.debug(f"Checking Powerwall Gateway at {url}") 63 | print(f" - Connecting to {url}...", end="") 64 | try: 65 | resp = requests.get(url, verify=False, timeout=5) 66 | log.debug(f"Connection to Powerwall Gateway successful, code {resp.status_code}.") 67 | print(" SUCCESS") 68 | except Exception as e: 69 | print(" FAILED") 70 | print() 71 | print(f"ERROR: Unable to connect to Powerwall Gateway {GW_IP} on port 443.") 72 | print("Please verify your your host has a route to the Gateway.") 73 | print(f"\nError details: {e}") 74 | sys.exit(1) 75 | 76 | # Get GW_PWD from User if not provided 77 | if gw_pwd is None: 78 | while not gw_pwd: 79 | try: 80 | gw_pwd = input("\nEnter Powerwall Gateway Password: ") 81 | except KeyboardInterrupt: 82 | print("") 83 | sys.exit(1) 84 | except Exception as e: 85 | print(f"Error: {e}") 86 | sys.exit(1) 87 | if not gw_pwd: 88 | print("Password Required") 89 | 90 | # Create TEDAPI Object and get Configuration and Status 91 | print() 92 | print(f"Connecting to Powerwall Gateway {GW_IP}") 93 | ted = TEDAPI(gw_pwd, host=GW_IP) 94 | if ted.din is None: 95 | print("\nERROR: Unable to connect to Powerwall Gateway. Check your password and try again") 96 | sys.exit(1) 97 | config = ted.get_config() 98 | status = ted.get_status() 99 | print() 100 | 101 | # Print Configuration 102 | print(" - Configuration:") 103 | site_info = config.get('site_info', {}) 104 | site_name = site_info.get('site_name', 'Unknown') 105 | print(f" - Site Name: {site_name}") 106 | battery_commission_date = site_info.get('battery_commission_date', 'Unknown') 107 | print(f" - Battery Commission Date: {battery_commission_date}") 108 | vin = config.get('vin', 'Unknown') 109 | print(f" - VIN: {vin}") 110 | number_of_powerwalls = len(config.get('battery_blocks', [])) 111 | print(f" - Number of Powerwalls: {number_of_powerwalls}") 112 | print() 113 | 114 | # Print power data 115 | print(" - Power Data:") 116 | nominalEnergyRemainingWh = status.get('control', {}).get('systemStatus', {}).get('nominalEnergyRemainingWh', 0) 117 | nominalFullPackEnergyWh = status.get('control', {}).get('systemStatus', {}).get('nominalFullPackEnergyWh', 0) 118 | if nominalFullPackEnergyWh == 0: 119 | print(f" - Battery Full Charge Unknown ({nominalEnergyRemainingWh}Wh of {nominalFullPackEnergyWh}Wh)") 120 | else: 121 | soe = round(nominalEnergyRemainingWh / nominalFullPackEnergyWh * 100, 2) 122 | print(f" - Battery Charge: {soe}% ({nominalEnergyRemainingWh}Wh of {nominalFullPackEnergyWh}Wh)") 123 | meterAggregates = status.get('control', {}).get('meterAggregates', []) 124 | for meter in meterAggregates: 125 | location = meter.get('location', 'Unknown').title() 126 | realPowerW = int(meter.get('realPowerW', 0)) 127 | print(f" - {location}: {realPowerW}W") 128 | print() 129 | 130 | # Save Configuration and Status to JSON files 131 | with open('status.json', 'w') as f: 132 | json.dump(status, f) 133 | with open('config.json', 'w') as f: 134 | json.dump(config, f) 135 | print(" - Configuration and Status saved to config.json and status.json") 136 | print() 137 | 138 | if __name__ == "__main__": 139 | run_tedapi_test() 140 | -------------------------------------------------------------------------------- /pypowerwall/tedapi/decorators.py: -------------------------------------------------------------------------------- 1 | import functools 2 | import logging 3 | 4 | log = logging.getLogger('pypowerwall.tedapi.pypowerwall_tedapi') 5 | WARNED_ONCE = {} 6 | 7 | 8 | def not_implemented_mock_data(func): 9 | @functools.wraps(func) 10 | def wrapper(*args, **kwargs): 11 | if not WARNED_ONCE.get(func.__name__): 12 | log.warning(f"This API [{func.__name__}] is using mock data in tedapi mode. This message will be " 13 | "printed only once at the warning level.") 14 | WARNED_ONCE[func.__name__] = 1 15 | else: 16 | log.debug(f"This API [{func.__name__}] is using mock data in tedapi mode.") 17 | return func(*args, **kwargs) 18 | 19 | return wrapper 20 | -------------------------------------------------------------------------------- /pypowerwall/tedapi/exceptions.py: -------------------------------------------------------------------------------- 1 | class PyPowerwallTEDAPINoTeslaAuthFile(Exception): 2 | pass 3 | 4 | 5 | class PyPowerwallTEDAPITeslaNotConnected(Exception): 6 | pass 7 | 8 | 9 | class PyPowerwallTEDAPINotImplemented(Exception): 10 | pass 11 | 12 | 13 | class PyPowerwallTEDAPIInvalidPayload(Exception): 14 | pass 15 | -------------------------------------------------------------------------------- /pypowerwall/tedapi/stubs.py: -------------------------------------------------------------------------------- 1 | API_METERS_AGGREGATES_STUB = { 2 | "site": { 3 | "last_communication_time": None, 4 | "instant_power": None, 5 | "instant_reactive_power": 0, 6 | "instant_apparent_power": 0, 7 | "frequency": 0, 8 | "energy_exported": 0, 9 | "energy_imported": 0, 10 | "instant_average_voltage": 0, 11 | "instant_average_current": 0, 12 | "i_a_current": 0, 13 | "i_b_current": 0, 14 | "i_c_current": 0, 15 | "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z", 16 | "last_phase_power_communication_time": "0001-01-01T00:00:00Z", 17 | "last_phase_energy_communication_time": "0001-01-01T00:00:00Z", 18 | "timeout": 1500000000, 19 | "num_meters_aggregated": 1, 20 | "instant_total_current": None 21 | }, 22 | "battery": { 23 | "last_communication_time": None, 24 | "instant_power": None, 25 | "instant_reactive_power": 0, 26 | "instant_apparent_power": 0, 27 | "frequency": 0, 28 | "energy_exported": 0, 29 | "energy_imported": 0, 30 | "instant_average_voltage": 0, 31 | "instant_average_current": 0, 32 | "i_a_current": 0, 33 | "i_b_current": 0, 34 | "i_c_current": 0, 35 | "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z", 36 | "last_phase_power_communication_time": "0001-01-01T00:00:00Z", 37 | "last_phase_energy_communication_time": "0001-01-01T00:00:00Z", 38 | "timeout": 1500000000, 39 | "num_meters_aggregated": None, 40 | "instant_total_current": None 41 | }, 42 | "load": { 43 | "last_communication_time": None, 44 | "instant_power": None, 45 | "instant_reactive_power": 0, 46 | "instant_apparent_power": 0, 47 | "frequency": 0, 48 | "energy_exported": 0, 49 | "energy_imported": 0, 50 | "instant_average_voltage": 0, 51 | "instant_average_current": 0, 52 | "i_a_current": 0, 53 | "i_b_current": 0, 54 | "i_c_current": 0, 55 | "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z", 56 | "last_phase_power_communication_time": "0001-01-01T00:00:00Z", 57 | "last_phase_energy_communication_time": "0001-01-01T00:00:00Z", 58 | "timeout": 1500000000, 59 | "instant_total_current": None 60 | }, 61 | "solar": { 62 | "last_communication_time": None, 63 | "instant_power": None, 64 | "instant_reactive_power": 0, 65 | "instant_apparent_power": 0, 66 | "frequency": 0, 67 | "energy_exported": 0, 68 | "energy_imported": 0, 69 | "instant_average_voltage": 0, 70 | "instant_average_current": 0, 71 | "i_a_current": 0, 72 | "i_b_current": 0, 73 | "i_c_current": 0, 74 | "last_phase_voltage_communication_time": "0001-01-01T00:00:00Z", 75 | "last_phase_power_communication_time": "0001-01-01T00:00:00Z", 76 | "last_phase_energy_communication_time": "0001-01-01T00:00:00Z", 77 | "timeout": 1000000000, 78 | "num_meters_aggregated": None, 79 | "instant_total_current": None 80 | } 81 | } 82 | 83 | API_SYSTEM_STATUS_STUB = { # TODO: Fill in 0 values 84 | "command_source": "Configuration", 85 | "battery_target_power": 0, 86 | "battery_target_reactive_power": 0, 87 | "nominal_full_pack_energy": None, 88 | "nominal_energy_remaining": None, 89 | "max_power_energy_remaining": 0, # TODO: Calculate 90 | "max_power_energy_to_be_charged": 0, # TODO: Calculate 91 | "max_charge_power": None, 92 | "max_discharge_power": None, 93 | "max_apparent_power": None, 94 | "instantaneous_max_discharge_power": 0, 95 | "instantaneous_max_charge_power": 0, 96 | "instantaneous_max_apparent_power": 0, 97 | "hardware_capability_charge_power": 0, 98 | "hardware_capability_discharge_power": 0, 99 | "grid_services_power": None, 100 | "system_island_state": None, 101 | "available_blocks": None, 102 | "available_charger_blocks": 0, 103 | "battery_blocks": [], # TODO: Populate with battery blocks 104 | "ffr_power_availability_high": 0, 105 | "ffr_power_availability_low": 0, 106 | "load_charge_constraint": 0, 107 | "max_sustained_ramp_rate": 0, 108 | "grid_faults": [], # TODO: Populate with grid faults 109 | "can_reboot": "Yes", 110 | "smart_inv_delta_p": 0, 111 | "smart_inv_delta_q": 0, 112 | "last_toggle_timestamp": "2023-10-13T04:08:05.957195-07:00", 113 | "solar_real_power_limit": None, 114 | "score": 10000, 115 | "blocks_controlled": None, 116 | "primary": True, 117 | "auxiliary_load": 0, 118 | "all_enable_lines_high": True, 119 | "inverter_nominal_usable_power": 0, 120 | "expected_energy_remaining": 0 121 | } 122 | -------------------------------------------------------------------------------- /pypowerwall/tedapi/tedapi.proto: -------------------------------------------------------------------------------- 1 | // Tesla tedapi API Protocol Buffer definition (tedapi.proto) 2 | // 3 | // Create tedapi_pb2.py for use in projects using the protoc compiler: 4 | // protoc --python_out=. tedapi.proto 5 | // 6 | // Author: Jason A. Cox - Date: 22 Nov 2023 - Version: 1.1 7 | // 8 | // For more information see https://github.com/jasonacox/pypowerwall 9 | 10 | syntax = "proto3"; 11 | 12 | package tedapi; 13 | 14 | // ***** Message ***** 15 | 16 | message Message { 17 | MessageEnvelope message = 1; 18 | Tail tail = 2; 19 | } 20 | 21 | message MessageEnvelope { 22 | int32 deliveryChannel = 1; 23 | Participant sender = 2; 24 | Participant recipient = 3; 25 | FirmwareType firmware = 4; 26 | optional ConfigType config = 15; 27 | optional QueryType payload = 16; 28 | } 29 | 30 | message Participant { 31 | oneof id { 32 | string din = 1; 33 | int32 teslaService = 2; 34 | int32 local = 3; 35 | int32 authorizedClient = 4; 36 | } 37 | } 38 | 39 | message Tail { 40 | int32 value = 1; 41 | } 42 | 43 | // ***** Query = 4 **** 44 | 45 | message FirmwareType { 46 | oneof id { 47 | string request = 2; 48 | FirmwarePayload system = 3; 49 | } 50 | } 51 | 52 | message FirmwarePayload { 53 | EcuId gateway = 1; 54 | string din = 2; 55 | FirmwareVersion version = 3; 56 | FirmwareFive five = 5; 57 | int32 six = 6; 58 | DeviceArray wireless = 7; 59 | bytes field8 = 8; 60 | bytes field9 = 9; 61 | } 62 | 63 | message EcuId { 64 | string partNumber = 1; 65 | string serialNumber = 2; 66 | } 67 | 68 | message FirmwareVersion { 69 | string text = 1; 70 | bytes githash = 2; 71 | } 72 | 73 | message FirmwareFive { 74 | int32 d = 2; 75 | } 76 | 77 | message DeviceArray { 78 | repeated DeviceInfo device = 1; 79 | } 80 | 81 | message DeviceInfo { 82 | StringValue company = 1; 83 | StringValue model = 2; 84 | StringValue fcc_id = 3; 85 | StringValue ic = 4; 86 | } 87 | 88 | // ***** Query = 16 ***** 89 | 90 | message QueryType { // 16 91 | optional PayloadQuerySend send = 1; 92 | optional PayloadString recv = 2; 93 | } 94 | 95 | message PayloadQuerySend { // 1 96 | optional int32 num = 1; 97 | optional PayloadString payload = 2; 98 | optional bytes code = 3; 99 | optional StringValue b = 4; 100 | } 101 | 102 | // ***** Config = 15 ***** 103 | 104 | message ConfigType { // 15 105 | oneof config { 106 | PayloadConfigSend send = 1; 107 | PayloadConfigRecv recv = 2; 108 | } 109 | } 110 | 111 | message PayloadConfigSend { // 1 112 | int32 num = 1; 113 | string file = 2; 114 | } 115 | 116 | message PayloadConfigRecv { // 2 117 | ConfigString file = 1; 118 | bytes code = 2; 119 | } 120 | 121 | message ConfigString { 122 | string name = 1; 123 | string text = 100; 124 | } 125 | 126 | // ***** General ***** 127 | 128 | message PayloadString { 129 | int32 value = 1; 130 | string text = 2; 131 | } 132 | 133 | message StringValue { 134 | string value = 1; 135 | } 136 | 137 | // ***** BASED ON RAW DECODED PAYLOADS ***** 138 | // 139 | // REQUEST - config 140 | // 1 { 141 | // 1: 1 142 | // 2 { 143 | // 3: 1 144 | // } 145 | // 3 { 146 | // 1: "1232100-00-E--TG123456789012" 147 | // } 148 | // 15 { 149 | // 1 { 150 | // 1: 1 151 | // 2: "config.json" 152 | // } 153 | // } 154 | // } 155 | // 2 { 156 | // 1: 1 157 | // } 158 | // 159 | // RESPONSE - config 160 | // 1 { 161 | // 1: 1 162 | // 2 { 163 | // 1: "1232100-00-E--TG123456789012" 164 | // } 165 | // 3 { 166 | // 3: 1 167 | // } 168 | // 15 { 169 | // 2 { 170 | // 1 { 171 | // 1: "config.json" 172 | // 100: "{}" 173 | // } 174 | // 2: "\255\177t+5\35..." 175 | // } 176 | // } 177 | // } 178 | // 2 { 179 | // 1: 1 180 | // } 181 | // 182 | // 183 | // REQUEST - query 184 | // 1 { 185 | // 1: 1 186 | // 2 { 187 | // 3: 1 188 | // } 189 | // 3 { 190 | // 1: "1232100-00-E--TG123456789012" 191 | // } 192 | // 16 { 193 | // 1 { 194 | // 1: 2 195 | // 2 { 196 | // 1: 1 197 | // 2: " query DeviceControllerQuery {..." 198 | // } 199 | // 3: "0\201\210\002B\0026\335T\310\02..." 200 | // 4 { 201 | // 1: "{}" 202 | // } 203 | // } 204 | // } 205 | // } 206 | // 2 { 207 | // 1: 1 208 | // } 209 | // 210 | // RESPONSE - query 211 | // 1 { 212 | // 1: 1 213 | // 2 { 214 | // 1: "1232100-00-E--TG123456789012" 215 | // } 216 | // 3 { 217 | // 3: 1 218 | // } 219 | // 16 { 220 | // 2 { 221 | // 1: 1 222 | // 2: "{...}" 223 | // } 224 | // } 225 | // } 226 | // 2 { 227 | // 1: 1 228 | // } 229 | // 230 | // REQUEST - firmware 231 | // 1 { 232 | // 1: 1 233 | // 2 { 234 | // 3: 1 235 | // } 236 | // 3 { 237 | // 1: "1707000-00-J--TG9999999999XP" 238 | // } 239 | // 4 { 240 | // 2: "" 241 | // } 242 | // } 243 | // 2 { 244 | // 1: 1 245 | // } 246 | // 247 | // RESPONSE - firmware 248 | // 1 { 249 | // 1: 1 250 | // 2 { 251 | // 1: "1707000-00-J--TG9999999999XP" 252 | // } 253 | // 3 { 254 | // 3: 1 255 | // } 256 | // 4 { 257 | // 3 { 258 | // 1 { 259 | // 1: "1707000-00-J" 260 | // 2: "TG9999999999XP" 261 | // } 262 | // 2: "1707000-00-J--TG9999999999XP" 263 | // 3 { 264 | // 1: "24.12.6-PW3-AFCI 008bf6ff" <--- PW3 firmware version 265 | // 2: "\000\213\366\...Redacted..." 266 | // } 267 | // 5 { 268 | // 2: 1 269 | // } 270 | // 6: 4 271 | // 7 { 272 | // 1 { 273 | // 1 { 274 | // 1: "Quectel" 275 | // } 276 | // 2 { 277 | // 1: "BG95-M2" 278 | // } 279 | // 3 { 280 | // 1: "XMR2020BG95M2" 281 | // } 282 | // 4 { 283 | // 1: "10224A-2020BG95M2" 284 | // } 285 | // } 286 | // 1 { 287 | // 1 { 288 | // 1: "Texas Instruments" 289 | // } 290 | // 2 { 291 | // 1: "WL18MODGI" 292 | // } 293 | // 3 { 294 | // 1: "Z64-WL18DBMOD" 295 | // } 296 | // 4 { 297 | // 1: "451I-WL18DBMOD" 298 | // } 299 | // } 300 | // } 301 | // 8: "\370!s\306\212...Redacted..." 302 | // 9: "\373U\353\322...Redacted..." 303 | // } 304 | // } 305 | // } 306 | // 2 { 307 | // 1: 1 308 | // } -------------------------------------------------------------------------------- /pypowerwall/tedapi/tedapi_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: tedapi.proto 4 | # Protobuf Python Version: 4.25.2 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0ctedapi.proto\x12\x06tedapi\"O\n\x07Message\x12(\n\x07message\x18\x01 \x01(\x0b\x32\x17.tedapi.MessageEnvelope\x12\x1a\n\x04tail\x18\x02 \x01(\x0b\x32\x0c.tedapi.Tail\"\x88\x02\n\x0fMessageEnvelope\x12\x17\n\x0f\x64\x65liveryChannel\x18\x01 \x01(\x05\x12#\n\x06sender\x18\x02 \x01(\x0b\x32\x13.tedapi.Participant\x12&\n\trecipient\x18\x03 \x01(\x0b\x32\x13.tedapi.Participant\x12&\n\x08\x66irmware\x18\x04 \x01(\x0b\x32\x14.tedapi.FirmwareType\x12\'\n\x06\x63onfig\x18\x0f \x01(\x0b\x32\x12.tedapi.ConfigTypeH\x00\x88\x01\x01\x12\'\n\x07payload\x18\x10 \x01(\x0b\x32\x11.tedapi.QueryTypeH\x01\x88\x01\x01\x42\t\n\x07_configB\n\n\x08_payload\"g\n\x0bParticipant\x12\r\n\x03\x64in\x18\x01 \x01(\tH\x00\x12\x16\n\x0cteslaService\x18\x02 \x01(\x05H\x00\x12\x0f\n\x05local\x18\x03 \x01(\x05H\x00\x12\x1a\n\x10\x61uthorizedClient\x18\x04 \x01(\x05H\x00\x42\x04\n\x02id\"\x15\n\x04Tail\x12\r\n\x05value\x18\x01 \x01(\x05\"R\n\x0c\x46irmwareType\x12\x11\n\x07request\x18\x02 \x01(\tH\x00\x12)\n\x06system\x18\x03 \x01(\x0b\x32\x17.tedapi.FirmwarePayloadH\x00\x42\x04\n\x02id\"\xe0\x01\n\x0f\x46irmwarePayload\x12\x1e\n\x07gateway\x18\x01 \x01(\x0b\x32\r.tedapi.EcuId\x12\x0b\n\x03\x64in\x18\x02 \x01(\t\x12(\n\x07version\x18\x03 \x01(\x0b\x32\x17.tedapi.FirmwareVersion\x12\"\n\x04\x66ive\x18\x05 \x01(\x0b\x32\x14.tedapi.FirmwareFive\x12\x0b\n\x03six\x18\x06 \x01(\x05\x12%\n\x08wireless\x18\x07 \x01(\x0b\x32\x13.tedapi.DeviceArray\x12\x0e\n\x06\x66ield8\x18\x08 \x01(\x0c\x12\x0e\n\x06\x66ield9\x18\t \x01(\x0c\"1\n\x05\x45\x63uId\x12\x12\n\npartNumber\x18\x01 \x01(\t\x12\x14\n\x0cserialNumber\x18\x02 \x01(\t\"0\n\x0f\x46irmwareVersion\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x0f\n\x07githash\x18\x02 \x01(\x0c\"\x19\n\x0c\x46irmwareFive\x12\t\n\x01\x64\x18\x02 \x01(\x05\"1\n\x0b\x44\x65viceArray\x12\"\n\x06\x64\x65vice\x18\x01 \x03(\x0b\x32\x12.tedapi.DeviceInfo\"\x9c\x01\n\nDeviceInfo\x12$\n\x07\x63ompany\x18\x01 \x01(\x0b\x32\x13.tedapi.StringValue\x12\"\n\x05model\x18\x02 \x01(\x0b\x32\x13.tedapi.StringValue\x12#\n\x06\x66\x63\x63_id\x18\x03 \x01(\x0b\x32\x13.tedapi.StringValue\x12\x1f\n\x02ic\x18\x04 \x01(\x0b\x32\x13.tedapi.StringValue\"t\n\tQueryType\x12+\n\x04send\x18\x01 \x01(\x0b\x32\x18.tedapi.PayloadQuerySendH\x00\x88\x01\x01\x12(\n\x04recv\x18\x02 \x01(\x0b\x32\x15.tedapi.PayloadStringH\x01\x88\x01\x01\x42\x07\n\x05_sendB\x07\n\x05_recv\"\xac\x01\n\x10PayloadQuerySend\x12\x10\n\x03num\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12+\n\x07payload\x18\x02 \x01(\x0b\x32\x15.tedapi.PayloadStringH\x01\x88\x01\x01\x12\x11\n\x04\x63ode\x18\x03 \x01(\x0cH\x02\x88\x01\x01\x12#\n\x01\x62\x18\x04 \x01(\x0b\x32\x13.tedapi.StringValueH\x03\x88\x01\x01\x42\x06\n\x04_numB\n\n\x08_payloadB\x07\n\x05_codeB\x04\n\x02_b\"l\n\nConfigType\x12)\n\x04send\x18\x01 \x01(\x0b\x32\x19.tedapi.PayloadConfigSendH\x00\x12)\n\x04recv\x18\x02 \x01(\x0b\x32\x19.tedapi.PayloadConfigRecvH\x00\x42\x08\n\x06\x63onfig\".\n\x11PayloadConfigSend\x12\x0b\n\x03num\x18\x01 \x01(\x05\x12\x0c\n\x04\x66ile\x18\x02 \x01(\t\"E\n\x11PayloadConfigRecv\x12\"\n\x04\x66ile\x18\x01 \x01(\x0b\x32\x14.tedapi.ConfigString\x12\x0c\n\x04\x63ode\x18\x02 \x01(\x0c\"*\n\x0c\x43onfigString\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x64 \x01(\t\",\n\rPayloadString\x12\r\n\x05value\x18\x01 \x01(\x05\x12\x0c\n\x04text\x18\x02 \x01(\t\"\x1c\n\x0bStringValue\x12\r\n\x05value\x18\x01 \x01(\tb\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'tedapi_pb2', _globals) 22 | if _descriptor._USE_C_DESCRIPTORS == False: 23 | DESCRIPTOR._options = None 24 | _globals['_MESSAGE']._serialized_start=24 25 | _globals['_MESSAGE']._serialized_end=103 26 | _globals['_MESSAGEENVELOPE']._serialized_start=106 27 | _globals['_MESSAGEENVELOPE']._serialized_end=370 28 | _globals['_PARTICIPANT']._serialized_start=372 29 | _globals['_PARTICIPANT']._serialized_end=475 30 | _globals['_TAIL']._serialized_start=477 31 | _globals['_TAIL']._serialized_end=498 32 | _globals['_FIRMWARETYPE']._serialized_start=500 33 | _globals['_FIRMWARETYPE']._serialized_end=582 34 | _globals['_FIRMWAREPAYLOAD']._serialized_start=585 35 | _globals['_FIRMWAREPAYLOAD']._serialized_end=809 36 | _globals['_ECUID']._serialized_start=811 37 | _globals['_ECUID']._serialized_end=860 38 | _globals['_FIRMWAREVERSION']._serialized_start=862 39 | _globals['_FIRMWAREVERSION']._serialized_end=910 40 | _globals['_FIRMWAREFIVE']._serialized_start=912 41 | _globals['_FIRMWAREFIVE']._serialized_end=937 42 | _globals['_DEVICEARRAY']._serialized_start=939 43 | _globals['_DEVICEARRAY']._serialized_end=988 44 | _globals['_DEVICEINFO']._serialized_start=991 45 | _globals['_DEVICEINFO']._serialized_end=1147 46 | _globals['_QUERYTYPE']._serialized_start=1149 47 | _globals['_QUERYTYPE']._serialized_end=1265 48 | _globals['_PAYLOADQUERYSEND']._serialized_start=1268 49 | _globals['_PAYLOADQUERYSEND']._serialized_end=1440 50 | _globals['_CONFIGTYPE']._serialized_start=1442 51 | _globals['_CONFIGTYPE']._serialized_end=1550 52 | _globals['_PAYLOADCONFIGSEND']._serialized_start=1552 53 | _globals['_PAYLOADCONFIGSEND']._serialized_end=1598 54 | _globals['_PAYLOADCONFIGRECV']._serialized_start=1600 55 | _globals['_PAYLOADCONFIGRECV']._serialized_end=1669 56 | _globals['_CONFIGSTRING']._serialized_start=1671 57 | _globals['_CONFIGSTRING']._serialized_end=1713 58 | _globals['_PAYLOADSTRING']._serialized_start=1715 59 | _globals['_PAYLOADSTRING']._serialized_end=1759 60 | _globals['_STRINGVALUE']._serialized_start=1761 61 | _globals['_STRINGVALUE']._serialized_end=1789 62 | # @@protoc_insertion_point(module_scope) 63 | -------------------------------------------------------------------------------- /pypowerwall/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/pypowerwall/tests/__init__.py -------------------------------------------------------------------------------- /pypowerwall/tests/tedapi/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jasonacox/pypowerwall/19beedcb940751224e12315f837d55487758740a/pypowerwall/tests/tedapi/__init__.py -------------------------------------------------------------------------------- /pypowerwall/tests/tedapi/test_init.py: -------------------------------------------------------------------------------- 1 | import time 2 | from unittest.mock import patch 3 | 4 | from pypowerwall.tedapi import TEDAPI 5 | import pytest 6 | 7 | @pytest.fixture 8 | def mock_tedapi(): 9 | """Create a TEDAPI instance with mocked connection""" 10 | with patch('pypowerwall.tedapi.TEDAPI.connect', return_value="TEST_DIN"): 11 | api = TEDAPI("test_password", pwcacheexpire=50) 12 | api.pwcache = { 13 | "status": { 14 | "control": { 15 | "meterAggregates": [ 16 | {"location": "LOAD", "realPowerW": 1500}, 17 | {"location": "SOLAR", "realPowerW": 3000}, 18 | {"location": "BATTERY", "realPowerW": -500} 19 | ] 20 | } 21 | } 22 | } 23 | return api 24 | 25 | class TestTEDAPIPowerMethods: 26 | def test_current_power_single_location(self, mock_tedapi): 27 | """Test current power for single location""" 28 | mock_tedapi.pwcachetime = {"status": time.time()} 29 | 30 | assert mock_tedapi.current_power("LOAD") == 1500 31 | assert mock_tedapi.current_power("SOLAR") == 3000 32 | assert mock_tedapi.current_power("BATTERY") == -500 33 | 34 | def test_current_power_all_locations(self, mock_tedapi): 35 | """Test current power for all locations""" 36 | mock_tedapi.pwcachetime = {"status": time.time()} 37 | 38 | result = mock_tedapi.current_power() 39 | assert result == {"LOAD": 1500, "SOLAR": 3000, "BATTERY": -500} 40 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | # Basic configuration 3 | testpaths = pypowerwall/tests/tedapi 4 | python_files = test_*.py 5 | python_classes = Test* 6 | python_functions = test_* 7 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # 2 | requests 3 | protobuf>=3.20.0 4 | teslapy -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import setuptools 2 | 3 | from pypowerwall import __version__ 4 | 5 | with open("README.md", "r") as fh: 6 | long_description = fh.read() 7 | 8 | setuptools.setup( 9 | name="pypowerwall", 10 | version=__version__, 11 | author="Jason Cox", 12 | author_email="jason@jasonacox.com", 13 | description="Python module to access Tesla Energy Gateway for Powerwall and solar power data", 14 | long_description=long_description, 15 | long_description_content_type="text/markdown", 16 | url='https://github.com/jasonacox/pypowerwall', 17 | packages=setuptools.find_packages(), 18 | install_requires=[ 19 | 'requests', 20 | 'protobuf>=3.20.0', 21 | 'teslapy', 22 | ], 23 | classifiers=[ 24 | "Programming Language :: Python :: 3", 25 | "License :: OSI Approved :: MIT License", 26 | "Operating System :: OS Independent", 27 | ], 28 | ) 29 | -------------------------------------------------------------------------------- /test.py: -------------------------------------------------------------------------------- 1 | # Basic test for pypowerwall module 2 | 3 | import pypowerwall 4 | 5 | pypowerwall.set_debug(True) 6 | 7 | # TO DO: Add simulator and tests - see example.py 8 | 9 | -------------------------------------------------------------------------------- /test_requirements.txt: -------------------------------------------------------------------------------- 1 | # Additional dependencies required for running tests only 2 | pytest 3 | # Add any other test-only dependencies below 4 | -------------------------------------------------------------------------------- /tools/cron.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Cronjob to check Powerwall battery charge level and 4 | # adjust the reserve limit. 5 | # 6 | # This script is set to optimize Powerwall charging during 7 | # solar production (clean energy) and use battery during 8 | # peak grid usage (dirty energy) time. 9 | # 10 | # Requires: 11 | # * pypowerwall python module (pip install pypowerwall) 12 | # * Tesla auth file setup - see instructions at: 13 | # https://github.com/jasonacox/pypowerwall/tree/main/tools 14 | # * weather411 service (optional) 15 | # (see https://github.com/jasonacox/Powerwall-Dashboard/tree/main/weather) 16 | 17 | # SET THIS 18 | POWERWALL='10.1.1.10' # address of Powerwall 19 | #INFLUXDB='10.1.1.20' # address of InfluxDB - Comment out if not using Powerwall-Dashboard 20 | WEATHER='10.1.1.11' # address of weather411 service 21 | PASSWORD='yourPassword' # Powerwall password 22 | FOLDER='/home/tesla' # Location of Tesla auth file 23 | 24 | # Reserve Settigs 25 | MAX=80 26 | MIN=20 27 | CLOUDS=0 28 | 29 | LOGFILE=cron.log 30 | cd $FOLDER 31 | 32 | # Fetch cloud conditions from jasonacox/weather411 container 33 | # Note: comment out if you do not have weather411 service 34 | CLOUDS=`curl --silent http://${WEATHER}:8676/clouds | jq -r '.clouds'` 35 | 36 | # Fetch current stats from Powerwall 1=grid, 2=house, 3=solar, 4=pw, 5=level, 6=reserve 37 | STATE=`python3 << END 38 | import pypowerwall 39 | pw = pypowerwall.Powerwall("${POWERWALL}","${PASSWORD}") 40 | print("%d,%d,%d,%d,%d,%d" % (pw.grid(),pw.home(),pw.solar(),pw.battery(),pw.level(True),pw.get_reserve(True))) 41 | END` 42 | 43 | # Powerwall-Dashboard users: Python funtion to get max temperature from InfluxDB for past 24 hours 44 | MAXTEMP=0 45 | if [ ! -z "$INFLUXDB" ]; then 46 | MAXTEMP=`python3 << END 47 | import influxdb 48 | client = influxdb.InfluxDBClient("${INFLUXDB}", database='powerwall') 49 | query = 'SELECT max("temp_max") FROM "autogen"."weather" WHERE time > now() - 24h GROUP BY time(1d) fill(none)' 50 | result = client.query(query) 51 | points = list(result.get_points()) 52 | if len(points) > 0: 53 | print(points[0]['max']) 54 | else: 55 | print(0) 56 | END` 57 | fi 58 | 59 | # Data from pypowerwall 1=grid, 2=house, 3=solar, 4=pw, 5=level 60 | GRID=`echo ${STATE} | cut -f1 -d,` 61 | HOUSE=`echo ${STATE} | cut -f2 -d,` 62 | SOLAR=`echo ${STATE} | cut -f3 -d,` 63 | PW=`echo ${STATE} | cut -f4 -d,` 64 | LEVEL=`echo ${STATE} | cut -f5 -d,` 65 | CUR=`echo ${STATE} | cut -f6 -d,` 66 | 67 | # Current date and time 68 | MONTH=`date +%b` 69 | DATE=`date +%d` 70 | YEAR=`date +%Y` 71 | HOUR=`date +%H` 72 | MINUTE=`date +%M` 73 | H=`date +%H | bc` # remove leading zero 74 | M=`date +%M | bc` 75 | echo "$MONTH $DATE $YEAR ${HOUR}:${MINUTE}: The battery level is ${LEVEL}, Grid=${GRID}, House=${HOUSE}, Solar=${SOLAR}, PW=${PW}, Reserve Setting=${CUR}, Clouds=${CLOUDS}" 76 | 77 | # Function to change reserve 78 | change() { 79 | echo "Change to ${1}" 80 | # if variable is current 81 | if [ "$1" == "current" ]; then 82 | /usr/bin/python3 set-reserve.py --current 83 | else 84 | /usr/bin/python3 set-reserve.py --set $1 85 | fi 86 | echo "$MONTH $DATE $YEAR ${HOUR}:${MINUTE}: Updated to ${1} - The battery level is ${LEVEL}, Grid=${GRID}, House=${HOUSE}, Solar=${SOLAR}, PW=${PW}, Reserve was=${CUR}" | tee -a $LOGFILE 87 | } 88 | 89 | # Logic for operations 90 | 91 | # WINTER - Nov, Dec and Jan - Adjust Reserve to save energy for peak 92 | if [[ "${MONTH}" =~ ^(Nov|Dec|Jan)$ ]]; then 93 | # From 9am to 4pm - Peak solar production time - charge battery 94 | if (( $H >= 9 )) && (( $H < 16 )); then 95 | # 9am to 4pm 96 | if (( $(echo "$LEVEL < $MAX" |bc -l) )); then 97 | # If not charged 98 | if (( $(echo "$CUR < $MAX" |bc -l) )); then 99 | # change reserve if not already set 100 | change $MAX 101 | fi 102 | fi 103 | fi 104 | else 105 | # NOT WINTER 106 | # From 11am to 4pm - Peak solar production time - charge battery if cloudy 107 | if (( $H >= 11 )) && (( $H < 16 )) && (( $CLOUDS > 90 )); then 108 | # 11am to 4pm 109 | if (( $(echo "$LEVEL < $MAX" |bc -l) )); then 110 | # If not charged 111 | if (( $(echo "$CUR < $MAX" |bc -l) )); then 112 | # change reserve if not already set 113 | change $MAX 114 | fi 115 | fi 116 | fi 117 | fi 118 | 119 | # Afternoon - Peak grid usage - force switch to battery 120 | if (( $H >= 16 )) && (( $H < 21 )); then 121 | if (( $(echo "$CUR <= $MAX" |bc -l) )); then 122 | if (( $(echo "$CUR > $MIN" |bc -l) )); then 123 | # change reserve if not already set 124 | change $MIN 125 | fi 126 | fi 127 | fi 128 | 129 | # Evening 9pm to Midnight - Non-peak grid usage 130 | # Stop using battery if 24h max temp was above 25C - Heavy A/C Use 131 | if (( $H >= 21 )) && (( $MAXTEMP > 25 )); then 132 | # Make sure CUR is >= LEVEL and not below MIN 133 | if (( $(echo "$CUR < $LEVEL" |bc -l) )); then 134 | if (( $(echo "$LEVEL > $MIN" |bc -l) )); then 135 | change $LEVEL 136 | fi 137 | fi 138 | fi 139 | 140 | # Powerwall Protection 141 | 142 | # Never let reserve go below MIN 143 | if (( $CUR < $MIN )); then 144 | change $MIN 145 | fi -------------------------------------------------------------------------------- /tools/fleetapi/create_pem_key.py: -------------------------------------------------------------------------------- 1 | # pyPowerWall - Tesla FleetAPI - Create PEM Key 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Tesla FleetAPI Setup - Create PEM Key 5 | 6 | This script creates a PEM-encoded public and private key. 7 | Put the public key in 8 | {site}/.well-known/appspecific/com.tesla.3p.public-key.pem 9 | 10 | Author: Jason A. Cox 11 | For more information see https://github.com/jasonacox/pypowerwall 12 | 13 | Requires: pip install cryptography 14 | """ 15 | 16 | from cryptography.hazmat.primitives import serialization 17 | from cryptography.hazmat.primitives.asymmetric import ec 18 | 19 | # Generate an EC key pair using the secp256r1 curve 20 | private_key = ec.generate_private_key(ec.SECP256R1()) 21 | 22 | # Extract the public key 23 | public_key = private_key.public_key() 24 | 25 | # Serialize the public key in PEM format 26 | pem_public_key = public_key.public_bytes( 27 | encoding=serialization.Encoding.PEM, 28 | format=serialization.PublicFormat.SubjectPublicKeyInfo 29 | ) 30 | 31 | # Print the PEM-encoded public and private keys 32 | print(f"Private Key: \n{private_key.private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode()}") 33 | print(f"\nPublic Key: \n{pem_public_key.decode()}") 34 | 35 | # Write the PEM-encoded public key to a file 36 | with open('com.tesla.3p.public-key.pem', 'w') as f: 37 | f.write(pem_public_key.decode()) 38 | # Write the PEM-encoded private key to a file 39 | with open('com.tesla.3p.private-key.pem', 'w') as f: 40 | f.write(private_key.private_bytes(encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode()) 41 | 42 | # Tell User 43 | print("\nPublic and Private Keys written to PEM files:") 44 | print(" * com.tesla.3p.public-key.pem") 45 | print(" * com.tesla.3p.private-key.pem") 46 | print("\nPut the public key on your registered website at:") 47 | print("https://{domain}/.well-known/appspecific/com.tesla.3p.public-key.pem") 48 | -------------------------------------------------------------------------------- /tools/fleetapi/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Tesla FleetAPI Setup 5 | 18 | 19 | 20 |

Tesla FleetAPI Setup

21 |

Copy the code below and paste it into the "code" field in the Tesla FleetAPI setup script.

22 |

Authorization Code

23 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /tools/fleetapi/live.py: -------------------------------------------------------------------------------- 1 | # pyPowerWall - Tesla FleetAPI - Setup 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Tesla FleetAPI - Poll Live Status 5 | 6 | This script is an example of using the Tesla FleetAPI to poll 7 | configuration and power data from the PowerWall. 8 | 9 | Requirements: 10 | - Tesla Partner Account 11 | - Run setup.py first to get tokens 12 | 13 | Author: Jason A. Cox 14 | For more information see https://github.com/jasonacox/pypowerwall 15 | 16 | """ 17 | # Import Modules 18 | import requests 19 | import json 20 | import os 21 | 22 | # Configuration Files - Required 23 | ENV_FILE = ".env" # Location of CLIENT_ID 24 | USER_TOKENS_FILE = ".fleetapi.user_tokens.json" 25 | 26 | # Load Environment Variables 27 | if os.path.isfile(ENV_FILE): 28 | with open(ENV_FILE, 'r') as f: 29 | for line in f: 30 | line = line.strip() 31 | if line and not line.startswith('#'): 32 | key, value = line.split('=', 1) 33 | # remove any quotes and whitespace 34 | value = value.strip().strip("'").strip('"') 35 | os.environ[key] = value 36 | 37 | CLIENT_ID=os.environ.get('CLIENT_ID', '') 38 | 39 | # Load Access Token 40 | try: 41 | with open(USER_TOKENS_FILE, 'r') as f: 42 | user_tokens = json.loads(f.read()) 43 | access_token = user_tokens['access_token'] 44 | refresh_token = user_tokens['refresh_token'] 45 | print(f"Using cached user tokens: {user_tokens}\n") 46 | except: 47 | print("No cached user tokens found, please run setup.py first.") 48 | exit(1) 49 | 50 | # Utility Function to Refresh Token 51 | def new_token(): 52 | global access_token, refresh_token 53 | print("Token expired, refreshing token...") 54 | data = { 55 | 'grant_type': 'refresh_token', 56 | 'client_id': CLIENT_ID, 57 | 'refresh_token': refresh_token 58 | } 59 | headers = { 60 | 'Content-Type': 'application/x-www-form-urlencoded' 61 | } 62 | response = requests.post('https://auth.tesla.com/oauth2/v3/token', 63 | data=data, headers=headers) 64 | # Extract access_token and refresh_token from this response 65 | access_token = response.json()['access_token'] 66 | refresh_token = response.json()['refresh_token'] 67 | print(f" Response Code: {response.status_code}") 68 | print(f" Access Token: {access_token}") 69 | print(f" Refresh Token: {refresh_token}\n") 70 | # Write both tokens to file 71 | with open(USER_TOKENS_FILE, 'w') as f: 72 | f.write(json.dumps(response.json())) 73 | 74 | # Function to poll FleetAPI 75 | def poll(api="api/1/products"): 76 | url = f"https://fleet-api.prd.na.vn.cloud.tesla.com/{api}" 77 | headers = { 78 | "Content-Type": "application/json", 79 | "Authorization": "Bearer " + access_token 80 | } 81 | response = requests.get(url, headers=headers) 82 | if response.status_code == 401: 83 | # Token expired, refresh token and try again 84 | new_token() 85 | headers = { 86 | "Content-Type": "application/json", 87 | "Authorization": "Bearer " + access_token 88 | } 89 | response = requests.get(url, headers=headers) 90 | if response.status_code == 401: 91 | print("Token expired, refresh token failed, exiting...") 92 | return None 93 | return response.json() 94 | 95 | # Get list of sites 96 | print(" Get list of Sites") 97 | site_id = "0" 98 | payload = poll("api/1/products") 99 | print(f" Response: {payload}") 100 | if payload and 'response' in payload: 101 | # Extract the site_id from the response 102 | site_id = payload['response'][0]['energy_site_id'] 103 | print(f" Site ID: {site_id}\n") 104 | else: 105 | print(" No sites found, exiting...\n") 106 | exit(1) 107 | 108 | # Get the current power information for the site. 109 | payload = poll(f"api/1/energy_sites/{site_id}/live_status") 110 | print(f" Response: {payload}\n") 111 | 112 | # Get site info 113 | payload = poll(f"api/1/energy_sites/{site_id}/site_info") 114 | print(f" Response: {payload}\n") 115 | 116 | 117 | -------------------------------------------------------------------------------- /tools/fleetapi/test.py: -------------------------------------------------------------------------------- 1 | from fleetapi import FleetAPI 2 | 3 | fleet = FleetAPI() 4 | 5 | # Current Status 6 | print(f"Solar: {fleet.solar_power()}") 7 | print(f"Grid: {fleet.grid_power()}") 8 | print(f"Load: {fleet.load_power()}") 9 | print(f"Battery: {fleet.battery_power()}") 10 | print(f"Battery Level: {fleet.battery_level()}") 11 | 12 | # Change Reserve to 30% 13 | fleet.set_battery_reserve(80) 14 | 15 | # Change Operating Mode to Autonomous 16 | fleet.set_operating_mode("self_consumption") 17 | 18 | -------------------------------------------------------------------------------- /tools/tedapi/decode.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Python module to decode tedapi protobuf data 4 | 5 | Requires: 6 | - Protobuf pip install protobuf 7 | - Generate tedapi_pb2.py with protoc --python_out=. tedapi.proto 8 | 9 | Author: Jason A. Cox 10 | For more information see https://github.com/jasonacox/pypowerwall 11 | """ 12 | 13 | import tedapi_pb2 14 | import sys 15 | 16 | FILENAME = 'request.bin' 17 | 18 | # Set filename from command line if specified 19 | filename = FILENAME 20 | if len(sys.argv) > 1: 21 | filename = sys.argv[1] 22 | 23 | # Open request or response file and read data 24 | with open(filename, 'rb') as f: 25 | data = f.read() 26 | 27 | # Decode protobuf data 28 | tedapi = tedapi_pb2.ParentMessage() 29 | tedapi.ParseFromString(data) 30 | print(tedapi) 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /tools/tedapi/tedapi.proto: -------------------------------------------------------------------------------- 1 | // Tesla tedapi API Protocol Buffer definition (tedapi.proto) 2 | // 3 | // Create tedapi_pb2.py for use in projects using the protoc compiler: 4 | // protoc --python_out=. tedapi.proto 5 | // 6 | // Author: Jason A. Cox - Date: 22 Nov 2023 - Version: 1.1 7 | // 8 | // For more information see https://github.com/jasonacox/pypowerwall 9 | 10 | syntax = "proto3"; 11 | 12 | package tedapi; 13 | 14 | // ***** Message ***** 15 | 16 | message Message { 17 | MessageEnvelope message = 1; 18 | Tail tail = 2; 19 | } 20 | 21 | message MessageEnvelope { 22 | int32 deliveryChannel = 1; 23 | Participant sender = 2; 24 | Participant recipient = 3; 25 | FirmwareType firmware = 4; 26 | optional ConfigType config = 15; 27 | optional QueryType payload = 16; 28 | } 29 | 30 | message Participant { 31 | oneof id { 32 | string din = 1; 33 | int32 teslaService = 2; 34 | int32 local = 3; 35 | int32 authorizedClient = 4; 36 | } 37 | } 38 | 39 | message Tail { 40 | int32 value = 1; 41 | } 42 | 43 | // ***** Query = 4 **** 44 | 45 | message FirmwareType { 46 | oneof id { 47 | string request = 2; 48 | FirmwarePayload system = 3; 49 | } 50 | } 51 | 52 | message FirmwarePayload { 53 | EcuId gateway = 1; 54 | string din = 2; 55 | FirmwareVersion version = 3; 56 | FirmwareFive five = 5; 57 | int32 six = 6; 58 | DeviceArray wireless = 7; 59 | bytes field8 = 8; 60 | bytes field9 = 9; 61 | } 62 | 63 | message EcuId { 64 | string partNumber = 1; 65 | string serialNumber = 2; 66 | } 67 | 68 | message FirmwareVersion { 69 | string text = 1; 70 | bytes githash = 2; 71 | } 72 | 73 | message FirmwareFive { 74 | int32 d = 2; 75 | } 76 | 77 | message DeviceArray { 78 | repeated DeviceInfo device = 1; 79 | } 80 | 81 | message DeviceInfo { 82 | StringValue company = 1; 83 | StringValue model = 2; 84 | StringValue fcc_id = 3; 85 | StringValue ic = 4; 86 | } 87 | 88 | // ***** Query = 16 ***** 89 | 90 | message QueryType { // 16 91 | optional PayloadQuerySend send = 1; 92 | optional PayloadString recv = 2; 93 | } 94 | 95 | message PayloadQuerySend { // 1 96 | optional int32 num = 1; 97 | optional PayloadString payload = 2; 98 | optional bytes code = 3; 99 | optional StringValue b = 4; 100 | } 101 | 102 | // ***** Config = 15 ***** 103 | 104 | message ConfigType { // 15 105 | oneof config { 106 | PayloadConfigSend send = 1; 107 | PayloadConfigRecv recv = 2; 108 | } 109 | } 110 | 111 | message PayloadConfigSend { // 1 112 | int32 num = 1; 113 | string file = 2; 114 | } 115 | 116 | message PayloadConfigRecv { // 2 117 | ConfigString file = 1; 118 | bytes code = 2; 119 | } 120 | 121 | message ConfigString { 122 | string name = 1; 123 | string text = 100; 124 | } 125 | 126 | // ***** General ***** 127 | 128 | message PayloadString { 129 | int32 value = 1; 130 | string text = 2; 131 | } 132 | 133 | message StringValue { 134 | string value = 1; 135 | } 136 | 137 | // ***** BASED ON RAW DECODED PAYLOADS ***** 138 | // 139 | // REQUEST - config 140 | // 1 { 141 | // 1: 1 142 | // 2 { 143 | // 3: 1 144 | // } 145 | // 3 { 146 | // 1: "1232100-00-E--TG123456789012" 147 | // } 148 | // 15 { 149 | // 1 { 150 | // 1: 1 151 | // 2: "config.json" 152 | // } 153 | // } 154 | // } 155 | // 2 { 156 | // 1: 1 157 | // } 158 | // 159 | // RESPONSE - config 160 | // 1 { 161 | // 1: 1 162 | // 2 { 163 | // 1: "1232100-00-E--TG123456789012" 164 | // } 165 | // 3 { 166 | // 3: 1 167 | // } 168 | // 15 { 169 | // 2 { 170 | // 1 { 171 | // 1: "config.json" 172 | // 100: "{}" 173 | // } 174 | // 2: "\255\177t+5\35..." 175 | // } 176 | // } 177 | // } 178 | // 2 { 179 | // 1: 1 180 | // } 181 | // 182 | // 183 | // REQUEST - query 184 | // 1 { 185 | // 1: 1 186 | // 2 { 187 | // 3: 1 188 | // } 189 | // 3 { 190 | // 1: "1232100-00-E--TG123456789012" 191 | // } 192 | // 16 { 193 | // 1 { 194 | // 1: 2 195 | // 2 { 196 | // 1: 1 197 | // 2: " query DeviceControllerQuery {..." 198 | // } 199 | // 3: "0\201\210\002B\0026\335T\310\02..." 200 | // 4 { 201 | // 1: "{}" 202 | // } 203 | // } 204 | // } 205 | // } 206 | // 2 { 207 | // 1: 1 208 | // } 209 | // 210 | // RESPONSE - query 211 | // 1 { 212 | // 1: 1 213 | // 2 { 214 | // 1: "1232100-00-E--TG123456789012" 215 | // } 216 | // 3 { 217 | // 3: 1 218 | // } 219 | // 16 { 220 | // 2 { 221 | // 1: 1 222 | // 2: "{...}" 223 | // } 224 | // } 225 | // } 226 | // 2 { 227 | // 1: 1 228 | // } 229 | // 230 | // REQUEST - firmware 231 | // 1 { 232 | // 1: 1 233 | // 2 { 234 | // 3: 1 235 | // } 236 | // 3 { 237 | // 1: "1707000-00-J--TG9999999999XP" 238 | // } 239 | // 4 { 240 | // 2: "" 241 | // } 242 | // } 243 | // 2 { 244 | // 1: 1 245 | // } 246 | // 247 | // RESPONSE - firmware 248 | // 1 { 249 | // 1: 1 250 | // 2 { 251 | // 1: "1707000-00-J--TG9999999999XP" 252 | // } 253 | // 3 { 254 | // 3: 1 255 | // } 256 | // 4 { 257 | // 3 { 258 | // 1 { 259 | // 1: "1707000-00-J" 260 | // 2: "TG9999999999XP" 261 | // } 262 | // 2: "1707000-00-J--TG9999999999XP" 263 | // 3 { 264 | // 1: "24.12.6-PW3-AFCI 008bf6ff" <--- PW3 firmware version 265 | // 2: "\000\213\366\...Redacted..." 266 | // } 267 | // 5 { 268 | // 2: 1 269 | // } 270 | // 6: 4 271 | // 7 { 272 | // 1 { 273 | // 1 { 274 | // 1: "Quectel" 275 | // } 276 | // 2 { 277 | // 1: "BG95-M2" 278 | // } 279 | // 3 { 280 | // 1: "XMR2020BG95M2" 281 | // } 282 | // 4 { 283 | // 1: "10224A-2020BG95M2" 284 | // } 285 | // } 286 | // 1 { 287 | // 1 { 288 | // 1: "Texas Instruments" 289 | // } 290 | // 2 { 291 | // 1: "WL18MODGI" 292 | // } 293 | // 3 { 294 | // 1: "Z64-WL18DBMOD" 295 | // } 296 | // 4 { 297 | // 1: "451I-WL18DBMOD" 298 | // } 299 | // } 300 | // } 301 | // 8: "\370!s\306\212...Redacted..." 302 | // 9: "\373U\353\322...Redacted..." 303 | // } 304 | // } 305 | // } 306 | // 2 { 307 | // 1: 1 308 | // } -------------------------------------------------------------------------------- /tools/tedapi/tedapi_pb2.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # Generated by the protocol buffer compiler. DO NOT EDIT! 3 | # source: tedapi.proto 4 | # Protobuf Python Version: 4.25.2 5 | """Generated protocol buffer code.""" 6 | from google.protobuf import descriptor as _descriptor 7 | from google.protobuf import descriptor_pool as _descriptor_pool 8 | from google.protobuf import symbol_database as _symbol_database 9 | from google.protobuf.internal import builder as _builder 10 | # @@protoc_insertion_point(imports) 11 | 12 | _sym_db = _symbol_database.Default() 13 | 14 | 15 | 16 | 17 | DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0ctedapi.proto\x12\x06tedapi\"O\n\x07Message\x12(\n\x07message\x18\x01 \x01(\x0b\x32\x17.tedapi.MessageEnvelope\x12\x1a\n\x04tail\x18\x02 \x01(\x0b\x32\x0c.tedapi.Tail\"\x88\x02\n\x0fMessageEnvelope\x12\x17\n\x0f\x64\x65liveryChannel\x18\x01 \x01(\x05\x12#\n\x06sender\x18\x02 \x01(\x0b\x32\x13.tedapi.Participant\x12&\n\trecipient\x18\x03 \x01(\x0b\x32\x13.tedapi.Participant\x12&\n\x08\x66irmware\x18\x04 \x01(\x0b\x32\x14.tedapi.FirmwareType\x12\'\n\x06\x63onfig\x18\x0f \x01(\x0b\x32\x12.tedapi.ConfigTypeH\x00\x88\x01\x01\x12\'\n\x07payload\x18\x10 \x01(\x0b\x32\x11.tedapi.QueryTypeH\x01\x88\x01\x01\x42\t\n\x07_configB\n\n\x08_payload\"g\n\x0bParticipant\x12\r\n\x03\x64in\x18\x01 \x01(\tH\x00\x12\x16\n\x0cteslaService\x18\x02 \x01(\x05H\x00\x12\x0f\n\x05local\x18\x03 \x01(\x05H\x00\x12\x1a\n\x10\x61uthorizedClient\x18\x04 \x01(\x05H\x00\x42\x04\n\x02id\"\x15\n\x04Tail\x12\r\n\x05value\x18\x01 \x01(\x05\"R\n\x0c\x46irmwareType\x12\x11\n\x07request\x18\x02 \x01(\tH\x00\x12)\n\x06system\x18\x03 \x01(\x0b\x32\x17.tedapi.FirmwarePayloadH\x00\x42\x04\n\x02id\"\xe0\x01\n\x0f\x46irmwarePayload\x12\x1e\n\x07gateway\x18\x01 \x01(\x0b\x32\r.tedapi.EcuId\x12\x0b\n\x03\x64in\x18\x02 \x01(\t\x12(\n\x07version\x18\x03 \x01(\x0b\x32\x17.tedapi.FirmwareVersion\x12\"\n\x04\x66ive\x18\x05 \x01(\x0b\x32\x14.tedapi.FirmwareFive\x12\x0b\n\x03six\x18\x06 \x01(\x05\x12%\n\x08wireless\x18\x07 \x01(\x0b\x32\x13.tedapi.DeviceArray\x12\x0e\n\x06\x66ield8\x18\x08 \x01(\x0c\x12\x0e\n\x06\x66ield9\x18\t \x01(\x0c\"1\n\x05\x45\x63uId\x12\x12\n\npartNumber\x18\x01 \x01(\t\x12\x14\n\x0cserialNumber\x18\x02 \x01(\t\"0\n\x0f\x46irmwareVersion\x12\x0c\n\x04text\x18\x01 \x01(\t\x12\x0f\n\x07githash\x18\x02 \x01(\x0c\"\x19\n\x0c\x46irmwareFive\x12\t\n\x01\x64\x18\x02 \x01(\x05\"1\n\x0b\x44\x65viceArray\x12\"\n\x06\x64\x65vice\x18\x01 \x03(\x0b\x32\x12.tedapi.DeviceInfo\"\x9c\x01\n\nDeviceInfo\x12$\n\x07\x63ompany\x18\x01 \x01(\x0b\x32\x13.tedapi.StringValue\x12\"\n\x05model\x18\x02 \x01(\x0b\x32\x13.tedapi.StringValue\x12#\n\x06\x66\x63\x63_id\x18\x03 \x01(\x0b\x32\x13.tedapi.StringValue\x12\x1f\n\x02ic\x18\x04 \x01(\x0b\x32\x13.tedapi.StringValue\"t\n\tQueryType\x12+\n\x04send\x18\x01 \x01(\x0b\x32\x18.tedapi.PayloadQuerySendH\x00\x88\x01\x01\x12(\n\x04recv\x18\x02 \x01(\x0b\x32\x15.tedapi.PayloadStringH\x01\x88\x01\x01\x42\x07\n\x05_sendB\x07\n\x05_recv\"\xac\x01\n\x10PayloadQuerySend\x12\x10\n\x03num\x18\x01 \x01(\x05H\x00\x88\x01\x01\x12+\n\x07payload\x18\x02 \x01(\x0b\x32\x15.tedapi.PayloadStringH\x01\x88\x01\x01\x12\x11\n\x04\x63ode\x18\x03 \x01(\x0cH\x02\x88\x01\x01\x12#\n\x01\x62\x18\x04 \x01(\x0b\x32\x13.tedapi.StringValueH\x03\x88\x01\x01\x42\x06\n\x04_numB\n\n\x08_payloadB\x07\n\x05_codeB\x04\n\x02_b\"l\n\nConfigType\x12)\n\x04send\x18\x01 \x01(\x0b\x32\x19.tedapi.PayloadConfigSendH\x00\x12)\n\x04recv\x18\x02 \x01(\x0b\x32\x19.tedapi.PayloadConfigRecvH\x00\x42\x08\n\x06\x63onfig\".\n\x11PayloadConfigSend\x12\x0b\n\x03num\x18\x01 \x01(\x05\x12\x0c\n\x04\x66ile\x18\x02 \x01(\t\"E\n\x11PayloadConfigRecv\x12\"\n\x04\x66ile\x18\x01 \x01(\x0b\x32\x14.tedapi.ConfigString\x12\x0c\n\x04\x63ode\x18\x02 \x01(\x0c\"*\n\x0c\x43onfigString\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0c\n\x04text\x18\x64 \x01(\t\",\n\rPayloadString\x12\r\n\x05value\x18\x01 \x01(\x05\x12\x0c\n\x04text\x18\x02 \x01(\t\"\x1c\n\x0bStringValue\x12\r\n\x05value\x18\x01 \x01(\tb\x06proto3') 18 | 19 | _globals = globals() 20 | _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) 21 | _builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'tedapi_pb2', _globals) 22 | if _descriptor._USE_C_DESCRIPTORS == False: 23 | DESCRIPTOR._options = None 24 | _globals['_MESSAGE']._serialized_start=24 25 | _globals['_MESSAGE']._serialized_end=103 26 | _globals['_MESSAGEENVELOPE']._serialized_start=106 27 | _globals['_MESSAGEENVELOPE']._serialized_end=370 28 | _globals['_PARTICIPANT']._serialized_start=372 29 | _globals['_PARTICIPANT']._serialized_end=475 30 | _globals['_TAIL']._serialized_start=477 31 | _globals['_TAIL']._serialized_end=498 32 | _globals['_FIRMWARETYPE']._serialized_start=500 33 | _globals['_FIRMWARETYPE']._serialized_end=582 34 | _globals['_FIRMWAREPAYLOAD']._serialized_start=585 35 | _globals['_FIRMWAREPAYLOAD']._serialized_end=809 36 | _globals['_ECUID']._serialized_start=811 37 | _globals['_ECUID']._serialized_end=860 38 | _globals['_FIRMWAREVERSION']._serialized_start=862 39 | _globals['_FIRMWAREVERSION']._serialized_end=910 40 | _globals['_FIRMWAREFIVE']._serialized_start=912 41 | _globals['_FIRMWAREFIVE']._serialized_end=937 42 | _globals['_DEVICEARRAY']._serialized_start=939 43 | _globals['_DEVICEARRAY']._serialized_end=988 44 | _globals['_DEVICEINFO']._serialized_start=991 45 | _globals['_DEVICEINFO']._serialized_end=1147 46 | _globals['_QUERYTYPE']._serialized_start=1149 47 | _globals['_QUERYTYPE']._serialized_end=1265 48 | _globals['_PAYLOADQUERYSEND']._serialized_start=1268 49 | _globals['_PAYLOADQUERYSEND']._serialized_end=1440 50 | _globals['_CONFIGTYPE']._serialized_start=1442 51 | _globals['_CONFIGTYPE']._serialized_end=1550 52 | _globals['_PAYLOADCONFIGSEND']._serialized_start=1552 53 | _globals['_PAYLOADCONFIGSEND']._serialized_end=1598 54 | _globals['_PAYLOADCONFIGRECV']._serialized_start=1600 55 | _globals['_PAYLOADCONFIGRECV']._serialized_end=1669 56 | _globals['_CONFIGSTRING']._serialized_start=1671 57 | _globals['_CONFIGSTRING']._serialized_end=1713 58 | _globals['_PAYLOADSTRING']._serialized_start=1715 59 | _globals['_PAYLOADSTRING']._serialized_end=1759 60 | _globals['_STRINGVALUE']._serialized_start=1761 61 | _globals['_STRINGVALUE']._serialized_end=1789 62 | # @@protoc_insertion_point(module_scope) 63 | -------------------------------------------------------------------------------- /tools/tedapi/test_tedapi.py: -------------------------------------------------------------------------------- 1 | # pyPowerWall - Tesla TEDAPI Class 2 | # -*- coding: utf-8 -*- 3 | """ 4 | TEDAPI Test Script 5 | 6 | This module allows you to access the Tesla Powerwall Gateway 7 | TEDAPI on 192.168.91.1 as used by the Tesla One app. 8 | 9 | Note: 10 | This module requires access to the Powerwall Gateway. You can add a route to 11 | using the command: sudo route add -host 192.168.91.1 12 | The Powerwall Gateway password is required to access the TEDAPI. 13 | 14 | Author: Jason A. Cox 15 | Date: 8 Jun 2024 16 | For more information see https://github.com/jasonacox/pypowerwall 17 | """ 18 | 19 | # Import Class 20 | from pypowerwall.tedapi import TEDAPI 21 | 22 | gw_pwd = "THEGWPASS" # Change to your Powerwall Gateway Password 23 | 24 | # Connect to Gateway 25 | gw = TEDAPI(gw_pwd) 26 | 27 | # Grab the Config and Live Status 28 | config = gw.get_config() 29 | status = gw.get_status() 30 | 31 | # Print 32 | site_info = config.get('site_info', {}) 33 | site_name = site_info.get('site_name', 'Unknown') 34 | print(f"My Site: {site_name}") 35 | meterAggregates = status.get('control', {}).get('meterAggregates', []) 36 | for meter in meterAggregates: 37 | location = meter.get('location', 'Unknown').title() 38 | realPowerW = int(meter.get('realPowerW', 0)) 39 | print(f" - {location}: {realPowerW}W") 40 | 41 | -------------------------------------------------------------------------------- /tools/tedapi/web.py: -------------------------------------------------------------------------------- 1 | # pyPowerWall - Test Web API Server for TEDAPI 2 | # -*- coding: utf-8 -*- 3 | """ 4 | Test Web API Server for TEDAPI 5 | 6 | This module allows you to access the Tesla Powerwall Gateway 7 | TEDAPI on 192.168.91.1 via a web API. 8 | 9 | Usage: python web.py 10 | 11 | Web API http://localhost:4444 12 | GET /din - Returns the Powerwall Gateway DIN number 13 | GET /config - Returns the Powerwall Gateway configuration 14 | GET /status - Returns the Powerwall Gateway status 15 | 16 | Note: 17 | This module requires access to the Powerwall Gateway. You can add a route to 18 | using the command: sudo route add -host 192.168.91.1 19 | The Powerwall Gateway password is required to access the TEDAPI. 20 | 21 | Author: Jason A. Cox 22 | Date: 1 Jun 2024 23 | For more information see https://github.com/jasonacox/pypowerwall 24 | """ 25 | 26 | from flask import Flask, jsonify 27 | from pypowerwall.tedapi import TEDAPI 28 | 29 | app = Flask(__name__) 30 | 31 | # Get gateway password from command line 32 | import sys 33 | try: 34 | gw_pwd = sys.argv[1] 35 | except IndexError: 36 | print('Usage: python web.py ') 37 | sys.exit(1) 38 | 39 | # Connect to Powerwall 40 | tedapi = TEDAPI(gw_pwd=gw_pwd) 41 | if not tedapi.din: 42 | print('Failed to connect to Powerwall') 43 | sys.exit(1) 44 | print(f"Connected to Powerwall: {tedapi.din}") 45 | 46 | # Landing Page - links to both APIs 47 | @app.route('/', methods=['GET']) 48 | def home(): 49 | return '''

pyPowerWall - TEDAPI Web Server

50 |

A prototype API for accessing the Tesla Powerwall Gateway TEDAPI.

51 |

Use the following links:

52 |

/din - Returns the Powerwall Gateway DIN number

53 |

/config - Returns the Powerwall Gateway configuration

54 |

/status - Returns the Powerwall Gateway status

55 | ''' 56 | 57 | @app.route('/din', methods=['GET']) 58 | def din(): 59 | return jsonify({'din': tedapi.din}) 60 | 61 | @app.route('/config', methods=['GET']) 62 | def config(): 63 | return jsonify(tedapi.get_config()) 64 | 65 | @app.route('/status', methods=['GET']) 66 | def status(): 67 | return jsonify(tedapi.get_status()) 68 | 69 | # Main 70 | if __name__ == '__main__': 71 | app.run(host='0.0.0.0', port=4444) 72 | # End of file 73 | 74 | -------------------------------------------------------------------------------- /web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | pyPowerwall 12 | 90 | 91 | 92 |
93 |
pyPowerwall
94 |
Python module to interface with Tesla Energy Gateways for Powerwall and Solar Power Data.
95 |
5
96 |
97 | 98 |
99 | (c) 2024 by Jason A. Cox - Open Source Project - https://github.com/jasonacox/pypowerwall 100 |
101 | 102 | 103 |
104 | 105 | 122 | 123 | --------------------------------------------------------------------------------