From b8e1cb50d75b225fd07d9f4869921a152337c520 Mon Sep 17 00:00:00 2001 From: Roger Gonzalez Date: Wed, 16 Aug 2023 22:48:11 -0300 Subject: [PATCH] Initial commit --- .env.example | 2 + .flake8 | 2 + .gitignore | 181 ++++++++++++++++++++++++++++++++++++++++ .pre-commit-config.yaml | 25 ++++++ pyproject.toml | 40 +++++++++ script.py | 151 +++++++++++++++++++++++++++++++++ 6 files changed, 401 insertions(+) create mode 100644 .env.example create mode 100755 .flake8 create mode 100644 .gitignore create mode 100755 .pre-commit-config.yaml create mode 100755 pyproject.toml create mode 100755 script.py diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..37d6873 --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +DEVICE_ID=12345 +AUTHORIZATION=authorization_code diff --git a/.flake8 b/.flake8 new file mode 100755 index 0000000..eb31e88 --- /dev/null +++ b/.flake8 @@ -0,0 +1,2 @@ +[flake8] +max-line-length = 121 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0f2e980 --- /dev/null +++ b/.gitignore @@ -0,0 +1,181 @@ +.DS_Store +.idea +*.log +tmp/ + +# Created by https://www.toptal.com/developers/gitignore/api/python +# Edit at https://www.toptal.com/developers/gitignore?templates=python + +### Python ### +# 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/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +# End of https://www.toptal.com/developers/gitignore/api/python diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100755 index 0000000..6304ab9 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,25 @@ +exclude: ^(.bzr|\.direnv|\.eggs|\.git|\.hg|\.mypy_cache|\.nox|\.pants\.d|\.svn|\.tox|\.venv|_build|buck-out|build|dist|node_modules|venv|\.idea|dockerdata|static|.*\b(migrations)\b.*) +repos: +- repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort +- repo: https://github.com/ambv/black + rev: 22.3.0 + hooks: + - id: black + language_version: python3 +- repo: https://github.com/pycqa/flake8 + rev: 3.9.2 + hooks: + - id: flake8 + additional_dependencies: + - flake8-bugbear==20.1.4 + - flake8-builtins==1.5.3 + - flake8-comprehensions==3.2.3 + - flake8-tidy-imports==4.1.0 + - flake8-eradicate==1.1.0 + - flake8-print==4.0.0 + - flake8-return==1.1.2 + - flake8-use-fstring==1.1 + - git+https://github.com/derrix060/flake8-expression-complexity.git diff --git a/pyproject.toml b/pyproject.toml new file mode 100755 index 0000000..37ff8e2 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,40 @@ +[tool.black] +line-length = 121 +target-version = ['py36', 'py37', 'py38'] +include = '\.pyi?$' +# regex below includes default list from isort, for parity +exclude = ''' +/( + \.bzr + | \.direnv + | \.eggs + | \.git + | \.hg + | \.mypy_cache + | \.nox + | \.pants\.d + | \.svn + | \.tox + | \.venv + | _build + | buck-out + | build + | dist + | node_modules + | venv + | \.idea + | dockerdata + | static +)/ +''' + +[tool.isort] +# these are black-compatible settings +multi_line_output = 3 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true +line_length = 121 +skip = "dockerdata,.idea,static" +filter_files = true diff --git a/script.py b/script.py new file mode 100755 index 0000000..96464b6 --- /dev/null +++ b/script.py @@ -0,0 +1,151 @@ +import os +from datetime import datetime, timedelta +from time import sleep +from typing import Optional + +import requests +from dotenv import load_dotenv + +load_dotenv() + +DEVICE_ID = os.environ.get("DEVICE_ID") +AUTHORIZATION = os.environ.get("AUTHORIZATION") + + +def make_request(method: str, url: str, data: Optional[dict] = None) -> requests.Response: + """ + Make request + + :param method: Method + :param url: URL + :param data: Data + :return: Response + """ + + headers = { + "Authorization": f"Bearer {AUTHORIZATION}", + "X-Client-Type": "Android", + "User-Agent": "okhttp/3.8.1", + "Content-Type": "application/json; charset=utf-8", + "Connection": "Keep-Alive", + "User-Agent": "okhttp/3.8.1", + } + + if method == "GET": + return requests.get(url, headers=headers) + + if method == "POST": + return requests.post(url, headers=headers, json=data) + + return None + + +def generate_ute_info( + device_id: str, cost_per_kwh: float, date_start: Optional[str] = None, date_end: Optional[str] = None +) -> dict: + """ + Generate UTE info from device id and date range + + :param device_id: Device id + :param authorization: Authorization token + :param cost_per_kwh: Cost per kwh + :param date_start: Date start + :param date_end: Date end + :return: UTE info + """ + + if date_start is None: + yesterday = datetime.now() - timedelta(days=1) + date_start = yesterday.strftime("%Y-%m-%d") + + if date_end is None: + yesterday = datetime.now() - timedelta(days=1) + date_end = yesterday.strftime("%Y-%m-%d") + + url = f"https://rocme.ute.com.uy/api/v2/device/{device_id}/curvefromtodate/D/{date_start}/{date_end}" + + response = make_request("GET", url).json() + + active_energy = {"total": {"sum_in_kwh": 0}} + + for item in response["data"]: + if item["magnitudeVO"] == "IMPORT_ACTIVE_ENERGY": + date = datetime.strptime(item["date"], "%Y-%m-%dT%H:%M:%S%z") + day_in_week = date.strftime("%A") + value = round(float(item["value"]), 3) + + active_energy[date.strftime("%d/%m/%Y")] = { + "kwh": value, + "aproximated_cost_in_uyu": round(value * cost_per_kwh, 3), + "day_in_week": day_in_week, + } + active_energy["total"]["sum_in_kwh"] = active_energy["total"]["sum_in_kwh"] + value + + active_energy["total"]["aproximated_cost_in_uyu"] = round(active_energy["total"]["sum_in_kwh"] * average_price, 3) + active_energy["total"]["daily_average_cost"] = round( + active_energy["total"]["aproximated_cost_in_uyu"] / (len(active_energy) - 1), 3 + ) + return active_energy + + +def get_current_usage(device_id: str) -> dict: + """ + Get current usage + + :param device_id: Device id + :param authorization: Authorization token + :raises Exception: Error getting reading request + :return: Current usage + """ + reading_request_url = "https://rocme.ute.com.uy/api/v1/device/readingRequest" + reading_url = f"https://rocme.ute.com.uy/api/v1/device/{device_id}/lastReading/30" + + data = {"AccountServicePointId": device_id} + + reading_request = make_request("POST", reading_request_url, data) + + if reading_request.status_code != 200: + raise Exception("Error getting reading request") + + response = make_request("GET", reading_url).json() + + while not response["success"]: + sleep(5) + response = make_request("GET", reading_url).json() + + readings = response["data"]["readings"] + + for reading in readings: + reading_type = reading["tipoLecturaMGMI"] + if reading_type == "I1": + i1 = float(reading["valor"]) + elif reading_type == "I2": + i2 = float(reading["valor"]) + elif reading_type == "I3": + i3 = float(reading["valor"]) + elif reading_type == "V1": + v1 = float(reading["valor"]) + elif reading_type == "V2": + v2 = float(reading["valor"]) + elif reading_type == "V3": + v3 = float(reading["valor"]) + + power_1_in_watts = v1 * i1 + power_2_in_watts = v2 * i2 + power_3_in_watts = v3 * i3 + + power_in_watts = round(power_1_in_watts + power_2_in_watts + power_3_in_watts, 3) + + return_dict = {**response} + return_dict["data"]["power_in_watts"] = power_in_watts + + return return_dict + + +if __name__ == "__main__": + date_start = "2023-08-01" + date_end = "2023-08-31" + average_price = (10.680 * 0.1667) + (2.223 * 0.2917) + (4.875 * 0.5416) # Each price * percentage of time on the day + + ute_historic_usage = generate_ute_info(DEVICE_ID, average_price, date_start, date_end) + ute_current_usage = get_current_usage(DEVICE_ID)