diff options
author | Roger Gonzalez <roger@rogs.me> | 2024-01-16 20:45:01 -0300 |
---|---|---|
committer | Roger Gonzalez <roger@rogs.me> | 2024-01-16 20:45:01 -0300 |
commit | 018f5284b2245368bdf5566bc6ebb6e9ba6ae44b (patch) | |
tree | 2b291a427e1587c28e1a6e65b03cf3a2d5b6ee54 | |
parent | dade47ce1653667ed6e3e4962429099fc1a7638e (diff) |
Refactored the code to make it more readable and maintanable
-rwxr-xr-x | .flake8 | 1 | ||||
-rw-r--r-- | README.md | 2 | ||||
-rwxr-xr-x | pyproject.toml | 2 | ||||
-rw-r--r-- | src/ute_wrapper/exceptions.py | 43 | ||||
-rwxr-xr-x | src/ute_wrapper/ute.py | 299 |
5 files changed, 212 insertions, 135 deletions
@@ -1,2 +1,3 @@ [flake8] max-line-length = 121 +ignore = E402 @@ -29,7 +29,7 @@ email = "your_email@example.com" phone_number = "your_phone_number" device_id = "your_device_id" # Optional average_cost_per_kwh = 4.0 # Optional, your average cost per kWh in UYU -power_factor = 0.7 # Optional, your power factor +power_factor = 0.9 # Optional, your power factor. It's almost always close to 1 ute_client = UTEClient(email, phone_number, device_id, average_cost_per_kwh, power_factor) ``` diff --git a/pyproject.toml b/pyproject.toml index 6c6c307..29ca2f9 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,7 +45,7 @@ build-backend = "hatchling.build" [project] name = "ute_wrapper" -version = "1.2.0" +version = "2.0.0" authors = [ { name="Roger Gonzalez", email="roger@rogs.me" }, ] diff --git a/src/ute_wrapper/exceptions.py b/src/ute_wrapper/exceptions.py new file mode 100644 index 0000000..fd57c2b --- /dev/null +++ b/src/ute_wrapper/exceptions.py @@ -0,0 +1,43 @@ +"""This module contains all the exceptions used in the project.""" + + +class InvalidPowerFactorException(Exception): + """Raised when the power factor is not between 0 and 1.""" + + pass + + +class MultipleDevicesException(Exception): + """Raised when there are multiple devices associated with the account.""" + + pass + + +class UnsupportedMethodException(Exception): + """Raised when an unsupported method is used.""" + + pass + + +class InvalidPlanException(Exception): + """Raised when the plan is not valid.""" + + pass + + +class TariffException(Exception): + """Raised when the tariff is not valid.""" + + pass + + +class ReadingRequestFailedException(Exception): + """Raised when the reading request fails.""" + + pass + + +class ReadingResponseInvalidException(Exception): + """Raised when the reading response is invalid.""" + + pass diff --git a/src/ute_wrapper/ute.py b/src/ute_wrapper/ute.py index 9d79518..4836e33 100755 --- a/src/ute_wrapper/ute.py +++ b/src/ute_wrapper/ute.py @@ -1,19 +1,20 @@ +"""Main UTE API Wrapper module.""" """ - UTE (Administración Nacional de Usinas y Trasmisiones Eléctricas) API Wrapper - Copyright (C) 2023 Roger Gonzalez +UTE (Administración Nacional de Usinas y Trasmisiones Eléctricas) API Wrapper. +Copyright (C) 2023 Roger Gonzalez - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. - You should have received a copy of the GNU General Public License - along with this program. If not, see <https://www.gnu.org/licenses/>. +You should have received a copy of the GNU General Public License +along with this program. If not, see <https://www.gnu.org/licenses/>. """ from datetime import datetime, timedelta @@ -22,10 +23,40 @@ from typing import List, Optional import requests -BASE_URL = "https://rocme.ute.com.uy/api/v1" +from .exceptions import ( + InvalidPlanException, + InvalidPowerFactorException, + MultipleDevicesException, + ReadingRequestFailedException, + ReadingResponseInvalidException, + TariffException, + UnsupportedMethodException, +) + +API_VERSION_1 = "https://rocme.ute.com.uy/api/v1" +API_VERSION_2 = "https://rocme.ute.com.uy/api/v2" class UTEClient: + """ + 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__( self, email: str, @@ -34,43 +65,63 @@ class UTEClient: average_cost_per_kwh: float = None, power_factor: float = None, ): + """ + Initialize the UTE client. + + 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 + """ self.email = email self.phone_number = phone_number self.device_id = device_id self.average_cost_per_kwh = average_cost_per_kwh self.authorization = self._login() self.power_factor = power_factor + self._validate_power_factor() + self._initialize_device_id() + self._initialize_average_cost_per_kwh() - if self.power_factor and (self.power_factor < 0 or self.power_factor > 1): - raise Exception("Your power factor has to be between 0 and 1") + def _validate_power_factor(self): + if self.power_factor and not 0 <= self.power_factor <= 1: + raise InvalidPowerFactorException("Power factor must be between 0 and 1") + def _initialize_device_id(self): if not self.device_id: - devices = self.get_devices_list() + self.device_id = self._select_device_id() - if len(devices) > 1: - devices_dict = {} - for device in devices: - devices_dict[device["name"]] = device["accountServicePointId"] - - raise Exception( - f""" - You have multiple device IDs. You need to choose one from the list - Valid options are: {devices_dict} - """ - ) + def _initialize_average_cost_per_kwh(self): + if not self.average_cost_per_kwh: + self.average_cost_per_kwh = self._determine_average_cost() - self.device_id = devices[0]["accountServicePointId"] + def _select_device_id(self) -> str: + devices = self.get_devices_list() + if len(devices) > 1: + devices_dict = {device["name"]: device["accountServicePointId"] for device in devices} + raise MultipleDevicesException(f"Multiple device IDs found. Valid options: {devices_dict}") + return devices[0]["accountServicePointId"] - if not self.average_cost_per_kwh: - try: - tariff_type = self.get_account()["meterInfo"]["tariffType"].lower() - self.average_cost_per_kwh = self.get_average_price(tariff_type) - except Exception: - raise Exception("Your tariff type is not standard. Try making it explicit on the client initialization") + def _determine_average_cost(self) -> float: + try: + tariff_type = self.get_account()["meterInfo"]["tariffType"].lower() + return self.get_average_price(tariff_type) + except KeyError: + raise TariffException("Tariff type not standard. Explicit definition required.") def _make_request(self, method: str, url: str, data: Optional[dict] = None) -> requests.Response: """ - Make a HTTP request + Make a HTTP request. Args: method (str): The HTTP method to use. Accepted methods are ``GET``, ``POST``. @@ -84,13 +135,11 @@ class UTEClient: Raises: Exception: If the method is not supported. """ - headers = { "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", } try: @@ -99,17 +148,14 @@ class UTEClient: except AttributeError: pass - if method == "GET": - return requests.get(url, headers=headers) - - if method == "POST": - return requests.post(url, headers=headers, json=data) + return getattr(requests, method.lower(), self._method_not_supported)(url, headers=headers, json=data) - raise Exception("Method not supported") + def _method_not_supported(self, *args, **kwargs): + raise UnsupportedMethodException("HTTP method not supported") def _login(self) -> str: """ - Login to UTE + Login to UTE. Args: email (str): User email for authentication @@ -118,8 +164,7 @@ class UTEClient: Returns: str: Authorization token """ - - url = f"{BASE_URL}/token" + url = f"{API_VERSION_1}/token" data = { "Email": self.email, "PhoneNumber": self.phone_number, @@ -129,57 +174,52 @@ class UTEClient: def get_devices_list(self) -> List[dict]: """ - Get UTE devices list + Get UTE devices list. Returns: List[dict]: List of devices """ - - accounts_url = f"{BASE_URL}/accounts" + accounts_url = f"{API_VERSION_1}/accounts" return self._make_request("GET", accounts_url).json()["data"] def get_account(self) -> dict: """ - Get UTE account info from device id + Get UTE account info from device id. Returns: dict: UTE account information """ - - accounts_by_id_url = f"{BASE_URL}/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"] def get_peak(self) -> dict: """ - Get UTE peak info from device id + Get UTE peak info from device id. Returns: dict: UTE peak info """ - - peak_by_id_url = f"{BASE_URL}/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"] def get_network_status(self) -> List[dict]: """ - Get UTE network status from device id + Get UTE network status from device id. Returns: dict: UTE network status """ - - network_status_url = f"{BASE_URL}/info/network/status" + network_status_url = f"{API_VERSION_1}/info/network/status" return self._make_request("GET", network_status_url).json()["data"]["summary"] def get_renewable_sources(self) -> str: """ - Get UTE renewable sources + Get UTE renewable sources. Returns: str: UTE renewable sources percentage """ - - global_demand_url = f"{BASE_URL}/info/demand/global" + global_demand_url = f"{API_VERSION_1}/info/demand/global" return self._make_request("GET", global_demand_url).json()["data"]["renewableSources"] def get_historic_consumption( @@ -188,7 +228,7 @@ class UTEClient: date_end: Optional[str] = None, ) -> dict: """ - Generate UTE historic consumption from device id and date range + Generate UTE historic consumption from device id and date range. Args: date_start (str): Start date to check in format YYYY-MM-DD @@ -197,48 +237,45 @@ class UTEClient: Returns: dict: 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: + if date_start is None or date_end is None: yesterday = datetime.now() - timedelta(days=1) - date_end = yesterday.strftime("%Y-%m-%d") - - historic_url = ( - f"https://rocme.ute.com.uy/api/v2/device/{self.device_id}/curvefromtodate/D/{date_start}/{date_end}" - ) + yesterday_formatted = yesterday.strftime("%Y-%m-%d") + date_start = date_start or yesterday_formatted + date_end = date_end or yesterday_formatted + historic_url = f"{API_VERSION_2}/device/{self.device_id}/curvefromtodate/D/{date_start}/{date_end}" response = self._make_request("GET", historic_url).json() - active_energy = {"total": {"sum_in_kwh": 0}} + active_energy = {"total": {"sum_in_kwh": 0, "aproximated_cost_in_uyu": 0}} + num_days = 0 for item in response["data"]: if item["magnitudeVO"] == "IMPORT_ACTIVE_ENERGY": date = datetime.strptime(item["date"], "%Y-%m-%dT%H:%M:%S%z") + formatted_date = date.strftime("%d/%m/%Y") 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 * self.average_cost_per_kwh, 3), - "day_in_week": day_in_week, - } + if formatted_date not in active_energy: + active_energy[formatted_date] = {"kwh": 0, "aproximated_cost_in_uyu": 0} + num_days += 1 + + value = round(float(item["value"]), 3) + active_energy[formatted_date]["kwh"] += value + active_energy[formatted_date]["aproximated_cost_in_uyu"] += round(value * self.average_cost_per_kwh, 3) + active_energy[formatted_date]["day_in_week"] = day_in_week active_energy["total"]["sum_in_kwh"] += value - active_energy["total"]["aproximated_cost_in_uyu"] = round( - active_energy["total"]["sum_in_kwh"] * self.average_cost_per_kwh, 3 - ) - active_energy["total"]["daily_average_cost"] = round( - active_energy["total"]["aproximated_cost_in_uyu"] / (len(active_energy) - 1), 3 + total_kwh = active_energy["total"]["sum_in_kwh"] + active_energy["total"]["aproximated_cost_in_uyu"] = round(total_kwh * self.average_cost_per_kwh, 3) + active_energy["total"]["daily_average_cost"] = ( + round(active_energy["total"]["aproximated_cost_in_uyu"] / num_days, 3) if num_days > 0 else 0 ) + return active_energy 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, or triphasic) - automatically. + Convert powers to power in watts and determine the system type (monophasic, biphasic, triphasic) automatically. Args: readings (List[dict]): List of readings @@ -246,57 +283,48 @@ class UTEClient: Returns: float: Power in watts """ + SQUARE_ROOT_OF_THREE = 1.732 reading_sums = {"I1": 0, "I2": 0, "I3": 0, "V1": 0, "V2": 0, "V3": 0} num_voltages = num_currents = 0 - total_power_in_watts = 0 - square_root_of_three = 1.732 for reading in readings: - reading_type = reading["tipoLecturaMGMI"] - if reading_type in reading_sums: + reading_type = reading.get("tipoLecturaMGMI") + if reading_type in reading_sums and "valor" in reading: reading_sums[reading_type] += float(reading["valor"]) - if "V" in reading_type: - num_voltages += 1 - elif "I" in reading_type: - num_currents += 1 + num_voltages += "V" in reading_type + num_currents += "I" in reading_type - if num_voltages > 0 and num_currents > 0: - averaged_voltage = sum(reading_sums[v] for v in ["V1", "V2", "V3"]) / num_voltages - averaged_current = sum(reading_sums[i] for i in ["I1", "I2", "I3"]) / num_currents + if num_voltages == 0 or num_currents == 0: + return 0.0 - if num_voltages == 3 and num_currents == 3: - total_power_in_watts = averaged_voltage * averaged_current * self.power_factor * square_root_of_three - elif num_voltages == 2 and num_currents == 2: - total_power_in_watts = averaged_voltage * averaged_current * self.power_factor * square_root_of_three - else: - total_power_in_watts = averaged_voltage * averaged_current * self.power_factor + averaged_voltage = sum(reading_sums[v] for v in ["V1", "V2", "V3"]) / num_voltages + averaged_current = sum(reading_sums[i] for i in ["I1", "I2", "I3"]) / num_currents - return round(total_power_in_watts, 3) + if num_voltages == 3 and num_currents == 3: + return round(averaged_voltage * averaged_current * self.power_factor * SQUARE_ROOT_OF_THREE, 3) + else: + return round(averaged_voltage * averaged_current * self.power_factor, 3) def get_current_usage_info(self) -> dict: """ - Get current usage info from device id - - Args: - device_id (str): UTE Device id - authorization (str): Authorization token + Get current usage info from device id. Returns: dict: UTE info Raises: - Exception: If the reading request fails + ReadingRequestFailedException: If the reading request fails. + ReadingResponseInvalidException: If the reading response is invalid. """ - - reading_request_url = f"{BASE_URL}/device/readingRequest" - reading_url = f"{BASE_URL}/device/{self.device_id}/lastReading/30" + reading_request_url = f"{API_VERSION_1}/device/readingRequest" + reading_url = f"{API_VERSION_1}/device/{self.device_id}/lastReading/30" data = {"AccountServicePointId": self.device_id} reading_request = self._make_request("POST", reading_request_url, data=data) if reading_request.status_code != 200: - raise Exception("Error getting reading request") + raise ReadingRequestFailedException("Error getting reading request") response = self._make_request("GET", reading_url).json() @@ -304,19 +332,24 @@ class UTEClient: sleep(5) response = self._make_request("GET", reading_url).json() - readings = response["data"]["readings"] + readings = response.get("data", {}).get("readings") + if readings is None: + raise ReadingResponseInvalidException("Response data is missing 'readings'") power_in_watts = self._convert_powers_to_power_in_watts(readings) - return_dict = {**response} - return_dict["data"]["power_in_watts"] = power_in_watts - return_dict["data"]["using_power_factor"] = True if self.power_factor else False - - return return_dict + return { + **response, + "data": { + **response.get("data", {}), + "power_in_watts": power_in_watts, + "using_power_factor": bool(self.power_factor), + }, + } def get_average_price(self, plan: str) -> float: """ - Get the average price for a plan + Get the average price for a plan. Args: plan (str): Plan name. Can be "triple" or "doble" @@ -327,15 +360,15 @@ class UTEClient: Raises: Exception: If the plan is invalid """ - + TRIPLE_HOUR_RATES = (11.032, 5.036, 2.298) + DOUBLE_HOUR_RATES = (11.032, 4.422) if plan == "triple": - # 10.680 UYU/kwh * 16.67% of the day (4 hours) - # 2.223 UYU/kwh * 29.17% of the day (7 hours) - # 4.875 UYU/kwh * 54.16% of the day (13 hours) - return (10.680 * 0.1667) + (2.223 * 0.2917) + (4.875 * 0.5416) - if plan == "doble": - # 10.680 UYU/kwh * 16.67% of the day (4 hours) - # 4.280 UYU/kwh * 83.33% of the day (20 hours) - return (10.680 * 0.1667) + (4.280 * 0.8333) - - raise Exception("Invalid plan") + # 11.032 UYU/kwh * 16.67% of the day (4 hours) + # 5.036 UYU/kwh * 54.16% of the day (13 hours) + # 2.298 UYU/kwh * 29.17% of the day (7 hours) + return sum(rate * portion for rate, portion in zip(TRIPLE_HOUR_RATES, (0.1667, 0.2917, 0.5416))) + elif plan == "doble": + # 11.032 UYU/kwh * 16.67% of the day (4 hours) + # 4.422 UYU/kwh * 83.33% of the day (20 hours) + return sum(rate * portion for rate, portion in zip(DOUBLE_HOUR_RATES, (0.1667, 0.8333))) + raise InvalidPlanException("Invalid plan name.") |