pax_global_header00006660000000000000000000000064150246272750014524gustar00rootroot0000000000000052 comment=55eeb30009b0ac1cd032ad23ad99236d67006a15 ovoenergy-2.0.1/000077500000000000000000000000001502462727500135415ustar00rootroot00000000000000ovoenergy-2.0.1/.codecov.yml000066400000000000000000000002651502462727500157670ustar00rootroot00000000000000codecov: branch: master coverage: status: project: default: target: 70 threshold: "0.09" patch: default: target: auto comment: false ovoenergy-2.0.1/.editorconfig000066400000000000000000000007651502462727500162260ustar00rootroot00000000000000; EditorConfig helps developers define and maintain consistent ; coding styles between different editors and IDEs. ; For more visit http://editorconfig.org. root = true ; Choose between lf or rf on "end_of_line" property [*] indent_style = space indent_size = 2 end_of_line = lf charset = utf-8 trim_trailing_whitespace = true insert_final_newline = true [*.{js,css,scss}] indent_size = 2 [*.html] indent_style = space [*.{py,qss,html,md}] indent_size = 4 [*.md] trim_trailing_whitespace = true ovoenergy-2.0.1/.github/000077500000000000000000000000001502462727500151015ustar00rootroot00000000000000ovoenergy-2.0.1/.github/CODEOWNERS000066400000000000000000000000241502462727500164700ustar00rootroot00000000000000.github/* @timmo001 ovoenergy-2.0.1/.github/FUNDING.yml000066400000000000000000000000451502462727500167150ustar00rootroot00000000000000--- github: timmo001 ko_fi: timmo001 ovoenergy-2.0.1/.github/dependabot.yaml000066400000000000000000000003561502462727500200760ustar00rootroot00000000000000--- version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: daily - package-ecosystem: "pip" directory: "/" open-pull-requests-limit: 20 schedule: interval: "daily" ovoenergy-2.0.1/.github/labels.yml000066400000000000000000000066461502462727500171020ustar00rootroot00000000000000--- - name: "breaking-change" color: ee0701 description: "A breaking change for existing users." - name: "bug" color: ee0701 description: "Inconsistencies or issues which will cause a problem for users or implementors." - name: "bugfix" color: ee0701 description: "Inconsistencies or issues which will cause a problem for users or implementors." - name: "documentation" color: 0052cc description: "Solely about the documentation of the project." - name: "enhancement" color: 1d76db description: "Enhancement of the code, not introducing new features." - name: "refactor" color: 1d76db description: "Improvement of existing code, not introducing new features." - name: "performance" color: 1d76db description: "Improving performance, not introducing new features." - name: "new-feature" color: 0e8a16 description: "New features or options." - name: "maintenance" color: 2af79e description: "Generic maintenance tasks." - name: "ci" color: 1d76db description: "Work that improves the continue integration." - name: "dependencies" color: 1d76db description: "Upgrade or downgrade of project dependencies." - name: "in-progress" color: fbca04 description: "Issue is currently being resolved by a developer." - name: "stale" color: fef2c0 description: "There has not been activity on this issue or PR for quite some time." - name: "no-stale" color: fef2c0 description: "This issue or PR is exempted from the stable bot." - name: "security" color: ee0701 description: "Marks a security issue that needs to be resolved asap." - name: "incomplete" color: fef2c0 description: "Marks a PR or issue that is missing information." - name: "invalid" color: fef2c0 description: "Marks a PR or issue that is missing information." - name: "beginner-friendly" color: 0e8a16 description: "Good first issue for people wanting to contribute to the project." - name: "help-wanted" color: 0e8a16 description: "We need some extra helping hands or expertise in order to resolve this." - name: "hacktoberfest" description: "Issues/PRs are participating in the Hacktoberfest." color: fbca04 - name: "hacktoberfest-accepted" description: "Issues/PRs are participating in the Hacktoberfest." color: fbca04 - name: "priority-critical" color: ee0701 description: "This should be dealt with ASAP. Not fixing this issue would be a serious error." - name: "priority-high" color: b60205 description: "After critical issues are fixed, these should be dealt with before any further issues." - name: "priority-medium" color: 0e8a16 description: "This issue may be useful, and needs some attention." - name: "priority-low" color: e4ea8a description: "Nice addition, maybe... someday..." - name: "major" color: b60205 description: "This PR causes a major version bump in the version number." - name: "minor" color: 0e8a16 description: "This PR causes a minor version bump in the version number." - name: "investigation" color: 0e8a16 description: "An investigation is needed to determine what causes this." - name: "waiting-for-response" color: ffee58 description: "This issue is waiting for a response." - name: "unable-to-reproduce" color: ffee58 description: "This issue cannot be reproduced. It may be configuration or system related." - name: "linux" color: ffeb3b description: "This issue is related to Linux." - name: "windows" color: 03a9f4 description: "This issue is related to Windows." ovoenergy-2.0.1/.github/release-drafter.yml000066400000000000000000000020451502462727500206720ustar00rootroot00000000000000--- name-template: "$RESOLVED_VERSION" tag-template: "$RESOLVED_VERSION" change-template: "- $TITLE @$AUTHOR (#$NUMBER)" sort-direction: ascending categories: - title: "🚨 Breaking changes" labels: - "breaking-change" - title: "✨ New features" labels: - "new-feature" - title: "πŸ› Bug fixes" labels: - "bugfix" - title: "πŸš€ Enhancements" labels: - "enhancement" - "refactor" - "performance" - title: "🧰 Maintenance" labels: - "maintenance" - "ci" - title: "πŸ“š Documentation" labels: - "documentation" - title: "⬆️ Dependency updates" labels: - "dependencies" version-resolver: major: labels: - "major" - "breaking-change" minor: labels: - "minor" - "new-feature" patch: labels: - "bugfix" - "chore" - "ci" - "dependencies" - "documentation" - "enhancement" - "performance" - "refactor" default: patch template: | ## What’s changed $CHANGES ovoenergy-2.0.1/.github/workflows/000077500000000000000000000000001502462727500171365ustar00rootroot00000000000000ovoenergy-2.0.1/.github/workflows/build.yml000066400000000000000000000006171502462727500207640ustar00rootroot00000000000000--- name: "Build" # yamllint disable-line rule:truthy on: push: branches: - master pull_request: types: - opened - reopened - synchronize workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} cancel-in-progress: true jobs: build: uses: timmo001/workflows/.github/workflows/build-python-linux.yml@master ovoenergy-2.0.1/.github/workflows/codeql.yml000066400000000000000000000007461502462727500211370ustar00rootroot00000000000000--- name: "CodeQL" # yamllint disable-line rule:truthy on: push: branches: - master pull_request: branches: - master schedule: - cron: "0 12 * * 4" workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} cancel-in-progress: true permissions: actions: read contents: read security-events: write jobs: codeql-analyze-python: uses: timmo001/workflows/.github/workflows/codeql-python.yml@master ovoenergy-2.0.1/.github/workflows/dependabot-automerge.yml000066400000000000000000000004151502462727500237540ustar00rootroot00000000000000--- name: "Dependabot - Auto-merge" # yamllint disable-line rule:truthy on: pull_request_target: permissions: pull-requests: write contents: write jobs: dependabot-automerge: uses: timmo001/workflows/.github/workflows/dependabot-automerge-any.yml@master ovoenergy-2.0.1/.github/workflows/dependency-review.yml000066400000000000000000000003361502462727500233000ustar00rootroot00000000000000--- name: "Dependency Review" # yamllint disable-line rule:truthy on: - pull_request permissions: contents: read jobs: dependency-review: uses: timmo001/workflows/.github/workflows/depedency-review.yml@master ovoenergy-2.0.1/.github/workflows/deploy.yml000066400000000000000000000076221502462727500211640ustar00rootroot00000000000000--- name: "Deploy" # yamllint disable-line rule:truthy on: release: types: - published workflow_dispatch: env: MODULE_NAME: ovoenergy jobs: deploy: name: πŸš€ Linux - Deploy Module runs-on: ubuntu-latest permissions: id-token: write steps: - name: ‡️ Check out code from GitHub uses: actions/checkout@v4.1.7 with: ref: "master" token: ${{ secrets.PUSH_TOKEN }} - name: πŸ— Set up Python uses: actions/setup-python@v5.2.0 with: python-version: "3.12" architecture: "x64" cache: "pip" - name: πŸ— Install setuptools, wheel, twine, click, twisted, incremental run: | python -m pip install --upgrade setuptools wheel twine click twisted incremental - name: πŸ”’ Get old version id: get-version-old run: | python -m pip install . # Read version from _version.py result=$(python <> $GITHUB_OUTPUT - name: πŸ”’ Set correct vertion - Developement if: ${{ github.event_name != 'release' }} run: | # If version does not contain dev, add it if [[ ! "${{ steps.get-version-old.outputs.version }}" == *"dev"* ]]; then python -m incremental.update ${{ env.MODULE_NAME }} --dev fi - name: πŸ”’ Set correct vertion - Release if: ${{ github.event_name == 'release' }} run: | # If version contains dev*, remove it if [[ "${{ steps.get-version-old.outputs.version }}" == *"dev"* ]]; then NEW_VERSION=$(echo "${{ steps.get-version-old.outputs.version }}" | sed 's/.dev.*//') python -m incremental.update ${{ env.MODULE_NAME }} --newversion $NEW_VERSION fi - name: πŸ”’ Get current version id: get-version-current run: | result=$(python <> $GITHUB_OUTPUT - name: ‡️ Pull latest changes from GitHub run: | git pull --ff - name: πŸ–Š Commit uses: stefanzweifel/git-auto-commit-action@v5.0.1 env: GITHUB_TOKEN: ${{ secrets.PUSH_TOKEN }} with: commit_message: | Bump ${{ env.MODULE_NAME }} version to ${{ steps.get-version-current.outputs.version }} - name: πŸ— Install package run: | python setup.py sdist bdist_wheel - name: Publish package distributions to PyPI uses: pypa/gh-action-pypi-publish@release/v1 with: verbose: true - name: πŸ”’ Increment version - Developement if: ${{ github.event_name != 'release' }} run: | python -m incremental.update ${{ env.MODULE_NAME }} --dev - name: πŸ”’ Increment version - Release if: ${{ github.event_name == 'release' }} run: | python -m incremental.update ${{ env.MODULE_NAME }} --patch python -m incremental.update ${{ env.MODULE_NAME }} --dev - name: πŸ”’ Get new version id: get-version-new run: | result=$(python <> $GITHUB_OUTPUT - name: ‡️ Pull latest changes from GitHub run: | git pull --ff - name: πŸ–Š Commit uses: stefanzweifel/git-auto-commit-action@v5.0.1 env: GITHUB_TOKEN: ${{ secrets.PUSH_TOKEN }} with: commit_message: | Bump ${{ env.MODULE_NAME }} version to ${{ steps.get-version-new.outputs.version }} ovoenergy-2.0.1/.github/workflows/labels.yml000066400000000000000000000004301502462727500211200ustar00rootroot00000000000000--- name: "Sync labels" # yamllint disable-line rule:truthy on: push: branches: - master paths: - .github/labels.yml schedule: - cron: "34 5 * * *" workflow_dispatch: jobs: labels: uses: timmo001/workflows/.github/workflows/labels.yml@master ovoenergy-2.0.1/.github/workflows/lint.yml000066400000000000000000000020041502462727500206230ustar00rootroot00000000000000--- name: "Lint" # yamllint disable-line rule:truthy on: push: branches: - master pull_request: types: - opened - reopened - synchronize workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} cancel-in-progress: true jobs: lint-jsonlint: uses: timmo001/workflows/.github/workflows/lint-jsonlint.yml@master lint-markdown-links: uses: timmo001/workflows/.github/workflows/lint-markdown-links.yml@master lint-markdownlint: uses: timmo001/workflows/.github/workflows/lint-markdownlint.yml@master lint-prettier: uses: timmo001/workflows/.github/workflows/lint-prettier.yml@master with: file-types: "{json,yml,yaml}" lint-pylint: uses: timmo001/workflows/.github/workflows/lint-pylint.yml@master with: module-name: ovoenergy lint-ruff: uses: timmo001/workflows/.github/workflows/lint-ruff.yml@master lint-yamllint: uses: timmo001/workflows/.github/workflows/lint-yamllint.yml@master ovoenergy-2.0.1/.github/workflows/release-drafter.yml000066400000000000000000000003431502462727500227260ustar00rootroot00000000000000--- name: "Release Drafter" # yamllint disable-line rule:truthy on: push: branches: - master workflow_dispatch: jobs: release-drafter: uses: timmo001/workflows/.github/workflows/release-drafter.yml@master ovoenergy-2.0.1/.github/workflows/test.yml000066400000000000000000000007561502462727500206500ustar00rootroot00000000000000--- name: "Test" # yamllint disable-line rule:truthy on: push: branches: - master pull_request: types: - opened - reopened - synchronize workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} cancel-in-progress: true jobs: test: uses: timmo001/workflows/.github/workflows/test-pytest.yml@master secrets: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: module-name: "ovoenergy" ovoenergy-2.0.1/.gitignore000066400000000000000000000062441502462727500155370ustar00rootroot00000000000000# Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ share/python-wheels/ *.egg-info/ .installed.cfg *.egg MANIFEST # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .nox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover *.py,cover .hypothesis/ .pytest_cache/ cover/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py db.sqlite3 db.sqlite3-journal # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder .pybuilder/ target/ # Jupyter Notebook .ipynb_checkpoints # IPython profile_default/ ipython_config.py # pyenv # For a library or package, you might want to ignore these files since the code is # intended to run in multiple environments; otherwise, check them in: # .python-version # pipenv # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. # However, in case of collaboration, if having platform-specific dependencies or dependencies # having no cross-platform support, pipenv may install dependencies that don't work, or not # install all needed dependencies. #Pipfile.lock # poetry # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. # This is especially recommended for binary packages to ensure reproducibility, and is more # commonly ignored for libraries. # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control #poetry.lock # pdm # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. #pdm.lock # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it # in version control. # https://pdm.fming.dev/#use-with-ide .pdm.toml # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm __pypackages__/ # Celery stuff celerybeat-schedule celerybeat.pid # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ .dmypy.json dmypy.json # Pyre type checker .pyre/ # pytype static type analyzer .pytype/ # Cython debug symbols cython_debug/ # PyCharm # JetBrains specific template is maintained in a separate JetBrains.gitignore that can # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ # Additional !public/app/.gitkeep !rootfs/**/downloads/ .env .qt* *.apk *.app *.db* *.deb *.dmg *.key *.rpm *setup.exe junit.xml out/ public/app/* test.json ovoenergy-2.0.1/.mdl.rb000066400000000000000000000001671502462727500147240ustar00rootroot00000000000000all rule 'MD013', :tables => false exclude_rule 'MD002' exclude_rule 'MD013' exclude_rule 'MD024' exclude_rule 'MD041' ovoenergy-2.0.1/.mdlrc000066400000000000000000000000201502462727500146330ustar00rootroot00000000000000style '.mdl.rb' ovoenergy-2.0.1/.vscode/000077500000000000000000000000001502462727500151025ustar00rootroot00000000000000ovoenergy-2.0.1/.vscode/launch.json000066400000000000000000000004141502462727500172460ustar00rootroot00000000000000{ "version": "0.2.0", "configurations": [ { "name": "Module", "type": "debugpy", "request": "launch", "module": "ovoenergy", "preLaunchTask": "Module: pip install", "justMyCode": false, "args": ["version"] } ] } ovoenergy-2.0.1/.vscode/settings.json000066400000000000000000000000541502462727500176340ustar00rootroot00000000000000{ "python.formatting.provider": "black" } ovoenergy-2.0.1/.vscode/tasks.json000066400000000000000000000003011502462727500171140ustar00rootroot00000000000000{ "version": "2.0.0", "tasks": [ { "type": "shell", "label": "Module: pip install", "command": "pip install .", "dependsOn": [], "options": {} } ] } ovoenergy-2.0.1/.yamllint.yml000066400000000000000000000001611502462727500161710ustar00rootroot00000000000000--- extends: default rules: line-length: ignore: | .gitlab-ci.yml .github/ level: warning ovoenergy-2.0.1/LICENSE000066400000000000000000000261351502462727500145550ustar00rootroot00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ovoenergy-2.0.1/README.md000066400000000000000000000002151502462727500150160ustar00rootroot00000000000000# ovoenergy Get energy data from OVO's API. This is a reverse-engineered API for OVO's energy data. It is not officially supported by OVO. ovoenergy-2.0.1/build.json000066400000000000000000000005571502462727500155420ustar00rootroot00000000000000{ "build_from": { "aarch64": "ghcr.io/timmo001/container-debian-base/aarch64:stable", "amd64": "ghcr.io/timmo001/container-debian-base/amd64:stable", "armhf": "ghcr.io/timmo001/container-debian-base/armhf:stable", "armv7": "ghcr.io/timmo001/container-debian-base/armv7:stable", "i386": "ghcr.io/timmo001/container-debian-base/i386:stable" } } ovoenergy-2.0.1/mlc_config.json000066400000000000000000000004111502462727500165300ustar00rootroot00000000000000{ "ignorePatterns": [ { "pattern": "^http://localhost" }, { "pattern": "^aidan@timmo" }, { "pattern": "^https://www.buymeacoffee.com" }, { "pattern": "^https://microbadger.com" } ], "retryOn429": true } ovoenergy-2.0.1/ovoenergy/000077500000000000000000000000001502462727500155565ustar00rootroot00000000000000ovoenergy-2.0.1/ovoenergy/__init__.py000066400000000000000000000527331502462727500177010ustar00rootroot00000000000000"""Get energy data from OVO's API.""" import contextlib from datetime import datetime, timedelta from http.cookies import SimpleCookie import logging from typing import Literal from uuid import UUID import aiohttp from .exceptions import ( OVOEnergyAPINoCookies, OVOEnergyAPINotAuthorized, OVOEnergyNoAccount, ) from .models import ( OVOCost, OVODailyElectricity, OVODailyGas, OVODailyUsage, OVOHalfHour, OVOHalfHourUsage, OVOInterval, OVOMeterReadings, ) from .models.accounts import Account, BootstrapAccounts, Supply, SupplyPointInfo from .models.carbon_intensity import OVOCarbonIntensity, OVOCarbonIntensityForecast from .models.footprint import ( OVOCarbonFootprint, OVOFootprint, OVOFootprintBreakdown, OVOFootprintElectricity, OVOFootprintGas, ) from .models.oauth import OAuth from .models.plan import ( OVOPlanElectricity, OVOPlanGas, OVOPlanRate, OVOPlans, OVOPlanUnitRate, ) _LOGGER = logging.getLogger(__name__) class OVOEnergy: """Class for OVOEnergy.""" custom_account_id: int | None = None def __init__( self, client_session: aiohttp.ClientSession, ) -> None: """Initilalize.""" self._client_session = client_session self._bootstrap_accounts: BootstrapAccounts | None = None self._cookies: SimpleCookie | None = None self._oauth: OAuth | None = None self._username: str | None = None @property def account_id(self) -> int | None: """Return account id.""" if ( self.custom_account_id is not None and self.account_ids is not None and self.custom_account_id not in set(self.account_ids) ): raise OVOEnergyNoAccount("Custom account not found in accounts") return ( self.custom_account_id if self.custom_account_id is not None else self._bootstrap_accounts.selected_account_id if self._bootstrap_accounts else None ) @property def account_ids(self) -> list[int] | None: """Return account ids.""" return ( self._bootstrap_accounts.account_ids if self._bootstrap_accounts else None ) @property def customer_id(self) -> UUID | None: """Return customer id.""" return ( self._bootstrap_accounts.customer_id if self._bootstrap_accounts else None ) @property def oauth(self) -> OAuth | None: """Return OAuth.""" return self._oauth @property def oauth_expired(self) -> bool: """Return True if OAuth token has expired.""" return self.oauth is None or self.oauth.expires_at < datetime.now() @property def username(self) -> str | None: """Return username.""" return self._username async def _request( self, url: str, method: Literal["GET"] | Literal["POST"], with_cookies: bool = True, with_authorization: bool = True, **kwargs, ): """Request.""" if with_cookies and self._cookies is None: raise OVOEnergyAPINoCookies("No cookies set") if with_authorization and self._oauth is None: raise OVOEnergyAPINotAuthorized("No OAuth token set") if with_authorization and self.oauth_expired: _LOGGER.debug("OAuth token expired, refreshing: %s", self.oauth) if not await self.get_token() or self.oauth is None: raise OVOEnergyAPINotAuthorized("No OAuth token set after refresh") _LOGGER.debug("OAuth token refreshed: %s", self.oauth) response = await self._client_session.request( method, url, cookies=self._cookies if with_cookies else None, headers={ "Authorization": f"Bearer {self.oauth.access_token}" if self.oauth else None } if with_authorization else None, **kwargs, ) with contextlib.suppress(aiohttp.ClientResponseError): response.raise_for_status() if with_authorization and response.status in [401, 403]: raise OVOEnergyAPINotAuthorized(f"Not authorized: {response.status}") return response async def authenticate( self, username: str, password: str, ) -> bool: """Authenticate.""" response = await self._request( "https://my.ovoenergy.com/api/v2/auth/login", "POST", json={ "username": username, "password": password, "rememberMe": True, }, with_cookies=False, with_authorization=False, ) if response.status != 200: return False json_response = await response.json() if "code" in json_response and json_response["code"] == "Unknown": return False self._cookies = response.cookies self._username = username if not await self.get_token(): return False return True async def get_token(self) -> OAuth | Literal[False]: """Get token.""" response = await self._request( "https://my.ovoenergy.com/api/v2/auth/token", "GET", with_authorization=False, ) if response.status != 200: return False json_response = await response.json() self._oauth = OAuth( access_token=json_response["accessToken"]["value"], expires_in=json_response["expiresIn"], refresh_expires_in=json_response["refreshExpiresIn"], # Set expires_at to current time plus expiresIn (minutes) expires_at=datetime.now() + timedelta(minutes=json_response["expiresIn"]), ) return self._oauth async def bootstrap_accounts(self) -> BootstrapAccounts: """Bootstrap accounts.""" response = await self._request( "https://smartpaymapi.ovoenergy.com/first-login/api/bootstrap/v2/", "GET", ) json_response = await response.json() self._bootstrap_accounts = BootstrapAccounts( account_ids=json_response["accountIds"], customer_id=UUID(json_response["customerId"]), selected_account_id=json_response["selectedAccountId"], is_first_login=json_response.get("isFirstLogin", None), accounts=[ Account( account_id=account.get("accountId", None), is_payg=account.get("isPayg", None), is_blocked=account.get("isBlocked", None), supplies=[ Supply( mpxn=supply["mpxn"], fuel=supply["fuel"], is_onboarding=supply["isOnboarding"], start=datetime.fromisoformat(supply["start"]) if supply["start"] else None, is_payg=supply["isPayg"], supply_point_info=SupplyPointInfo( meter_type=supply["supplyPointInfo"]["meterType"], meter_not_found=supply["supplyPointInfo"][ "meterNotFound" ], address=supply["supplyPointInfo"]["address"], ) if supply["supplyPointInfo"] else None, ) for supply in account["supplies"] ] if "supplies" in account else None if "supplies" in account else None, ) for account in json_response["accounts"] ] if "accounts" in json_response else None, ) return self._bootstrap_accounts async def get_daily_usage( self, date: str, ) -> OVODailyUsage: """Get daily usage data.""" if self.account_id is None: raise OVOEnergyNoAccount("No account found") ovo_usage = OVODailyUsage( electricity=None, gas=None, ) response = await self._request( f"https://smartpaymapi.ovoenergy.com/usage/api/daily/{self.account_id}?date={date}", "GET", ) json_response = await response.json() if "electricity" in json_response: electricity = json_response["electricity"] if electricity and "data" in electricity: ovo_usage.electricity = [] for usage in electricity["data"]: if usage is not None: ovo_usage.electricity.append( OVODailyElectricity( consumption=usage.get("consumption", None), interval=OVOInterval( start=datetime.fromisoformat( usage["interval"]["start"] ), end=datetime.fromisoformat( usage["interval"]["end"] ), ) if "interval" in usage else None, meter_readings=OVOMeterReadings( start=usage["meterReadings"]["start"], end=usage["meterReadings"]["end"], ) if "meterReadings" in usage else None, has_half_hour_data=usage.get("hasHalfHourData", None), cost=OVOCost( amount=usage["cost"]["amount"], currency_unit=usage["cost"]["currencyUnit"], ) if "cost" in usage else None, ) ) if "gas" in json_response: gas = json_response["gas"] if gas and "data" in gas: ovo_usage.gas = [] for usage in gas["data"]: if usage is not None: ovo_usage.gas.append( OVODailyGas( consumption=usage.get("consumption", None), volume=usage.get("volume", None), interval=OVOInterval( start=datetime.fromisoformat( usage["interval"]["start"] ), end=datetime.fromisoformat( usage["interval"]["end"] ), ) if "interval" in usage else None, meter_readings=OVOMeterReadings( start=usage["meterReadings"]["start"], end=usage["meterReadings"]["end"], ) if "meterReadings" in usage else None, has_half_hour_data=usage.get("hasHalfHourData", None), cost=OVOCost( amount=usage["cost"]["amount"], currency_unit=usage["cost"]["currencyUnit"], ), ) ) return ovo_usage async def get_half_hourly_usage( self, date: str, ) -> OVOHalfHourUsage: """Get half hourly usage data.""" if self.account_id is None: raise OVOEnergyNoAccount("No account found") ovo_usage = OVOHalfHourUsage( electricity=None, gas=None, ) response = await self._request( f"https://smartpaymapi.ovoenergy.com/usage/api/half-hourly/{self.account_id}?date={date}", "GET", ) json_response = await response.json() if "electricity" in json_response: electricity = json_response["electricity"] if electricity and "data" in electricity: ovo_usage.electricity = [] for usage in electricity["data"]: if usage is not None: ovo_usage.electricity.append( OVOHalfHour( consumption=usage["consumption"], interval=OVOInterval( start=datetime.fromisoformat( usage["interval"]["start"] ), end=datetime.fromisoformat( usage["interval"]["end"] ), ), unit=usage["unit"], ) ) if "gas" in json_response: gas = json_response["gas"] if gas and "data" in gas: ovo_usage.gas = [] for usage in gas["data"]: if usage is not None: ovo_usage.gas.append( OVOHalfHour( consumption=usage["consumption"], interval=OVOInterval( start=datetime.fromisoformat( usage["interval"]["start"] ), end=datetime.fromisoformat( usage["interval"]["end"] ), ), unit=usage["unit"], ) ) return ovo_usage async def get_plans(self) -> OVOPlans: """Get plans.""" if self.account_id is None: raise OVOEnergyNoAccount("No account found") response = await self._request( f"https://smartpaymapi.ovoenergy.com/orex/api/plans/{self.account_id}", "GET", ) json_response = await response.json() return OVOPlans( electricity=[ OVOPlanElectricity( name=json_response["electricity"]["name"], exit_fee=OVOPlanRate( amount=json_response["electricity"]["exitFee"]["amount"], currency_unit=json_response["electricity"]["exitFee"][ "currencyUnit" ], ), contract_start_date=json_response["electricity"][ "contractStartDate" ], contract_end_date=json_response["electricity"]["contractEndDate"], contract_type=json_response["electricity"]["contractType"], is_in_renewal=json_response["electricity"]["isInRenewal"], has_future_contracts=json_response["electricity"][ "hasFutureContracts" ], mpxn=json_response["electricity"]["mpxn"], msn=json_response["electricity"]["msn"], personal_projection=json_response["electricity"][ "personalProjection" ], standing_charge=OVOPlanRate( amount=json_response["electricity"]["standingCharge"]["amount"], currency_unit=json_response["electricity"]["standingCharge"][ "currencyUnit" ], ), unit_rates=[ OVOPlanUnitRate( name=unit_rate["name"], unit_rate=OVOPlanRate( amount=unit_rate["unitRate"]["amount"], currency_unit=unit_rate["unitRate"]["currencyUnit"], ), ) for unit_rate in json_response["electricity"]["unitRates"] ], ) for json_response in json_response["electricity"] ], gas=[ OVOPlanGas( name=json_response["gas"]["name"], exit_fee=OVOPlanRate( amount=json_response["gas"]["exitFee"]["amount"], currency_unit=json_response["gas"]["exitFee"]["currencyUnit"], ), contract_start_date=json_response["gas"]["contractStartDate"], contract_end_date=json_response["gas"]["contractEndDate"], contract_type=json_response["gas"]["contractType"], is_in_renewal=json_response["gas"]["isInRenewal"], has_future_contracts=json_response["gas"]["hasFutureContracts"], mpxn=json_response["gas"]["mpxn"], msn=json_response["gas"]["msn"], personal_projection=json_response["gas"]["personalProjection"], standing_charge=OVOPlanRate( amount=json_response["gas"]["standingCharge"]["amount"], currency_unit=json_response["gas"]["standingCharge"][ "currencyUnit" ], ), unit_rates=[ OVOPlanUnitRate( name=unit_rate["name"], unit_rate=OVOPlanRate( amount=unit_rate["unitRate"]["amount"], currency_unit=unit_rate["unitRate"]["currencyUnit"], ), ) for unit_rate in json_response["gas"]["unitRates"] ], ) for json_response in json_response["gas"] ] if "gas" in json_response else None, ) async def get_footprint(self) -> OVOFootprint: """Get footprint.""" if self.account_id is None: raise OVOEnergyNoAccount("No account found") response = await self._request( f"https://smartpaymapi.ovoenergy.com/carbon-api/{self.account_id}/footprint", "GET", ) json_response = await response.json() return OVOFootprint( from_=json_response["from"], to=json_response["to"], carbon_reduction_product_ids=json_response["carbonReductionProductIds"], carbon_footprint=OVOCarbonFootprint( carbon_kg=json_response["carbonFootprint"]["carbonKg"], carbon_saved_kg=json_response["carbonFootprint"]["carbonSavedKg"], k_wh=json_response["carbonFootprint"]["kWh"], breakdown=OVOFootprintBreakdown( electricity=OVOFootprintElectricity( carbon_kg=json_response["carbonFootprint"]["breakdown"][ "electricity" ]["carbonKg"], carbon_saved_kg=json_response["carbonFootprint"]["breakdown"][ "electricity" ]["carbonSavedKg"], k_wh=json_response["carbonFootprint"]["breakdown"][ "electricity" ]["kWh"], ), gas=OVOFootprintGas( carbon_kg=json_response["carbonFootprint"]["breakdown"]["gas"][ "carbonKg" ], carbon_saved_kg=json_response["carbonFootprint"]["breakdown"][ "gas" ]["carbonSavedKg"], k_wh=json_response["carbonFootprint"]["breakdown"]["gas"][ "kWh" ], ), ), ), ) async def get_carbon_intensity(self): """Get carbon intensity.""" response = await self._request( "https://smartpaymapi.ovoenergy.com/carbon-bff/carbonintensity", "GET", ) json_response = await response.json() return OVOCarbonIntensity( forecast=[ OVOCarbonIntensityForecast( time_from=forecast["from"], intensity=forecast["intensity"], level=forecast["level"], colour=forecast["colour"], colour_v2=forecast["colourV2"], ) for forecast in json_response["forecast"] ], current=json_response["current"], greentime=json_response["greentime"], ) ovoenergy-2.0.1/ovoenergy/__main__.py000066400000000000000000000136321502462727500176550ustar00rootroot00000000000000"""Main.""" import asyncio from dataclasses import asdict from datetime import datetime, timedelta import aiohttp import typer from . import OVOEnergy from ._version import __version__ app = typer.Typer() loop = asyncio.new_event_loop() asyncio.set_event_loop(loop) async def _setup_client( username: str, password: str, account: int | None = None, ) -> tuple[OVOEnergy, aiohttp.ClientSession]: """Set up OVO Energy client.""" client_session = aiohttp.ClientSession() client = OVOEnergy( client_session=client_session, ) if not await client.authenticate(username, password): typer.secho("Authentication failed", fg=typer.colors.RED) raise typer.Abort() await client.bootstrap_accounts() if account is not None: client.custom_account_id = account return (client, client_session) @app.command(name="bootstrap", short_help="Bootstrap OVO Energy") def bootstrap( username: str = typer.Option(..., help="OVO Energy username"), password: str = typer.Option(..., help="OVO Energy password"), ) -> None: """Authenticate with OVO Energy.""" [client, client_session] = loop.run_until_complete( _setup_client(username, password) ) bootstrap_accounts = loop.run_until_complete(client.bootstrap_accounts()) typer.secho( asdict(bootstrap_accounts), fg=typer.colors.GREEN, ) loop.run_until_complete(client_session.close()) @app.command(name="daily", short_help="Get daily usage from OVO Energy") def daily( username: str = typer.Option(..., help="OVO Energy username"), password: str = typer.Option(..., help="OVO Energy password"), account: int = typer.Option( None, help="OVO Energy account number (default: first account)" ), date: str = typer.Option( None, help="Date to retrieve data for (default: this month)" ), ) -> None: """Get daily usage from OVO Energy.""" if date is None: # Get this month date = datetime.now().strftime("%Y-%m") [client, client_session] = loop.run_until_complete( _setup_client(username, password, account) ) ovo_usage = loop.run_until_complete(client.get_daily_usage(date)) typer.secho( asdict(ovo_usage) if ovo_usage is not None else '{"message": "No data"}', fg=typer.colors.GREEN, ) loop.run_until_complete(client_session.close()) @app.command(name="halfhourly", short_help="Get half hourly usage from OVO Energy") def half_hourly( username: str = typer.Option(..., help="OVO Energy username"), password: str = typer.Option(..., help="OVO Energy password"), account: int = typer.Option( None, help="OVO Energy account number (default: first account)" ), date: str = typer.Option( None, help="Date to retrieve data for (default: this month)" ), ) -> None: """Get half hourly usage from OVO Energy.""" if date is None: # Get yesterday's date date = (datetime.now() - timedelta(days=1)).strftime("%Y-%m-%d") [client, client_session] = loop.run_until_complete( _setup_client(username, password, account) ) ovo_usage = loop.run_until_complete(client.get_half_hourly_usage(date)) typer.secho( asdict(ovo_usage) if ovo_usage is not None else '{"message": "No data"}', fg=typer.colors.GREEN, ) loop.run_until_complete(client_session.close()) @app.command(name="plans", short_help="Get plans from OVO Energy") def plans( username: str = typer.Option(..., help="OVO Energy username"), password: str = typer.Option(..., help="OVO Energy password"), account: int = typer.Option( None, help="OVO Energy account number (default: first account)" ), ) -> None: """Get rates from OVO Energy.""" [client, client_session] = loop.run_until_complete( _setup_client(username, password, account) ) ovo_plans = loop.run_until_complete(client.get_plans()) typer.secho( asdict(ovo_plans) if ovo_plans is not None else '{"message": "No data"}', fg=typer.colors.GREEN, ) loop.run_until_complete(client_session.close()) @app.command(name="carbon-footprint", short_help="Get carbon footprint from OVO Energy") def carbon_footprint( username: str = typer.Option(..., help="OVO Energy username"), password: str = typer.Option(..., help="OVO Energy password"), account: int = typer.Option( None, help="OVO Energy account number (default: first account)" ), ) -> None: """Get carbon footprint from OVO Energy.""" [client, client_session] = loop.run_until_complete( _setup_client(username, password, account) ) ovo_footprint = loop.run_until_complete(client.get_footprint()) typer.secho( asdict(ovo_footprint) if ovo_footprint is not None else '{"message": "No data"}', fg=typer.colors.GREEN, ) loop.run_until_complete(client_session.close()) @app.command(name="carbon-intensity", short_help="Get carbon intensity from OVO Energy") def carbon_intensity( username: str = typer.Option(..., help="OVO Energy username"), password: str = typer.Option(..., help="OVO Energy password"), account: int = typer.Option( None, help="OVO Energy account number (default: first account)" ), ) -> None: """Get carbon intensity from OVO Energy.""" [client, client_session] = loop.run_until_complete( _setup_client(username, password, account) ) ovo_carbon_intensity = loop.run_until_complete(client.get_carbon_intensity()) typer.secho( asdict(ovo_carbon_intensity) if ovo_carbon_intensity is not None else '{"message": "No data"}', fg=typer.colors.GREEN, ) loop.run_until_complete(client_session.close()) @app.command(name="version", short_help="Module Version") def version() -> None: """Display module version.""" typer.secho(__version__.public(), fg=typer.colors.CYAN) if __name__ == "__main__": app() ovoenergy-2.0.1/ovoenergy/_version.py000066400000000000000000000004201502462727500177500ustar00rootroot00000000000000""" Provides ovoenergy version information. """ # This file is auto-generated! Do not edit! # Use `python -m incremental.update ovoenergy` to change this file. from incremental import Version __version__ = Version("ovoenergy", 2, 0, 1, dev=0) __all__ = ["__version__"] ovoenergy-2.0.1/ovoenergy/exceptions.py000066400000000000000000000010361502462727500203110ustar00rootroot00000000000000"""Exceptions for the OVO Energy API.""" # Base Exceptions class OVOEnergyException(Exception): """Base exception for OVO Energy.""" class OVOEnergyNoAccount(OVOEnergyException): """Exception for no account found.""" # API Exceptions class OVOEnergyAPIException(OVOEnergyException): """Exception for API exceptions.""" class OVOEnergyAPINotAuthorized(OVOEnergyAPIException): """Exception for API client not authorized.""" class OVOEnergyAPINoCookies(OVOEnergyAPIException): """Exception for no cookies found.""" ovoenergy-2.0.1/ovoenergy/models/000077500000000000000000000000001502462727500170415ustar00rootroot00000000000000ovoenergy-2.0.1/ovoenergy/models/__init__.py000066400000000000000000000026331502462727500211560ustar00rootroot00000000000000"""Models.""" from dataclasses import dataclass from datetime import datetime @dataclass class OVOInterval: """Interval model.""" start: datetime end: datetime @dataclass class OVOMeterReadings: """Meter readings model.""" start: float end: float @dataclass class OVOCost: """Cost model.""" amount: float | None currency_unit: str | None @dataclass class OVODailyElectricity: """Daily electricity model.""" consumption: float | None interval: OVOInterval | None meter_readings: OVOMeterReadings | None has_half_hour_data: bool | None cost: OVOCost | None @dataclass class OVODailyGas: """Daily gas model.""" consumption: float | None volume: float | None interval: OVOInterval | None meter_readings: OVOMeterReadings | None has_half_hour_data: bool | None cost: OVOCost | None @dataclass class OVOHalfHour: """Half hour model.""" consumption: float interval: OVOInterval unit: str @dataclass class OVODailyUsage: """Daily usage model.""" electricity: list[OVODailyElectricity] | None gas: list[OVODailyGas] | None @dataclass class OVOHalfHourUsage: """Half hour usage model.""" electricity: list[OVOHalfHour] | None gas: list[OVOHalfHour] | None @dataclass class OVOPlan: """Plan model.""" standing_charge: float | None unit_rate: float | None tariff: str | None ovoenergy-2.0.1/ovoenergy/models/accounts.py000066400000000000000000000016411502462727500212340ustar00rootroot00000000000000"""Dataclasses for the bootstrap/accounts endpoint.""" from dataclasses import dataclass from datetime import datetime from uuid import UUID @dataclass class SupplyPointInfo: """Supply point info model.""" meter_type: str | None = None meter_not_found: bool | None = None address: list[str] | None = None @dataclass class Supply: """Supply model.""" mpxn: str | None fuel: str | None is_onboarding: bool | None start: datetime | None is_payg: bool | None supply_point_info: SupplyPointInfo | None @dataclass class Account: """Account model.""" account_id: int is_payg: bool | None is_blocked: bool | None supplies: list[Supply] | None @dataclass class BootstrapAccounts: """Bootstrap Accounts model.""" account_ids: list[int] customer_id: UUID selected_account_id: int accounts: list[Account] | None is_first_login: bool | None ovoenergy-2.0.1/ovoenergy/models/carbon_intensity.py000066400000000000000000000006621502462727500227710ustar00rootroot00000000000000"""Footprint Models.""" from dataclasses import dataclass from typing import Any @dataclass class OVOCarbonIntensityForecast: """Carbon intensity forecast model.""" time_from: str intensity: float level: str colour: str colour_v2: str @dataclass class OVOCarbonIntensity: """Carbon intensity model.""" forecast: list[OVOCarbonIntensityForecast] current: str | None greentime: Any | None ovoenergy-2.0.1/ovoenergy/models/footprint.py000066400000000000000000000015551502462727500214450ustar00rootroot00000000000000"""Footprint Models.""" from dataclasses import dataclass from typing import Any @dataclass class OVOFootprintElectricity: """Electricity footprint model.""" carbon_kg: float carbon_saved_kg: float k_wh: float @dataclass class OVOFootprintGas: """Gas footprint model.""" carbon_kg: float carbon_saved_kg: float k_wh: float @dataclass class OVOFootprintBreakdown: """Footprint breakdown model.""" electricity: OVOFootprintElectricity gas: OVOFootprintGas @dataclass class OVOCarbonFootprint: """Carbon footprint model.""" carbon_kg: float carbon_saved_kg: float k_wh: float breakdown: OVOFootprintBreakdown @dataclass class OVOFootprint: """Footprint model.""" from_: str | None to: str | None carbon_reduction_product_ids: list[Any] carbon_footprint: OVOCarbonFootprint | None ovoenergy-2.0.1/ovoenergy/models/oauth.py000066400000000000000000000003451502462727500205350ustar00rootroot00000000000000"""OAuth model.""" from dataclasses import dataclass from datetime import datetime @dataclass class OAuth: """OAuth model.""" access_token: str expires_in: int refresh_expires_in: int expires_at: datetime ovoenergy-2.0.1/ovoenergy/models/plan.py000066400000000000000000000024531502462727500203510ustar00rootroot00000000000000"""Plan Models.""" from dataclasses import dataclass from typing import Any @dataclass class OVOPlanRate: """Plan rate model.""" amount: float currency_unit: str @dataclass class OVOPlanStatus: """Plan status model.""" active: bool in_renewal: bool in_loss: bool loss_complete: bool has_future_contracts: bool @dataclass class OVOPlanUnitRate: """Unit rate model.""" name: str unit_rate: OVOPlanRate @dataclass class OVOPlanElectricity: """Plan electricity model.""" name: str exit_fee: OVOPlanRate contract_start_date: str contract_end_date: Any contract_type: str is_in_renewal: bool has_future_contracts: bool mpxn: str msn: str personal_projection: float standing_charge: OVOPlanRate unit_rates: list[OVOPlanUnitRate] @dataclass class OVOPlanGas: """Plan gas model.""" name: str exit_fee: OVOPlanRate contract_start_date: str contract_end_date: Any contract_type: str is_in_renewal: bool has_future_contracts: bool mpxn: str msn: str personal_projection: float standing_charge: OVOPlanRate unit_rates: list[OVOPlanUnitRate] @dataclass class OVOPlans: """Plan model.""" electricity: list[OVOPlanElectricity] gas: list[OVOPlanGas] | None ovoenergy-2.0.1/pyproject.toml000066400000000000000000000170411502462727500164600ustar00rootroot00000000000000[tool.black] extend-exclude = "/generated/" [tool.pylint.MAIN] py-version = "3.12" ignore = [ "tests", ] # Use a conservative default here; 2 should speed up most setups and not hurt # any too bad. Override on command line as appropriate. jobs = 2 init-hook = """\ from pathlib import Path; \ import sys; \ from pylint.config import find_default_config_files; \ sys.path.append( \ str(Path(next(find_default_config_files())).parent.joinpath('pylint/plugins')) ) \ """ load-plugins = [ "pylint.extensions.code_style", "pylint.extensions.typing", ] persistent = false extension-pkg-allow-list = [ "av.audio.stream", "av.logging", "av.stream", "ciso8601", "orjson", "cv2", ] fail-on = [ "I", ] [tool.pylint.BASIC] class-const-naming-style = "any" [tool.pylint."MESSAGES CONTROL"] # Reasons disabled: # format - handled by black # locally-disabled - it spams too much # duplicate-code - unavoidable # cyclic-import - doesn't test if both import on load # unused-argument - generic callbacks and setup methods create a lot of warnings # too-many-* - are not enforced for the sake of readability # too-few-* - same as too-many-* # abstract-method - with intro of async there are always methods missing # inconsistent-return-statements - doesn't handle raise # too-many-ancestors - it's too strict. # wrong-import-order - isort guards this disable = [ "abstract-method", "consider-alternative-union-syntax", "cyclic-import", "duplicate-code", "fixme", "format", "inconsistent-return-statements", "locally-disabled", "not-context-manager", "too-few-public-methods", "too-many-ancestors", "too-many-arguments", "too-many-boolean-expressions", "too-many-branches", "too-many-instance-attributes", "too-many-lines", "too-many-locals", "too-many-public-methods", "too-many-return-statements", "too-many-statements", "unused-argument", "wrong-import-order", ] enable = [ #"useless-suppression", # temporarily every now and then to clean them up "use-symbolic-message-instead", ] [tool.isort] profile = "black" line_length = 88 [tool.pylint.REPORTS] score = false [tool.pylint.TYPECHECK] ignored-classes = [ "_CountingAttr", # for attrs ] mixin-class-rgx = ".*[Mm]ix[Ii]n" [tool.pylint.FORMAT] expected-line-ending-format = "LF" [tool.pylint.EXCEPTIONS] overgeneral-exceptions = [ "builtins.BaseException", "builtins.Exception", ] [tool.pylint.TYPING] runtime-typing = false [tool.pylint.CODE_STYLE] max-line-length-suggestions = 72 [tool.pytest.ini_options] testpaths = [ "tests", ] norecursedirs = [ ".git", "testing_config", ] log_format = "%(asctime)s.%(msecs)03d %(levelname)-8s %(threadName)s %(name)s:%(filename)s:%(lineno)s %(message)s" log_date_format = "%Y-%m-%d %H:%M:%S" asyncio_mode = "auto" [tool.ruff] select = [ "B002", # Python does not support the unary prefix increment "B007", # Loop control variable {name} not used within loop body "B014", # Exception handler with duplicate exception "B023", # Function definition does not bind loop variable {name} "B026", # Star-arg unpacking after a keyword argument is strongly discouraged "C", # complexity "COM818", # Trailing comma on bare tuple prohibited "D", # docstrings "DTZ003", # Use datetime.now(tz=) instead of datetime.utcnow() "DTZ004", # Use datetime.fromtimestamp(ts, tz=) instead of datetime.utcfromtimestamp(ts) "E", # pycodestyle "F", # pyflakes/autoflake "G", # flake8-logging-format "I", # isort "ICN001", # import concentions; {name} should be imported as {asname} "ISC001", # Implicitly concatenated string literals on one line "N804", # First argument of a class method should be named cls "N805", # First argument of a method should be named self "N815", # Variable {name} in class scope should not be mixedCase "PGH001", # No builtin eval() allowed "PGH004", # Use specific rule codes when using noqa "PLC", # pylint "PLC0414", # Useless import alias. Import alias does not rename original package. "PLE", # pylint "PLR", # pylint "PLW", # pylint "Q000", # Double quotes found but single quotes preferred "RUF006", # Store a reference to the return value of asyncio.create_task "S102", # Use of exec detected "S103", # bad-file-permissions "S108", # hardcoded-temp-file "S306", # suspicious-mktemp-usage "S307", # suspicious-eval-usage "S313", # suspicious-xmlc-element-tree-usage "S314", # suspicious-xml-element-tree-usage "S315", # suspicious-xml-expat-reader-usage "S316", # suspicious-xml-expat-builder-usage "S317", # suspicious-xml-sax-usage "S318", # suspicious-xml-mini-dom-usage "S319", # suspicious-xml-pull-dom-usage "S320", # suspicious-xmle-tree-usage "S601", # paramiko-call "S602", # subprocess-popen-with-shell-equals-true "S604", # call-with-shell-equals-true "S608", # hardcoded-sql-expression "S609", # unix-command-wildcard-injection "SIM105", # Use contextlib.suppress({exception}) instead of try-except-pass "SIM117", # Merge with-statements that use the same scope "SIM118", # Use {key} in {dict} instead of {key} in {dict}.keys() "SIM201", # Use {left} != {right} instead of not {left} == {right} "SIM208", # Use {expr} instead of not (not {expr}) "SIM212", # Use {a} if {a} else {b} instead of {b} if not {a} else {a} "SIM300", # Yoda conditions. Use 'age == 42' instead of '42 == age'. "SIM401", # Use get from dict with default instead of an if block "T100", # Trace found: {name} used "T20", # flake8-print "TID251", # Banned imports "TRY004", # Prefer TypeError exception for invalid type "TRY200", # Use raise from to specify exception cause "TRY302", # Remove exception handler; error is immediately re-raised "UP", # pyupgrade "W", # pycodestyle ] ignore = [ "D202", # No blank lines allowed after function docstring "D203", # 1 blank line required before class docstring "D213", # Multi-line docstring summary should start at the second line "D406", # Section name should end with a newline "D407", # Section name underlining "E501", # line too long "E731", # do not assign a lambda expression, use a def "PLC0208", # Use a sequence type instead of a `set` when iterating over values "PLR0911", # Too many return statements ({returns} > {max_returns}) "PLR0912", # Too many branches ({branches} > {max_branches}) "PLR0913", # Too many arguments to function call ({c_args} > {max_args}) "PLR0915", # Too many statements ({statements} > {max_statements}) "PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable "PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target "UP006", # keep type annotation style as is "UP007", # keep type annotation style as is "UP038", # Use `X | Y` in `isinstance` call instead of `(X, Y)` ] [tool.ruff.flake8-import-conventions.extend-aliases] voluptuous = "vol" [tool.ruff.flake8-pytest-style] fixture-parentheses = false [tool.ruff.flake8-tidy-imports.banned-api] "async_timeout".msg = "use asyncio.timeout instead" "pytz".msg = "use zoneinfo instead" [tool.ruff.isort] force-sort-within-sections = true combine-as-imports = true split-on-trailing-comma = false [tool.ruff.per-file-ignores] "_version.py" = ["D200", "D212"] # Allow for main entry & scripts to write to stdout "script/*" = ["T20"] [tool.ruff.mccabe] max-complexity = 25 ovoenergy-2.0.1/requirements.txt000066400000000000000000000001321502462727500170210ustar00rootroot00000000000000aiohttp>=3.8.5;python_version<'3.12' aiohttp>=3.9.0b0;python_version>='3.12' typer>=0.6.1 ovoenergy-2.0.1/requirements_setup.txt000066400000000000000000000000241502462727500202410ustar00rootroot00000000000000incremental==24.7.2 ovoenergy-2.0.1/requirements_test.txt000066400000000000000000000002561502462727500200670ustar00rootroot00000000000000aioresponses==0.7.6 pytest-aiohttp==1.0.5 pytest-asyncio==0.24.0 pytest-cov==5.0.0 pytest-socket==0.7.0 pytest-sugar==1.0.0 pytest-timeout==2.3.1 pytest==8.3.2 syrupy==4.7.1 ovoenergy-2.0.1/setup.py000066400000000000000000000016371502462727500152620ustar00rootroot00000000000000"""Setup.""" from setuptools import find_packages, setup # Get setup packages from requirements.txt with open("requirements_setup.txt", encoding="utf-8") as f: requirements_setup = f.read().splitlines() # Get packages from requirements.txt with open("requirements.txt", encoding="utf-8") as f: requirements = f.read().splitlines() with open("README.md", encoding="utf-8") as f: readme = f.read() setup( name="ovoenergy", author="Aidan Timson (Timmo)", author_email="aidan@timmo.dev", description="OVO Energy", keywords="python,ovoenergy,api", license="Apache-2.0", long_description=readme, long_description_content_type="text/markdown", url="https://github.com/timmo001/ovoenergy", install_requires=requirements, packages=find_packages(exclude=["tests", "generator"]), python_requires=">=3.11", setup_requires=requirements_setup, use_incremental=True, ) ovoenergy-2.0.1/tests/000077500000000000000000000000001502462727500147035ustar00rootroot00000000000000ovoenergy-2.0.1/tests/__init__.py000066400000000000000000000066251502462727500170250ustar00rootroot00000000000000"""Setup for tests.""" from typing import Final USERNAME: Final[str] = "test" PASSWORD: Final[str] = "test" ACCOUNT: Final[int] = 123456789 ACCOUNT_BAD: Final[int] = 654321789 RESPONSE_JSON_BASIC: Final[dict] = {"test": "test"} RESPONSE_JSON_AUTH: Final[dict] = {"code": "test"} RESPONSE_JSON_TOKEN: Final[dict] = { "accessToken": {"value": "test"}, "expiresIn": 3600, "refreshExpiresIn": 0, } RESPONSE_JSON_BOOTSTRAP_ACCOUNTS: Final[dict] = { "accountIds": [ACCOUNT], "customerId": "5cafe9c4-a942-46b5-a67c-5882eba0a03c", "selectedAccountId": ACCOUNT, "accounts": [ { "accountId": ACCOUNT, "isPayg": False, "isBlocked": False, "supplies": [ { "mpxn": "3456766576", "fuel": "gas", "isOnboarding": False, "start": "2024-01-01T23:00:00Z", "isPayg": False, "supplyPointInfo": { "meterType": "AB123", "meterNotFound": False, "address": ["ADDR"], }, }, { "mpxn": "4536756746", "fuel": "electricity", "isOnboarding": False, "start": "2024-01-01T23:00:00Z", "isPayg": False, "supplyPointInfo": { "meterType": "AB123", "meterNotFound": False, "address": ["ADDR"], }, }, ], } ], "isFirstLogin": False, } RESPONSE_JSON_DAILY_USAGE: Final[dict] = { "electricity": [ { "consumption": 10.24, "interval": { "start": "2024-01-01T00:00:00Z", "end": "2024-01-01T23:59:59.999000Z", }, "meterReadings": None, "hasHalfHourData": None, "cost": {"amount": "2.94", "currencyUnit": "GBP"}, } ], "gas": [ { "consumption": 14.68, "volume": None, "interval": { "start": "2024-01-01T00:00:00Z", "end": "2024-01-01T23:59:59.999000Z", }, "meterReadings": None, "hasHalfHourData": None, "cost": {"amount": "2.56", "currencyUnit": "GBP"}, }, ], } RESPONSE_JSON_PLANS: Final[dict] = { "electricity": [], "gas": [], } RESPONSE_JSON_FOOTPRINT: Final[dict] = { "from": "2024-01-01T00:00:00Z", "to": "2024-01-01T23:59:59.999000Z", "carbonReductionProductIds": [], "carbonFootprint": { "carbonKg": 2200.1234, "carbonSavedKg": 0.0, "kWh": 1578.3246, "breakdown": { "electricity": { "carbonKg": 200.1234, "carbonSavedKg": 230.02, "kWh": 65645.92, }, "gas": { "carbonKg": 2000.1234, "carbonSavedKg": 340.02, "kWh": 10664.74363579, }, }, }, } RESPONSE_JSON_INTENSITY: Final[dict] = { "forecast": [ { "from": "2pm", "intensity": 82, "level": "low", "colour": "#0A9928", "colourV2": "#0D8426", }, ], "current": "low", "greentime": None, } ovoenergy-2.0.1/tests/__snapshots__/000077500000000000000000000000001502462727500175215ustar00rootroot00000000000000ovoenergy-2.0.1/tests/__snapshots__/test__init__.ambr000066400000000000000000000043511502462727500230260ustar00rootroot00000000000000# serializer version: 1 # name: test_authorize[authorize_account_id] None # --- # name: test_authorize[authorize_customer_id] None # --- # name: test_authorize[authorize_oauth_access_token] 'test' # --- # name: test_authorize[authorize_oauth_expires_in] 3600 # --- # name: test_authorize[authorize_oauth_refresh_expires_in] 0 # --- # name: test_authorize[authorize_username] 'test' # --- # name: test_bootstrap[bootstrap_accounts] BootstrapAccounts(account_ids=[123456789], customer_id=UUID('5cafe9c4-a942-46b5-a67c-5882eba0a03c'), selected_account_id=123456789, accounts=[Account(account_id=123456789, is_payg=False, is_blocked=False, supplies=[Supply(mpxn='3456766576', fuel='gas', is_onboarding=False, start=datetime.datetime(2024, 1, 1, 23, 0, tzinfo=datetime.timezone.utc), is_payg=False, supply_point_info=SupplyPointInfo(meter_type='AB123', meter_not_found=False, address=['ADDR'])), Supply(mpxn='4536756746', fuel='electricity', is_onboarding=False, start=datetime.datetime(2024, 1, 1, 23, 0, tzinfo=datetime.timezone.utc), is_payg=False, supply_point_info=SupplyPointInfo(meter_type='AB123', meter_not_found=False, address=['ADDR']))])], is_first_login=False) # --- # name: test_bootstrap_custom_account[bootstrap_accounts_custom_account] OVODailyUsage(electricity=None, gas=None) # --- # name: test_get_carbon_intensity[carbon_intensity] OVOCarbonIntensity(forecast=[OVOCarbonIntensityForecast(time_from='2pm', intensity=82, level='low', colour='#0A9928', colour_v2='#0D8426')], current='low', greentime=None) # --- # name: test_get_daily_usage[daily_usage] OVODailyUsage(electricity=None, gas=None) # --- # name: test_get_footprint[footprint] OVOFootprint(from_='2024-01-01T00:00:00Z', to='2024-01-01T23:59:59.999000Z', carbon_reduction_product_ids=[], carbon_footprint=OVOCarbonFootprint(carbon_kg=2200.1234, carbon_saved_kg=0.0, k_wh=1578.3246, breakdown=OVOFootprintBreakdown(electricity=OVOFootprintElectricity(carbon_kg=200.1234, carbon_saved_kg=230.02, k_wh=65645.92), gas=OVOFootprintGas(carbon_kg=2000.1234, carbon_saved_kg=340.02, k_wh=10664.74363579)))) # --- # name: test_get_half_hourly_usage[half_hourly_usage] OVOHalfHourUsage(electricity=None, gas=None) # --- # name: test_get_plans[plans] OVOPlans(electricity=[], gas=[]) # --- ovoenergy-2.0.1/tests/conftest.py000066400000000000000000000045351502462727500171110ustar00rootroot00000000000000"""Fixtures for testing.""" from collections.abc import AsyncGenerator from aiohttp import ClientSession from aioresponses import aioresponses import pytest from ovoenergy import OVOEnergy from . import ( ACCOUNT, RESPONSE_JSON_AUTH, RESPONSE_JSON_BOOTSTRAP_ACCOUNTS, RESPONSE_JSON_DAILY_USAGE, RESPONSE_JSON_FOOTPRINT, RESPONSE_JSON_INTENSITY, RESPONSE_JSON_PLANS, RESPONSE_JSON_TOKEN, ) @pytest.fixture(autouse=True) def mock_aioresponse(): """Return a client session.""" with aioresponses() as mocker: mocker.post( "https://my.ovoenergy.com/api/v2/auth/login", payload=RESPONSE_JSON_AUTH, status=200, repeat=True, ) mocker.get( "https://my.ovoenergy.com/api/v2/auth/token", payload=RESPONSE_JSON_TOKEN, status=200, repeat=True, ) mocker.get( "https://smartpaymapi.ovoenergy.com/first-login/api/bootstrap/v2/", payload=RESPONSE_JSON_BOOTSTRAP_ACCOUNTS, status=200, repeat=True, ) mocker.get( f"https://smartpaymapi.ovoenergy.com/usage/api/daily/{ACCOUNT}?date=2024-01", payload=RESPONSE_JSON_DAILY_USAGE, status=200, repeat=True, ) mocker.get( f"https://smartpaymapi.ovoenergy.com/usage/api/half-hourly/{ACCOUNT}?date=2024-01-01", payload=RESPONSE_JSON_DAILY_USAGE, status=200, repeat=True, ) mocker.get( f"https://smartpaymapi.ovoenergy.com/orex/api/plans/{ACCOUNT}", payload=RESPONSE_JSON_PLANS, status=200, repeat=True, ) mocker.get( f"https://smartpaymapi.ovoenergy.com/carbon-api/{ACCOUNT}/footprint", payload=RESPONSE_JSON_FOOTPRINT, status=200, repeat=True, ) mocker.get( "https://smartpaymapi.ovoenergy.com/carbon-bff/carbonintensity", payload=RESPONSE_JSON_INTENSITY, status=200, repeat=True, ) yield mocker @pytest.fixture async def ovoenergy_client() -> AsyncGenerator[OVOEnergy, None]: """Return a OVOEnergy client.""" async with ClientSession() as session: yield OVOEnergy(client_session=session) ovoenergy-2.0.1/tests/test__init__.py000066400000000000000000000221211502462727500177120ustar00rootroot00000000000000"""Tests for the client module.""" from datetime import datetime, timedelta from aioresponses import aioresponses import pytest from syrupy.assertion import SnapshotAssertion from ovoenergy import OVOEnergy from ovoenergy.exceptions import ( OVOEnergyAPINoCookies, OVOEnergyAPINotAuthorized, OVOEnergyNoAccount, ) from . import ACCOUNT, ACCOUNT_BAD, PASSWORD, RESPONSE_JSON_AUTH, USERNAME @pytest.mark.asyncio async def test_authorize( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, snapshot: SnapshotAssertion, ) -> None: """Test authorize.""" await ovoenergy_client.authenticate(USERNAME, PASSWORD) assert not ovoenergy_client.oauth_expired assert ovoenergy_client.oauth is not None assert ovoenergy_client.oauth.access_token == snapshot( name="authorize_oauth_access_token", ) assert ovoenergy_client.oauth.expires_in == snapshot( name="authorize_oauth_expires_in", ) assert ovoenergy_client.oauth.refresh_expires_in == snapshot( name="authorize_oauth_refresh_expires_in", ) assert ovoenergy_client.account_id == snapshot( name="authorize_account_id", ) assert ovoenergy_client.account_ids == snapshot( name="authorize_account_id", ) assert ovoenergy_client.customer_id == snapshot( name="authorize_customer_id", ) assert ovoenergy_client.username == snapshot( name="authorize_username", ) @pytest.mark.asyncio async def test_bootstrap( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, snapshot: SnapshotAssertion, ) -> None: """Test bootstrap.""" await ovoenergy_client.authenticate(USERNAME, PASSWORD) assert await ovoenergy_client.bootstrap_accounts() == snapshot( name="bootstrap_accounts", ) @pytest.mark.asyncio async def test_get_daily_usage( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, snapshot: SnapshotAssertion, ) -> None: """Test get daily usage.""" with pytest.raises(OVOEnergyNoAccount): await ovoenergy_client.get_daily_usage("2024-01") await ovoenergy_client.authenticate(USERNAME, PASSWORD) await ovoenergy_client.bootstrap_accounts() assert await ovoenergy_client.get_daily_usage("2024-01") == snapshot( name="daily_usage", ) @pytest.mark.asyncio async def test_get_half_hourly_usage( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, snapshot: SnapshotAssertion, ) -> None: """Test get half hourly usage.""" with pytest.raises(OVOEnergyNoAccount): await ovoenergy_client.get_half_hourly_usage("2024-01-01") await ovoenergy_client.authenticate(USERNAME, PASSWORD) await ovoenergy_client.bootstrap_accounts() assert await ovoenergy_client.get_half_hourly_usage("2024-01-01") == snapshot( name="half_hourly_usage", ) @pytest.mark.asyncio async def test_get_plans( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, snapshot: SnapshotAssertion, ) -> None: """Test get plans.""" with pytest.raises(OVOEnergyNoAccount): await ovoenergy_client.get_plans() await ovoenergy_client.authenticate(USERNAME, PASSWORD) await ovoenergy_client.bootstrap_accounts() assert await ovoenergy_client.get_plans() == snapshot( name="plans", ) @pytest.mark.asyncio async def test_get_footprint( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, snapshot: SnapshotAssertion, ) -> None: """Test get footprint.""" with pytest.raises(OVOEnergyNoAccount): await ovoenergy_client.get_footprint() await ovoenergy_client.authenticate(USERNAME, PASSWORD) await ovoenergy_client.bootstrap_accounts() assert await ovoenergy_client.get_footprint() == snapshot( name="footprint", ) @pytest.mark.asyncio async def test_get_carbon_intensity( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, snapshot: SnapshotAssertion, ) -> None: """Test get carbon intensity.""" await ovoenergy_client.authenticate(USERNAME, PASSWORD) assert await ovoenergy_client.get_carbon_intensity() == snapshot( name="carbon_intensity", ) @pytest.mark.asyncio async def test_bootstrap_custom_account( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, snapshot: SnapshotAssertion, ) -> None: """Test bootstrap custom account.""" await ovoenergy_client.authenticate(USERNAME, PASSWORD) ovoenergy_client.custom_account_id = ACCOUNT await ovoenergy_client.bootstrap_accounts() assert await ovoenergy_client.get_daily_usage("2024-01") == snapshot( name="bootstrap_accounts_custom_account", ) @pytest.mark.asyncio async def test_bad_account( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, ) -> None: """Test bad account.""" await ovoenergy_client.authenticate(USERNAME, PASSWORD) await ovoenergy_client.bootstrap_accounts() ovoenergy_client.custom_account_id = ACCOUNT_BAD with pytest.raises(OVOEnergyNoAccount): await ovoenergy_client.get_daily_usage("2024-01") # pylint: disable=protected-access @pytest.mark.asyncio async def test_no_cookies( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, ) -> None: """Test no cookies.""" await ovoenergy_client.authenticate(USERNAME, PASSWORD) await ovoenergy_client.bootstrap_accounts() ovoenergy_client._cookies = None with pytest.raises(OVOEnergyAPINoCookies): await ovoenergy_client.get_daily_usage("2024-01") # pylint: disable=protected-access @pytest.mark.asyncio async def test_no_auth( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, ) -> None: """Test no auth.""" await ovoenergy_client.authenticate(USERNAME, PASSWORD) await ovoenergy_client.bootstrap_accounts() ovoenergy_client._oauth = None with pytest.raises(OVOEnergyAPINotAuthorized): await ovoenergy_client.get_daily_usage("2024-01") # pylint: disable=protected-access @pytest.mark.asyncio async def test_oauth_expired( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, ) -> None: """Test oauth expired.""" an_hour_ago = datetime.now() - timedelta(hours=1) await ovoenergy_client.authenticate(USERNAME, PASSWORD) await ovoenergy_client.bootstrap_accounts() assert ovoenergy_client._oauth is not None ovoenergy_client._oauth.expires_at = an_hour_ago await ovoenergy_client.get_daily_usage("2024-01") ovoenergy_client._oauth.expires_at = an_hour_ago mock_aioresponse.clear() mock_aioresponse.get( "https://my.ovoenergy.com/api/v2/auth/token", status=403, repeat=True, ) with pytest.raises(OVOEnergyAPINotAuthorized): await ovoenergy_client.get_daily_usage("2024-01") @pytest.mark.asyncio async def test_forbidden( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, ) -> None: """Test forbidden.""" await ovoenergy_client.authenticate(USERNAME, PASSWORD) await ovoenergy_client.bootstrap_accounts() mock_aioresponse.clear() mock_aioresponse.get( f"https://smartpaymapi.ovoenergy.com/usage/api/daily/{ACCOUNT}?date=2024-01", status=403, repeat=True, ) with pytest.raises(OVOEnergyAPINotAuthorized): await ovoenergy_client.get_daily_usage("2024-01") @pytest.mark.asyncio async def test_auth_not_found( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, ) -> None: """Test auth endpoint not found.""" mock_aioresponse.clear() mock_aioresponse.post( "https://my.ovoenergy.com/api/v2/auth/login", status=404, repeat=True, ) assert not await ovoenergy_client.authenticate(USERNAME, PASSWORD) @pytest.mark.asyncio async def test_auth_code_not_found( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, ) -> None: """Test auth endpoint code not found.""" mock_aioresponse.clear() mock_aioresponse.post( "https://my.ovoenergy.com/api/v2/auth/login", status=204, repeat=True, ) assert not await ovoenergy_client.authenticate(USERNAME, PASSWORD) @pytest.mark.asyncio async def test_auth_code_unknown( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, ) -> None: """Test auth token not found.""" mock_aioresponse.clear() mock_aioresponse.post( "https://my.ovoenergy.com/api/v2/auth/login", status=200, payload={"code": "Unknown"}, repeat=True, ) assert not await ovoenergy_client.authenticate(USERNAME, PASSWORD) @pytest.mark.asyncio async def test_auth_token_not_found( ovoenergy_client: OVOEnergy, mock_aioresponse: aioresponses, ) -> None: """Test auth token not found.""" mock_aioresponse.clear() mock_aioresponse.post( "https://my.ovoenergy.com/api/v2/auth/login", payload=RESPONSE_JSON_AUTH, status=200, repeat=True, ) mock_aioresponse.get( "https://my.ovoenergy.com/api/v2/auth/token", status=404, repeat=True, ) assert not await ovoenergy_client.authenticate(USERNAME, PASSWORD) ovoenergy-2.0.1/tests/test__version.py000066400000000000000000000002771502462727500201460ustar00rootroot00000000000000"""Test __version__ module.""" from aioazuredevops._version import __version__ def test__version(): """Test the __version__ string.""" assert isinstance(__version__.public(), str)