Compare commits

..

18 Commits

10 changed files with 625 additions and 105 deletions

View File

@ -1,20 +1,38 @@
image: python:latest image: python:latest
stages:
- lint
- test
- deploy
before_script:
- pip install poetry
- poetry install --no-root
lint: lint:
stage: test stage: lint
before_script:
- pip install poetry
- poetry install --no-root
script: script:
- poetry run ruff check ./src - poetry run ruff check
- poetry run ruff format --check ./src - poetry run ruff format --check
test:
stage: test
script:
- poetry run pytest -v --cov=. --cov-report=xml
after_script:
- bash <(curl -s https://codecov.io/bash)
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
deploy_to_pypi: deploy_to_pypi:
stage: deploy stage: deploy
only: only:
- master refs:
before_script: - master
- pip install poetry changes:
- poetry install --no-root - pyproject.toml
script: script:
- POETRY_PYPI_TOKEN_PYPI=$PYPI_PASSWORD poetry publish --build - POETRY_PYPI_TOKEN_PYPI=$PYPI_PASSWORD poetry publish --build

View File

@ -1,5 +1,18 @@
# UTE API Wrapper 🇺🇾 # UTE API Wrapper 🇺🇾
# THIS API NO LONGER WORKS
UTE deprecated the API that this wrapper uses on April 15th 2024. More information [here](https://github.com/rogsme/ute_homeassistant_integration/issues/3#issuecomment-2054332575).
I'll archive this repository in a few days.
<p align="center">
<img src="https://gitlab.com/uploads/-/system/project/avatar/48558040/icon.png" alt="ute-wrapper"/>
</p>
[![codecov](https://codecov.io/gl/rogs/ute/graph/badge.svg?token=D1B2J3EB6K)](https://codecov.io/gl/rogs/ute)
[![PyPI version](https://badge.fury.io/py/ute-wrapper.svg)](https://badge.fury.io/py/ute-wrapper)
This Python package provides a convenient wrapper for interacting with the [UTE (Administración Nacional de Usinas y Trasmisiones Eléctricas)](https://portal.ute.com.uy/) API in Uruguay 🇺🇾. It allows you to retrieve various information related to your UTE account, electricity consumption, network status, and more. This Python package provides a convenient wrapper for interacting with the [UTE (Administración Nacional de Usinas y Trasmisiones Eléctricas)](https://portal.ute.com.uy/) API in Uruguay 🇺🇾. It allows you to retrieve various information related to your UTE account, electricity consumption, network status, and more.
## Table of Contents ## Table of Contents
@ -45,6 +58,7 @@ ute_client = UTEClient(email, phone_number, device_id, average_cost_per_kwh, pow
- `get_current_usage_info()`: Get current usage information for the specified device ID. - `get_current_usage_info()`: Get current usage information for the specified device ID.
- `get_average_price(plan)`: Get the average price for a specific UTE plan ("triple" or "doble"). - `get_average_price(plan)`: Get the average price for a specific UTE plan ("triple" or "doble").
## Examples ## Examples
### Get Historic Consumption ### Get Historic Consumption
@ -63,7 +77,7 @@ print(current_usage_info)
## Contributing ## Contributing
Contributions are welcome! If you find a bug or have a suggestion, please create an issue or submit Merge Request on [Gitlab](https://gitlab.com/rogs/ute). Contributions are welcome! If you find a bug or have a suggestion, please create an issue or submit a Merge Request on [Gitlab](https://gitlab.com/rogs/ute).
## License ## License

173
poetry.lock generated
View File

@ -165,6 +165,84 @@ files = [
{file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"},
] ]
[[package]]
name = "colorama"
version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
]
[[package]]
name = "coverage"
version = "7.4.3"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "coverage-7.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8580b827d4746d47294c0e0b92854c85a92c2227927433998f0d3320ae8a71b6"},
{file = "coverage-7.4.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:718187eeb9849fc6cc23e0d9b092bc2348821c5e1a901c9f8975df0bc785bfd4"},
{file = "coverage-7.4.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:767b35c3a246bcb55b8044fd3a43b8cd553dd1f9f2c1eeb87a302b1f8daa0524"},
{file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae7f19afe0cce50039e2c782bff379c7e347cba335429678450b8fe81c4ef96d"},
{file = "coverage-7.4.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba3a8aaed13770e970b3df46980cb068d1c24af1a1968b7818b69af8c4347efb"},
{file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ee866acc0861caebb4f2ab79f0b94dbfbdbfadc19f82e6e9c93930f74e11d7a0"},
{file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:506edb1dd49e13a2d4cac6a5173317b82a23c9d6e8df63efb4f0380de0fbccbc"},
{file = "coverage-7.4.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd6545d97c98a192c5ac995d21c894b581f1fd14cf389be90724d21808b657e2"},
{file = "coverage-7.4.3-cp310-cp310-win32.whl", hash = "sha256:f6a09b360d67e589236a44f0c39218a8efba2593b6abdccc300a8862cffc2f94"},
{file = "coverage-7.4.3-cp310-cp310-win_amd64.whl", hash = "sha256:18d90523ce7553dd0b7e23cbb28865db23cddfd683a38fb224115f7826de78d0"},
{file = "coverage-7.4.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cbbe5e739d45a52f3200a771c6d2c7acf89eb2524890a4a3aa1a7fa0695d2a47"},
{file = "coverage-7.4.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:489763b2d037b164846ebac0cbd368b8a4ca56385c4090807ff9fad817de4113"},
{file = "coverage-7.4.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:451f433ad901b3bb00184d83fd83d135fb682d780b38af7944c9faeecb1e0bfe"},
{file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcc66e222cf4c719fe7722a403888b1f5e1682d1679bd780e2b26c18bb648cdc"},
{file = "coverage-7.4.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b3ec74cfef2d985e145baae90d9b1b32f85e1741b04cd967aaf9cfa84c1334f3"},
{file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:abbbd8093c5229c72d4c2926afaee0e6e3140de69d5dcd918b2921f2f0c8baba"},
{file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:35eb581efdacf7b7422af677b92170da4ef34500467381e805944a3201df2079"},
{file = "coverage-7.4.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8249b1c7334be8f8c3abcaaa996e1e4927b0e5a23b65f5bf6cfe3180d8ca7840"},
{file = "coverage-7.4.3-cp311-cp311-win32.whl", hash = "sha256:cf30900aa1ba595312ae41978b95e256e419d8a823af79ce670835409fc02ad3"},
{file = "coverage-7.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:18c7320695c949de11a351742ee001849912fd57e62a706d83dfc1581897fa2e"},
{file = "coverage-7.4.3-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b51bfc348925e92a9bd9b2e48dad13431b57011fd1038f08316e6bf1df107d10"},
{file = "coverage-7.4.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d6cdecaedea1ea9e033d8adf6a0ab11107b49571bbb9737175444cea6eb72328"},
{file = "coverage-7.4.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b2eccb883368f9e972e216c7b4c7c06cabda925b5f06dde0650281cb7666a30"},
{file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c00cdc8fa4e50e1cc1f941a7f2e3e0f26cb2a1233c9696f26963ff58445bac7"},
{file = "coverage-7.4.3-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b9a4a8dd3dcf4cbd3165737358e4d7dfbd9d59902ad11e3b15eebb6393b0446e"},
{file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:062b0a75d9261e2f9c6d071753f7eef0fc9caf3a2c82d36d76667ba7b6470003"},
{file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:ebe7c9e67a2d15fa97b77ea6571ce5e1e1f6b0db71d1d5e96f8d2bf134303c1d"},
{file = "coverage-7.4.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c0a120238dd71c68484f02562f6d446d736adcc6ca0993712289b102705a9a3a"},
{file = "coverage-7.4.3-cp312-cp312-win32.whl", hash = "sha256:37389611ba54fd6d278fde86eb2c013c8e50232e38f5c68235d09d0a3f8aa352"},
{file = "coverage-7.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:d25b937a5d9ffa857d41be042b4238dd61db888533b53bc76dc082cb5a15e914"},
{file = "coverage-7.4.3-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:28ca2098939eabab044ad68850aac8f8db6bf0b29bc7f2887d05889b17346454"},
{file = "coverage-7.4.3-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:280459f0a03cecbe8800786cdc23067a8fc64c0bd51dc614008d9c36e1659d7e"},
{file = "coverage-7.4.3-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c0cdedd3500e0511eac1517bf560149764b7d8e65cb800d8bf1c63ebf39edd2"},
{file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9a9babb9466fe1da12417a4aed923e90124a534736de6201794a3aea9d98484e"},
{file = "coverage-7.4.3-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dec9de46a33cf2dd87a5254af095a409ea3bf952d85ad339751e7de6d962cde6"},
{file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:16bae383a9cc5abab9bb05c10a3e5a52e0a788325dc9ba8499e821885928968c"},
{file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:2c854ce44e1ee31bda4e318af1dbcfc929026d12c5ed030095ad98197eeeaed0"},
{file = "coverage-7.4.3-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:ce8c50520f57ec57aa21a63ea4f325c7b657386b3f02ccaedeccf9ebe27686e1"},
{file = "coverage-7.4.3-cp38-cp38-win32.whl", hash = "sha256:708a3369dcf055c00ddeeaa2b20f0dd1ce664eeabde6623e516c5228b753654f"},
{file = "coverage-7.4.3-cp38-cp38-win_amd64.whl", hash = "sha256:1bf25fbca0c8d121a3e92a2a0555c7e5bc981aee5c3fdaf4bb7809f410f696b9"},
{file = "coverage-7.4.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b253094dbe1b431d3a4ac2f053b6d7ede2664ac559705a704f621742e034f1f"},
{file = "coverage-7.4.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:77fbfc5720cceac9c200054b9fab50cb2a7d79660609200ab83f5db96162d20c"},
{file = "coverage-7.4.3-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6679060424faa9c11808598504c3ab472de4531c571ab2befa32f4971835788e"},
{file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4af154d617c875b52651dd8dd17a31270c495082f3d55f6128e7629658d63765"},
{file = "coverage-7.4.3-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8640f1fde5e1b8e3439fe482cdc2b0bb6c329f4bb161927c28d2e8879c6029ee"},
{file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:69b9f6f66c0af29642e73a520b6fed25ff9fd69a25975ebe6acb297234eda501"},
{file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:0842571634f39016a6c03e9d4aba502be652a6e4455fadb73cd3a3a49173e38f"},
{file = "coverage-7.4.3-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a78ed23b08e8ab524551f52953a8a05d61c3a760781762aac49f8de6eede8c45"},
{file = "coverage-7.4.3-cp39-cp39-win32.whl", hash = "sha256:c0524de3ff096e15fcbfe8f056fdb4ea0bf497d584454f344d59fce069d3e6e9"},
{file = "coverage-7.4.3-cp39-cp39-win_amd64.whl", hash = "sha256:0209a6369ccce576b43bb227dc8322d8ef9e323d089c6f3f26a597b09cb4d2aa"},
{file = "coverage-7.4.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:7cbde573904625509a3f37b6fecea974e363460b556a627c60dc2f47e2fffa51"},
{file = "coverage-7.4.3.tar.gz", hash = "sha256:276f6077a5c61447a48d133ed13e759c09e62aff0dc84274a68dc18660104d52"},
]
[package.dependencies]
tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""}
[package.extras]
toml = ["tomli"]
[[package]] [[package]]
name = "distlib" name = "distlib"
version = "0.3.8" version = "0.3.8"
@ -261,6 +339,17 @@ docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.link
perf = ["ipython"] perf = ["ipython"]
testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"] testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)", "pytest-ruff"]
[[package]]
name = "iniconfig"
version = "2.0.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.7"
files = [
{file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"},
{file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"},
]
[[package]] [[package]]
name = "jedi" name = "jedi"
version = "0.19.1" version = "0.19.1"
@ -309,6 +398,17 @@ files = [
[package.dependencies] [package.dependencies]
setuptools = "*" setuptools = "*"
[[package]]
name = "packaging"
version = "23.2"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.7"
files = [
{file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"},
{file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"},
]
[[package]] [[package]]
name = "parso" name = "parso"
version = "0.8.3" version = "0.8.3"
@ -372,6 +472,63 @@ nodeenv = ">=0.11.1"
pyyaml = ">=5.1" pyyaml = ">=5.1"
virtualenv = ">=20.10.0" virtualenv = ">=20.10.0"
[[package]]
name = "pytest"
version = "8.1.1"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-8.1.1-py3-none-any.whl", hash = "sha256:2a8386cfc11fa9d2c50ee7b2a57e7d898ef90470a7a34c4b949ff59662bb78b7"},
{file = "pytest-8.1.1.tar.gz", hash = "sha256:ac978141a75948948817d360297b7aae0fcb9d6ff6bc9ec6d514b85d5a65c044"},
]
[package.dependencies]
colorama = {version = "*", markers = "sys_platform == \"win32\""}
exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""}
iniconfig = "*"
packaging = "*"
pluggy = ">=1.4,<2.0"
tomli = {version = ">=1", markers = "python_version < \"3.11\""}
[package.extras]
testing = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"]
[[package]]
name = "pytest-cov"
version = "4.1.0"
description = "Pytest plugin for measuring coverage."
optional = false
python-versions = ">=3.7"
files = [
{file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"},
{file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"},
]
[package.dependencies]
coverage = {version = ">=5.2.1", extras = ["toml"]}
pytest = ">=4.6"
[package.extras]
testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"]
[[package]]
name = "pytest-mock"
version = "3.12.0"
description = "Thin-wrapper around the mock package for easier use with pytest"
optional = false
python-versions = ">=3.8"
files = [
{file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"},
{file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"},
]
[package.dependencies]
pytest = ">=5.0"
[package.extras]
dev = ["pre-commit", "pytest-asyncio", "tox"]
[[package]] [[package]]
name = "python-lsp-jsonrpc" name = "python-lsp-jsonrpc"
version = "1.1.2" version = "1.1.2"
@ -577,20 +734,6 @@ files = [
{file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"},
] ]
[[package]]
name = "types-requests"
version = "2.31.0.20240218"
description = "Typing stubs for requests"
optional = false
python-versions = ">=3.8"
files = [
{file = "types-requests-2.31.0.20240218.tar.gz", hash = "sha256:f1721dba8385958f504a5386240b92de4734e047a08a40751c1654d1ac3349c5"},
{file = "types_requests-2.31.0.20240218-py3-none-any.whl", hash = "sha256:a82807ec6ddce8f00fe0e949da6d6bc1fbf1715420218a9640d695f70a9e5a9b"},
]
[package.dependencies]
urllib3 = ">=2"
[[package]] [[package]]
name = "typing-extensions" name = "typing-extensions"
version = "4.10.0" version = "4.10.0"
@ -731,4 +874,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p
[metadata] [metadata]
lock-version = "2.0" lock-version = "2.0"
python-versions = ">=3.9" python-versions = ">=3.9"
content-hash = "08191dc410632616a7f40322602fbd6b8b935ed7aad99c0fd9708e90fec9d448" content-hash = "3af2371329597b4b40f1b2b56d61cbca6009d76b97f1283b64475aee7261fc35"

View File

@ -1,7 +1,7 @@
[tool.poetry] [tool.poetry]
name = "ute-wrapper" name = "ute-wrapper"
version = "2.2.0" version = "2.5.1"
description = "A wrapper to interact with UTE's API" description = "[DEPRECATED] A wrapper to interact with UTE's API"
authors = ["Roger Gonzalez <roger@rogs.me>"] authors = ["Roger Gonzalez <roger@rogs.me>"]
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
readme = "README.md" readme = "README.md"
@ -17,13 +17,15 @@ classifiers = [
[tool.poetry.dependencies] [tool.poetry.dependencies]
python = ">=3.9" python = ">=3.9"
requests = "^2.31.0" requests = ">=2.0.0, <3.0.0"
types-requests = "^2.31.0.20240218"
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]
python-lsp-server = "^1.10.0" python-lsp-server = "^1.10.0"
python-lsp-ruff = "^2.2.0" python-lsp-ruff = "^2.2.0"
pre-commit = "^3.6.2" pre-commit = "^3.6.2"
pytest = "^8.1.1"
pytest-mock = "^3.12.0"
pytest-cov = "^4.1.0"
[build-system] [build-system]
requires = ["poetry-core"] requires = ["poetry-core"]
@ -66,7 +68,7 @@ target-version = "py39"
[tool.ruff.lint] [tool.ruff.lint]
select = ["E", "F", "T20", "PL", "B", "A", "C4", "TID", "ERA", "RET", "W", "C90", "ARG", "Q", "FLY", "SIM", "COM", "D"] select = ["E", "F", "T20", "PL", "B", "A", "C4", "TID", "ERA", "RET", "W", "C90", "ARG", "Q", "FLY", "SIM", "COM", "D"]
ignore = ["E402"] ignore = ["E402", "PLW2901", "COM812", "PLR2004"]
[tool.ruff.lint.pylint] [tool.ruff.lint.pylint]
max-args = 6 max-args = 6

View File

@ -1,4 +1,6 @@
"""Constants used in the application.""" """Constants used in the application."""
HTTP_200_OK = 200 TRIPHASIC = 3
API_VERSION_1 = "https://rocme.ute.com.uy/api/v1"
API_VERSION_2 = "https://rocme.ute.com.uy/api/v2"
TRIPHASIC = 3 TRIPHASIC = 3

View File

@ -1,4 +1,4 @@
"""This module contains all the exceptions used in the project.""" """Custom exceptions for the UTE wrapper."""
class InvalidPowerFactorException(Exception): class InvalidPowerFactorException(Exception):
@ -14,7 +14,7 @@ class MultipleDevicesException(Exception):
class UnsupportedMethodException(Exception): class UnsupportedMethodException(Exception):
"""Raised when an unsupported method is used.""" """Raised when an unsupported HTTP method is used."""
pass pass
@ -25,12 +25,6 @@ class InvalidPlanException(Exception):
pass pass
class TariffException(Exception):
"""Raised when the tariff is not valid."""
pass
class ReadingRequestFailedException(Exception): class ReadingRequestFailedException(Exception):
"""Raised when the reading request fails.""" """Raised when the reading request fails."""
@ -41,3 +35,9 @@ class ReadingResponseInvalidException(Exception):
"""Raised when the reading response is invalid.""" """Raised when the reading response is invalid."""
pass pass
class TariffException(Exception):
"""Raised when the tariff is not valid."""
pass

26
src/ute_wrapper/models.py Normal file
View File

@ -0,0 +1,26 @@
"""Models for the UTE Wrapper."""
from typing import TypedDict
class EnergyEntry(TypedDict):
"""Energy entry dict."""
kwh: float
aproximated_cost_in_uyu: float
day_in_week: str
class TotalEntry(TypedDict, total=False):
"""Total entry dict."""
sum_in_kwh: float
aproximated_cost_in_uyu: float
daily_average_cost: float
class ActiveEnergy(TypedDict, total=False):
"""Active energy dict."""
total: TotalEntry
dates: dict[str, EnergyEntry]

View File

@ -21,11 +21,11 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import time import time
from datetime import datetime, timedelta from datetime import datetime, timedelta
from time import sleep from time import sleep
from typing import Dict, List, Optional, TypedDict from typing import Optional
import requests import requests
from .constants import HTTP_200_OK, TRIPHASIC from .constants import API_VERSION_1, API_VERSION_2, TRIPHASIC
from .exceptions import ( from .exceptions import (
InvalidPlanException, InvalidPlanException,
InvalidPowerFactorException, InvalidPowerFactorException,
@ -35,53 +35,11 @@ from .exceptions import (
TariffException, TariffException,
UnsupportedMethodException, UnsupportedMethodException,
) )
from .models import ActiveEnergy
API_VERSION_1 = "https://rocme.ute.com.uy/api/v1"
API_VERSION_2 = "https://rocme.ute.com.uy/api/v2"
class EnergyEntry(TypedDict):
"""Energy entry dict."""
kwh: float
aproximated_cost_in_uyu: float
day_in_week: str
class TotalEntry(TypedDict, total=False):
"""Total entry dict."""
sum_in_kwh: float
aproximated_cost_in_uyu: float
daily_average_cost: float
class ActiveEnergy(TypedDict, total=False):
"""Active energy dict."""
total: TotalEntry
dates: Dict[str, EnergyEntry]
class UTEClient: class UTEClient:
""" """UTE (Administración Nacional de Usinas y Trasmisiones Eléctricas) API Wrapper."""
UTE (Administración Nacional de Usinas y Trasmisiones Eléctricas) API Wrapper.
Args:
email (str): User email for authentication
phone_number (str): User phone number for authentication
device_id (str): UTE Device id
average_cost_per_kwh (float): Average cost per kwh
power_factor (float): Power factor
Raises:
InvalidPowerFactorException: If the power factor is not between 0 and 1
MultipleDevicesException: If there are multiple devices associated with the account
UnsupportedMethodException: If an unsupported method is used
InvalidPlanException: If the plan is not valid
ReadingRequestException: If the reading request is not valid
TariffException: If the tariff is not valid
"""
def __init__( def __init__(
self, self,
@ -113,8 +71,8 @@ class UTEClient:
self.phone_number = phone_number self.phone_number = phone_number
self.device_id = device_id self.device_id = device_id
self.average_cost_per_kwh = average_cost_per_kwh self.average_cost_per_kwh = average_cost_per_kwh
self.authorization = self._login()
self.power_factor = power_factor self.power_factor = power_factor
self.authorization = None
self._validate_power_factor() self._validate_power_factor()
self._initialize_device_id() self._initialize_device_id()
self._initialize_average_cost_per_kwh() self._initialize_average_cost_per_kwh()
@ -154,7 +112,7 @@ class UTEClient:
delay: float = 2, delay: float = 2,
) -> requests.Response: ) -> requests.Response:
""" """
Make a HTTP request with retries. Make a HTTP request with retries and handle expired authorization.
Args: Args:
method (str): The HTTP method to use. Accepted methods are ``GET``, ``POST``. method (str): The HTTP method to use. Accepted methods are ``GET``, ``POST``.
@ -176,20 +134,20 @@ class UTEClient:
"Connection": "Keep-Alive", "Connection": "Keep-Alive",
} }
try: for attempt in range(retries):
if self.authorization: if self.authorization:
headers["Authorization"] = f"Bearer {self.authorization}" headers["Authorization"] = f"Bearer {self.authorization}"
except AttributeError:
pass
for attempt in range(retries):
try: try:
response = getattr(requests, method.lower(), self._method_not_supported)(url, headers=headers, json=data) response = getattr(requests, method.lower(), self._method_not_supported)(url, headers=headers, json=data)
if response.status_code == requests.codes.unauthorized:
self._login()
continue
response.raise_for_status() response.raise_for_status()
return response return response
except (requests.RequestException, Exception) as e: except (requests.RequestException, Exception):
if attempt == retries - 1: if attempt == retries - 1:
raise e break
time.sleep(delay) time.sleep(delay)
raise Exception("All retries failed.") raise Exception("All retries failed.")
@ -201,10 +159,6 @@ class UTEClient:
""" """
Login to UTE. Login to UTE.
Args:
email (str): User email for authentication
phone_number (str): User phone number for authentication
Returns: Returns:
str: Authorization token str: Authorization token
""" """
@ -214,15 +168,20 @@ class UTEClient:
"PhoneNumber": self.phone_number, "PhoneNumber": self.phone_number,
} }
return self._make_request("POST", url, data=data).text response = self._make_request("POST", url, data=data)
self.authorization = response.text
return self.authorization
def get_devices_list(self) -> List[dict]: def get_devices_list(self) -> list[dict]:
""" """
Get UTE devices list. Get UTE devices list.
Returns: Returns:
List[dict]: List of devices list[dict]: List of devices
""" """
if not self.authorization:
self._login()
accounts_url = f"{API_VERSION_1}/accounts" accounts_url = f"{API_VERSION_1}/accounts"
return self._make_request("GET", accounts_url).json()["data"] return self._make_request("GET", accounts_url).json()["data"]
@ -233,6 +192,9 @@ class UTEClient:
Returns: Returns:
dict: UTE account information dict: UTE account information
""" """
if not self.authorization:
self._login()
accounts_by_id_url = f"{API_VERSION_1}/accounts/{self.device_id}" accounts_by_id_url = f"{API_VERSION_1}/accounts/{self.device_id}"
return self._make_request("GET", accounts_by_id_url).json()["data"] return self._make_request("GET", accounts_by_id_url).json()["data"]
@ -243,16 +205,22 @@ class UTEClient:
Returns: Returns:
dict: UTE peak info dict: UTE peak info
""" """
if not self.authorization:
self._login()
peak_by_id_url = f"{API_VERSION_1}/accounts/{self.device_id}/peak" peak_by_id_url = f"{API_VERSION_1}/accounts/{self.device_id}/peak"
return self._make_request("GET", peak_by_id_url).json()["data"] return self._make_request("GET", peak_by_id_url).json()["data"]
def get_network_status(self) -> List[dict]: def get_network_status(self) -> list[dict]:
""" """
Get UTE network status from device id. Get UTE network status from device id.
Returns: Returns:
dict: UTE network status dict: UTE network status
""" """
if not self.authorization:
self._login()
network_status_url = f"{API_VERSION_1}/info/network/status" network_status_url = f"{API_VERSION_1}/info/network/status"
return self._make_request("GET", network_status_url).json()["data"]["summary"] return self._make_request("GET", network_status_url).json()["data"]["summary"]
@ -263,6 +231,9 @@ class UTEClient:
Returns: Returns:
str: UTE renewable sources percentage str: UTE renewable sources percentage
""" """
if not self.authorization:
self._login()
global_demand_url = f"{API_VERSION_1}/info/demand/global" global_demand_url = f"{API_VERSION_1}/info/demand/global"
return self._make_request("GET", global_demand_url).json()["data"]["renewableSources"] return self._make_request("GET", global_demand_url).json()["data"]["renewableSources"]
@ -281,6 +252,9 @@ class UTEClient:
Returns: Returns:
dict: UTE info dict: UTE info
""" """
if not self.authorization:
self._login()
if date_start is None or date_end is None: if date_start is None or date_end is None:
yesterday = datetime.now() - timedelta(days=1) yesterday = datetime.now() - timedelta(days=1)
yesterday_formatted = yesterday.strftime("%Y-%m-%d") yesterday_formatted = yesterday.strftime("%Y-%m-%d")
@ -324,12 +298,12 @@ class UTEClient:
return active_energy return active_energy
def _convert_powers_to_power_in_watts(self, readings: List[dict]) -> float: def _convert_powers_to_power_in_watts(self, readings: list[dict]) -> float:
""" """
Convert powers to power in watts and determine the system type (monophasic, biphasic, triphasic) automatically. Convert powers to power in watts and determine the system type (monophasic, biphasic, triphasic) automatically.
Args: Args:
readings (List[dict]): List of readings readings (list[dict]): List of readings
Returns: Returns:
float: Power in watts float: Power in watts
@ -367,6 +341,9 @@ class UTEClient:
ReadingRequestFailedException: If the reading request fails. ReadingRequestFailedException: If the reading request fails.
ReadingResponseInvalidException: If the reading response is invalid. ReadingResponseInvalidException: If the reading response is invalid.
""" """
if not self.authorization:
self._login()
reading_request_url = f"{API_VERSION_1}/device/readingRequest" reading_request_url = f"{API_VERSION_1}/device/readingRequest"
reading_url = f"{API_VERSION_1}/device/{self.device_id}/lastReading/30" reading_url = f"{API_VERSION_1}/device/{self.device_id}/lastReading/30"
@ -374,7 +351,7 @@ class UTEClient:
reading_request = self._make_request("POST", reading_request_url, data=data) reading_request = self._make_request("POST", reading_request_url, data=data)
if reading_request.status_code != HTTP_200_OK: if reading_request.status_code != requests.codes.ok:
raise ReadingRequestFailedException("Error getting reading request") raise ReadingRequestFailedException("Error getting reading request")
response = self._make_request("GET", reading_url).json() response = self._make_request("GET", reading_url).json()

0
tests/__init__.py Normal file
View File

338
tests/test_ute.py Normal file
View File

@ -0,0 +1,338 @@
"""Unit tests for UTEClient class."""
import time
import pytest
import requests
from pytest_mock import MockerFixture
from src.ute_wrapper.exceptions import (
InvalidPlanException,
InvalidPowerFactorException,
MultipleDevicesException,
ReadingRequestFailedException,
ReadingResponseInvalidException,
TariffException,
UnsupportedMethodException,
)
from src.ute_wrapper.ute import UTEClient
@pytest.fixture
def ute_client(mocker: MockerFixture):
"""Return a UTEClient instance with mocked methods."""
mocker.patch.object(UTEClient, "_login", return_value="mocked_token")
mocker.patch.object(
UTEClient,
"get_devices_list",
return_value=[{"name": "Device 1", "accountServicePointId": "device_id_1"}],
)
mocker.patch.object(UTEClient, "get_account", return_value={"meterInfo": {"tariffType": "TRIPLE"}})
return UTEClient("test@example.com", "1234567890")
def test_make_request_success(ute_client, mocker: MockerFixture):
"""Test the _make_request method with a successful request."""
mocked_response = mocker.Mock(status_code=200, json=lambda: {"success": True})
mocker.patch("src.ute_wrapper.ute.requests.get", return_value=mocked_response)
response = ute_client._make_request("GET", "http://example.com")
assert response.status_code == requests.codes.ok
assert response.json()["success"] is True
def test_make_request_no_authorization(ute_client, mocker: MockerFixture):
"""Test the _make_request method when there is no authorization token."""
ute_client.authorization = None
mocked_response = mocker.Mock(status_code=requests.codes.ok, json=lambda: {"success": True})
mocker.patch("requests.get", return_value=mocked_response)
response = ute_client._make_request("GET", "http://example.com")
assert response.status_code == requests.codes.ok
assert response.json()["success"] is True
def test_make_request_expired_authorization(ute_client, mocker: MockerFixture):
"""Test the _make_request method when the authorization code has expired."""
mocked_unauthorized_response = mocker.Mock(status_code=requests.codes.unauthorized)
mocked_success_response = mocker.Mock(status_code=requests.codes.ok, json=lambda: {"success": True})
mocker.patch("requests.get", side_effect=[mocked_unauthorized_response, mocked_success_response])
mocker.patch.object(ute_client, "_login")
response = ute_client._make_request("GET", "http://example.com")
assert response.status_code == requests.codes.ok
assert response.json()["success"] is True
ute_client._login.assert_called_once()
def test_make_request_all_retries_failed(ute_client, mocker: MockerFixture):
"""Test the _make_request method when all retries fail."""
mocked_response = mocker.Mock()
mocked_response.raise_for_status.side_effect = requests.RequestException
mocker.patch("requests.get", return_value=mocked_response)
mocker.patch("time.sleep")
total_call_count = 3
with pytest.raises(Exception, match="All retries failed."):
ute_client._make_request("GET", "http://example.com", retries=total_call_count)
assert mocked_response.raise_for_status.call_count == total_call_count
assert time.sleep.call_count == total_call_count - 1
def test_method_not_supported(ute_client):
"""Test the _method_not_supported method."""
with pytest.raises(UnsupportedMethodException):
ute_client._method_not_supported()
def test_login(ute_client, mocker: MockerFixture):
"""Test the _login method."""
mocked_response = mocker.Mock(text="mocked_token")
mocker.patch.object(ute_client, "_make_request", return_value=mocked_response)
token = ute_client._login()
assert token == "mocked_token"
def test_validate_power_factor(ute_client):
"""Test the _validate_power_factor method."""
with pytest.raises(InvalidPowerFactorException):
ute_client.power_factor = 1.5
ute_client._validate_power_factor()
def test_initialize_device_id(ute_client, mocker: MockerFixture):
"""Test the _initialize_device_id method."""
ute_client.device_id = ""
mocker.patch.object(ute_client, "_select_device_id", return_value="selected_device_id")
ute_client._initialize_device_id()
assert ute_client.device_id == "selected_device_id"
def test_initialize_average_cost_per_kwh(ute_client, mocker: MockerFixture):
"""Test the _initialize_average_cost_per_kwh method."""
ute_client.average_cost_per_kwh = 0.0
mocker.patch.object(ute_client, "_determine_average_cost", return_value=5.0)
ute_client._initialize_average_cost_per_kwh()
assert ute_client.average_cost_per_kwh == 5.0
def test_select_device_id_multiple_devices(ute_client, mocker: MockerFixture):
"""Test the _select_device_id method when there are multiple devices."""
mocker.patch.object(
ute_client,
"get_devices_list",
return_value=[
{"name": "Device 1", "accountServicePointId": "device_id_1"},
{"name": "Device 2", "accountServicePointId": "device_id_2"},
],
)
with pytest.raises(MultipleDevicesException):
ute_client._select_device_id()
def test_determine_average_cost_invalid_tariff(ute_client, mocker: MockerFixture):
"""Test the _determine_average_cost method with an invalid tariff."""
mocker.patch.object(ute_client, "get_account", return_value={"meterInfo": {"tariffType": "INVALID"}})
with pytest.raises(InvalidPlanException):
ute_client._determine_average_cost()
def test_determine_average_cost_invalid_tariff_key(ute_client, mocker: MockerFixture):
"""Test the _determine_average_cost method with an invalid tariff key."""
mocker.patch.object(ute_client, "get_account", side_effect=KeyError)
with pytest.raises(TariffException, match="Tariff type not standard. Explicit definition required."):
ute_client._determine_average_cost()
@pytest.mark.parametrize(
"date_start, date_end",
[
("2023-05-01", "2023-05-02"),
(None, None),
],
)
def test_get_historic_consumption(ute_client, mocker: MockerFixture, date_start, date_end):
"""Test the get_historic_consumption method."""
mocked_response = {
"data": [
{"magnitudeVO": "IMPORT_ACTIVE_ENERGY", "date": "2023-05-01T00:00:00+00:00", "value": "10.0"},
{"magnitudeVO": "IMPORT_ACTIVE_ENERGY", "date": "2023-05-02T00:00:00+00:00", "value": "20.0"},
],
}
mocker.patch.object(ute_client, "_login")
mocker.patch.object(ute_client, "_make_request", return_value=mocker.Mock(json=lambda: mocked_response))
result = ute_client.get_historic_consumption(date_start, date_end)
assert result["total"]["sum_in_kwh"] == 30.0
assert result["total"]["aproximated_cost_in_uyu"] == 136.579
assert len(result["dates"]) == 2
def test_get_current_usage_info_success(ute_client, mocker: MockerFixture):
"""Test the get_current_usage_info method with a successful request."""
readings = [
{"tipoLecturaMGMI": "I1", "valor": "10"},
{"tipoLecturaMGMI": "V1", "valor": "220"},
]
mocked_reading_request = mocker.Mock(status_code=requests.codes.ok)
mocked_reading_response = mocker.Mock(json=lambda: {"success": True, "data": {"readings": readings}})
mocker.patch.object(ute_client, "_login")
mocker.patch.object(ute_client, "_make_request", side_effect=[mocked_reading_request, mocked_reading_response])
result = ute_client.get_current_usage_info()
assert result["success"] is True
assert result["data"]["readings"] == readings
assert result["data"]["power_in_watts"] == 2200.0
assert result["data"]["using_power_factor"] is True
def test_get_current_usage_info_failed_request(ute_client, mocker: MockerFixture):
"""Test the get_current_usage_info method with a failed request."""
mocker.patch.object(ute_client, "_login")
mocker.patch.object(
ute_client,
"_make_request",
side_effect=[mocker.Mock(status_code=400), mocker.Mock(json=lambda: {"success": False})],
)
with pytest.raises(ReadingRequestFailedException):
ute_client.get_current_usage_info()
def test_get_current_usage_info_invalid_response(ute_client, mocker: MockerFixture):
"""Test the get_current_usage_info method with an invalid response."""
mocker.patch.object(ute_client, "_login")
mocker.patch.object(
ute_client,
"_make_request",
side_effect=[
mocker.Mock(status_code=requests.codes.ok),
mocker.Mock(json=lambda: {"success": True, "data": {}}),
],
)
with pytest.raises(ReadingResponseInvalidException):
ute_client.get_current_usage_info()
def test_get_average_price_invalid_plan(ute_client):
"""Test the get_average_price method with an invalid plan."""
with pytest.raises(InvalidPlanException):
ute_client.get_average_price("invalid_plan")
@pytest.mark.parametrize(
"plan, expected_price",
[
("triple", 4.5526324),
("doble", 5.523887),
],
)
def test_get_average_price(ute_client, plan, expected_price):
"""Test the get_average_price method."""
result = ute_client.get_average_price(plan)
assert result == expected_price
def test_get_peak(ute_client, mocker: MockerFixture):
"""Test the get_peak method."""
expected_peak_data = {
"deviceId": "test_device_id",
"peak": {
"value": 123.45,
"date": "2023-06-01T10:00:00Z",
},
}
mocked_response = mocker.Mock(json=lambda: {"data": expected_peak_data})
mocker.patch.object(ute_client, "_login")
mocker.patch.object(ute_client, "_make_request", return_value=mocked_response)
result = ute_client.get_peak()
assert result == expected_peak_data
ute_client._make_request.assert_called_once_with(
"GET",
f"https://rocme.ute.com.uy/api/v1/accounts/{ute_client.device_id}/peak",
)
def test_get_network_status(ute_client, mocker: MockerFixture):
"""Test the get_network_status method."""
expected_network_status = [
{
"departmentId": 1,
"clientsWithoutService": 100,
"percentageClientsWithoutService": 0.5,
},
{
"departmentId": 2,
"clientsWithoutService": requests.codes.ok,
"percentageClientsWithoutService": 1.0,
},
]
mocked_response = mocker.Mock(json=lambda: {"data": {"summary": expected_network_status}})
mocker.patch.object(ute_client, "_login")
mocker.patch.object(ute_client, "_make_request", return_value=mocked_response)
result = ute_client.get_network_status()
assert result == expected_network_status
ute_client._make_request.assert_called_once_with(
"GET",
"https://rocme.ute.com.uy/api/v1/info/network/status",
)
def test_get_renewable_sources(ute_client, mocker: MockerFixture):
"""Test the get_renewable_sources method."""
expected_renewable_sources = "50.0%"
mocked_response = mocker.Mock(json=lambda: {"data": {"renewableSources": expected_renewable_sources}})
mocker.patch.object(ute_client, "_login")
mocker.patch.object(ute_client, "_make_request", return_value=mocked_response)
result = ute_client.get_renewable_sources()
assert result == expected_renewable_sources
ute_client._make_request.assert_called_once_with(
"GET",
"https://rocme.ute.com.uy/api/v1/info/demand/global",
)
@pytest.mark.parametrize(
"readings, expected_power",
[
(
[
{"tipoLecturaMGMI": "I1", "valor": "10"},
{"tipoLecturaMGMI": "V1", "valor": "220"},
],
2200.0,
),
(
[
{"tipoLecturaMGMI": "I1", "valor": "10"},
{"tipoLecturaMGMI": "I2", "valor": "10"},
{"tipoLecturaMGMI": "V1", "valor": "220"},
{"tipoLecturaMGMI": "V2", "valor": "220"},
],
2200.0,
),
(
[
{"tipoLecturaMGMI": "I1", "valor": "10"},
{"tipoLecturaMGMI": "I2", "valor": "10"},
{"tipoLecturaMGMI": "I3", "valor": "10"},
{"tipoLecturaMGMI": "V1", "valor": "220"},
{"tipoLecturaMGMI": "V2", "valor": "220"},
{"tipoLecturaMGMI": "V3", "valor": "220"},
],
3810.4,
),
(
[],
0.0,
),
],
)
def test_convert_powers_to_power_in_watts(ute_client, readings, expected_power):
"""Test the _convert_powers_to_power_in_watts method."""
result = ute_client._convert_powers_to_power_in_watts(readings)
assert result == expected_power