e-data
Advanced tools
| Metadata-Version: 2.4 | ||
| Name: e-data | ||
| Version: 2.0.0.dev112 | ||
| Version: 2.0.0.dev120 | ||
| Summary: Python library for managing spanish energy data from various web providers | ||
@@ -826,3 +826,3 @@ Author-email: VMG <vmayorg@outlook.es> | ||
| print(f"Coste total: {total_cost:.2f} €") | ||
| print(f"Consumo total: {sum(e.value_kWh for e in energy):.2f} kWh") | ||
| print(f"Consumo total: {sum(e.value_kwh for e in energy):.2f} kWh") | ||
@@ -845,3 +845,3 @@ # Ejecutar | ||
| # Actualizar facturación con tarifa fija | ||
| python -m edata.cli update-bill \ | ||
| python -m edata.cli update-custom-bill \ | ||
| --cups ES0000000000000000XX \ | ||
@@ -848,0 +848,0 @@ --p1-kw-year-eur 30.67 \ |
+8
-8
@@ -20,3 +20,3 @@ import asyncio | ||
| @app.command() | ||
| def show_supplies(username: str): | ||
| def show_supplies(username: str) -> None: | ||
| """Show supplies and contracts for a given datadis user.""" | ||
@@ -32,3 +32,3 @@ | ||
| cups = supplies[0].cups | ||
| distributor = supplies[0].distributorCode | ||
| distributor = supplies[0].distributor_code | ||
| contracts = connector.get_contract_detail(cups, distributor) | ||
@@ -46,3 +46,3 @@ typer.echo("\nContracts:") | ||
| authorized_nif: str | None = None, | ||
| ): | ||
| ) -> None: | ||
| """Download all data for a given datadis account and CUPS.""" | ||
@@ -69,3 +69,3 @@ | ||
| authorized_nif: str | None = None, | ||
| ): | ||
| ) -> None: | ||
| """Download all data for a given datadis account and CUPS.""" | ||
@@ -84,3 +84,3 @@ | ||
| meter_month_eur: float, | ||
| ): | ||
| ) -> None: | ||
| """Download all data for a given datadis account and CUPS.""" | ||
@@ -113,3 +113,3 @@ | ||
| meter_month_eur: Annotated[float, typer.Option(help="Monthly cost of the meter")], | ||
| ): | ||
| ) -> None: | ||
| """Download all data for a given datadis account and CUPS.""" | ||
@@ -130,3 +130,3 @@ | ||
| async def _update_pvpc_bill(cups: str): | ||
| async def _update_pvpc_bill(cups: str) -> None: | ||
| """Download all data for a given datadis account and CUPS.""" | ||
@@ -141,3 +141,3 @@ | ||
| cups: Annotated[str, typer.Option(help="The identifier of the Supply")], | ||
| ): | ||
| ) -> None: | ||
| """Download all data for a given datadis account and CUPS.""" | ||
@@ -144,0 +144,0 @@ |
@@ -46,3 +46,3 @@ """Collection of utilities.""" | ||
| def get_contract_for_dt(contracts: list[Contract], date: datetime): | ||
| def get_contract_for_dt(contracts: list[Contract], date: datetime) -> Contract | None: | ||
| """Return the active contract for a provided datetime.""" | ||
@@ -49,0 +49,0 @@ |
@@ -52,3 +52,3 @@ import logging | ||
| @property | ||
| def engine(self): | ||
| def engine(self) -> AsyncEngine | None: | ||
| """Return the async database engine.""" | ||
@@ -58,3 +58,3 @@ | ||
| async def _ensure_tables(self): | ||
| async def _ensure_tables(self) -> None: | ||
| """Create tables if not already created (lazy init).""" | ||
@@ -89,7 +89,7 @@ | ||
| query: SelectOfScalar, | ||
| data, | ||
| data: typing.Any, | ||
| commit: bool = True, | ||
| overrides: dict[str, typing.Any] | None = None, | ||
| ) -> T | None: # type: ignore | ||
| """Updates a single record in the database.""" | ||
| """Update a single record in the database.""" | ||
@@ -246,3 +246,3 @@ result = await session.exec(query) | ||
| async def add_energy_list(self, cups: str, energy: list[Energy]): | ||
| async def add_energy_list(self, cups: str, energy: list[Energy]) -> None: | ||
| """Add or update a list of energy records.""" | ||
@@ -271,3 +271,3 @@ | ||
| async def add_power_list(self, cups: str, power: list[Power]): | ||
| async def add_power_list(self, cups: str, power: list[Power]) -> None: | ||
| """Add or update a list of power records.""" | ||
@@ -293,3 +293,3 @@ | ||
| async def add_pvpc_list(self, pvpc: list[EnergyPrice]): | ||
| async def add_pvpc_list(self, pvpc: list[EnergyPrice]) -> None: | ||
| """Add or update a list of pvpc records.""" | ||
@@ -360,3 +360,3 @@ | ||
| bill: list[Bill], | ||
| ): | ||
| ) -> None: | ||
| """Add or update a list of bill records.""" | ||
@@ -384,3 +384,3 @@ | ||
| async def list_supplies(self): | ||
| async def list_supplies(self) -> typing.Sequence[SupplyModel]: | ||
| """List all supply records.""" | ||
@@ -393,3 +393,5 @@ | ||
| async def list_contracts(self, cups: str | None = None): | ||
| async def list_contracts( | ||
| self, cups: str | None = None | ||
| ) -> typing.Sequence[ContractModel]: | ||
| """List all contract records.""" | ||
@@ -407,3 +409,3 @@ | ||
| date_to: datetime | None = None, | ||
| ): | ||
| ) -> typing.Sequence[EnergyModel]: | ||
| """List energy records.""" | ||
@@ -421,3 +423,3 @@ | ||
| date_to: datetime | None = None, | ||
| ): | ||
| ) -> typing.Sequence[PowerModel]: | ||
| """List power records.""" | ||
@@ -434,3 +436,3 @@ | ||
| date_to: datetime | None = None, | ||
| ): | ||
| ) -> typing.Sequence[PVPCModel]: | ||
| """List pvpc records.""" | ||
@@ -450,3 +452,3 @@ | ||
| complete: bool | None = None, | ||
| ): | ||
| ) -> typing.Sequence[StatisticsModel]: | ||
| """List statistics records filtered by type ('day' or 'month') and date range.""" | ||
@@ -468,3 +470,3 @@ | ||
| complete: bool | None = None, | ||
| ): | ||
| ) -> typing.Sequence[BillModel]: | ||
| """List bill records filtered by type ('hour', 'day' or 'month') and date range.""" | ||
@@ -471,0 +473,0 @@ |
@@ -218,3 +218,5 @@ import typing | ||
| def get_bill(cups, type_, datetime_): | ||
| def get_bill( | ||
| cups: str, type_: typing.Literal["hour", "day", "month"], datetime_: datetime | None | ||
| ) -> SelectOfScalar[BillModel]: | ||
| """Query that selects a bill.""" | ||
@@ -221,0 +223,0 @@ |
@@ -10,6 +10,3 @@ from typing import Any, Type, TypeVar | ||
| class PydanticJSON(TypeDecorator): | ||
| """ | ||
| Tipo de SQLAlchemy para guardar modelos Pydantic como JSON. | ||
| Automáticamente serializa al guardar y deserializa al leer. | ||
| """ | ||
| """SQLAlchemy type to guard Pydantic models as JSON.""" | ||
@@ -24,2 +21,3 @@ impl = JSON | ||
| def process_bind_param(self, value: T | None, dialect: Any) -> Any: | ||
| """Process binding parameter.""" | ||
| # Python -> Base de Datos | ||
@@ -32,2 +30,3 @@ if value is None: | ||
| def process_result_value(self, value: Any, dialect: Any) -> T | None: | ||
| """Process result value.""" | ||
| # Base de Datos -> Python | ||
@@ -34,0 +33,0 @@ if value is None: |
@@ -9,6 +9,6 @@ """Models for billing-related data""" | ||
| class EnergyPrice(BaseModel): | ||
| """Data structure to represent pricing data.""" | ||
| """Represent pricing data.""" | ||
| datetime: datetime | ||
| value_eur_kWh: float | ||
| value_eur_kwh: float | ||
| delta_h: float | ||
@@ -18,3 +18,3 @@ | ||
| class Bill(BaseModel): | ||
| """Data structure to represent a bill during a period.""" | ||
| """Represent a bill during a period.""" | ||
@@ -31,3 +31,3 @@ datetime: datetime | ||
| class BillingRules(BaseModel): | ||
| """Data structure to represent a generic billing rule.""" | ||
| """Represent a generic billing rule.""" | ||
@@ -56,3 +56,3 @@ p1_kw_year_eur: float | ||
| class PVPCBillingRules(BillingRules): | ||
| """Data structure to represent a PVPC billing rule.""" | ||
| """Represent a PVPC billing rule.""" | ||
@@ -59,0 +59,0 @@ p1_kw_year_eur: float = Field(default=30.67266) |
+66
-14
@@ -9,8 +9,10 @@ """Models for telemetry data""" | ||
| class Energy(BaseModel): | ||
| """Data structure to represent energy consumption and/or surplus measurements.""" | ||
| """Represent energy consumption and/or surplus measurements.""" | ||
| datetime: datetime | ||
| delta_h: float | ||
| value_kWh: float | ||
| surplus_kWh: float = Field(0) | ||
| consumption_kwh: float | ||
| surplus_kwh: float = Field(0) | ||
| generation_kwh: float = Field(0) | ||
| selfconsumption_kwh: float = Field(0) | ||
| real: bool | ||
@@ -20,20 +22,70 @@ | ||
| class Power(BaseModel): | ||
| """Data structure to represent power measurements.""" | ||
| """Represent power measurements.""" | ||
| datetime: datetime | ||
| value_kW: float | ||
| value_kw: float | ||
| class Statistics(BaseModel): | ||
| """Data structure to represent aggregated energy/surplus data.""" | ||
| """Represent aggregated energy/surplus data.""" | ||
| datetime: datetime | ||
| delta_h: float = Field(0) | ||
| value_kWh: float = Field(0) | ||
| value_p1_kWh: float = Field(0) | ||
| value_p2_kWh: float = Field(0) | ||
| value_p3_kWh: float = Field(0) | ||
| surplus_kWh: float = Field(0) | ||
| surplus_p1_kWh: float = Field(0) | ||
| surplus_p2_kWh: float = Field(0) | ||
| surplus_p3_kWh: float = Field(0) | ||
| consumption_kwh: float = Field(0) | ||
| consumption_by_tariff: list[float] = Field(default_factory=lambda: [0.0, 0.0, 0.0]) | ||
| surplus_kwh: float = Field(0) | ||
| surplus_by_tariff: list[float] = Field(default_factory=lambda: [0.0, 0.0, 0.0]) | ||
| generation_kwh: float = Field(0) | ||
| generation_by_tariff: list[float] = Field(default_factory=lambda: [0.0, 0.0, 0.0]) | ||
| selfconsumption_kwh: float = Field(0) | ||
| selfconsumption_by_tariff: list[float] = Field( | ||
| default_factory=lambda: [0.0, 0.0, 0.0] | ||
| ) | ||
| @property | ||
| def consumption_p1_kwh(self) -> float: | ||
| return self.consumption_by_tariff[0] | ||
| @property | ||
| def consumption_p2_kwh(self) -> float: | ||
| return self.consumption_by_tariff[1] | ||
| @property | ||
| def consumption_p3_kwh(self) -> float: | ||
| return self.consumption_by_tariff[2] | ||
| @property | ||
| def surplus_p1_kwh(self) -> float: | ||
| return self.surplus_by_tariff[0] | ||
| @property | ||
| def surplus_p2_kwh(self) -> float: | ||
| return self.surplus_by_tariff[1] | ||
| @property | ||
| def surplus_p3_kwh(self) -> float: | ||
| return self.surplus_by_tariff[2] | ||
| @property | ||
| def generation_p1_kwh(self) -> float: | ||
| return self.generation_by_tariff[0] | ||
| @property | ||
| def generation_p2_kwh(self) -> float: | ||
| return self.generation_by_tariff[1] | ||
| @property | ||
| def generation_p3_kwh(self) -> float: | ||
| return self.generation_by_tariff[2] | ||
| @property | ||
| def selfconsumption_p1_kwh(self) -> float: | ||
| return self.selfconsumption_by_tariff[0] | ||
| @property | ||
| def selfconsumption_p2_kwh(self) -> float: | ||
| return self.selfconsumption_by_tariff[1] | ||
| @property | ||
| def selfconsumption_p3_kwh(self) -> float: | ||
| return self.selfconsumption_by_tariff[2] |
@@ -19,4 +19,4 @@ """Models for contractual data""" | ||
| distributor: str | None | ||
| pointType: int | ||
| distributorCode: str | ||
| point_type: int | ||
| distributor_code: str | ||
@@ -30,4 +30,13 @@ | ||
| marketer: str | ||
| distributorCode: str | ||
| power_p1: float | None | ||
| power_p2: float | None | ||
| distributor_code: str | ||
| power: list[float] | ||
| @property | ||
| def power_p1(self) -> float | None: | ||
| """Return power P1.""" | ||
| return self.power[0] if len(self.power) > 0 else None | ||
| @property | ||
| def power_p2(self) -> float | None: | ||
| """Return power P2.""" | ||
| return self.power[1] if len(self.power) > 1 else None |
@@ -1,9 +0,3 @@ | ||
| """Datadis API connector. | ||
| """Datadis API connector.""" | ||
| To fetch data from datadis.es private API. | ||
| There a few issues that are workarounded: | ||
| - You have to wait 24h between two identical requests. | ||
| - Datadis server does not like ranges greater than 1 month. | ||
| """ | ||
| import asyncio | ||
@@ -31,3 +25,3 @@ import contextlib | ||
| # Supplies-related constants | ||
| URL_GET_SUPPLIES = "https://datadis.es/api-private/api/get-supplies" | ||
| URL_GET_SUPPLIES = "https://datadis.es/api-private/api/get-supplies-v2" | ||
| GET_SUPPLIES_MANDATORY_FIELDS = [ | ||
@@ -42,3 +36,3 @@ "cups", | ||
| # Contracts-related constants | ||
| URL_GET_CONTRACT_DETAIL = "https://datadis.es/api-private/api/get-contract-detail" | ||
| URL_GET_CONTRACT_DETAIL = "https://datadis.es/api-private/api/get-contract-detail-v2" | ||
| GET_CONTRACT_DETAIL_MANDATORY_FIELDS = [ | ||
@@ -52,3 +46,3 @@ "startDate", | ||
| # Consumption-related constants | ||
| URL_GET_CONSUMPTION_DATA = "https://datadis.es/api-private/api/get-consumption-data" | ||
| URL_GET_CONSUMPTION_DATA = "https://datadis.es/api-private/api/get-consumption-data-v2" | ||
| GET_CONSUMPTION_DATA_MANDATORY_FIELDS = [ | ||
@@ -62,3 +56,3 @@ "time", | ||
| # Maximeter-related constants | ||
| URL_GET_MAX_POWER = "https://datadis.es/api-private/api/get-max-power" | ||
| URL_GET_MAX_POWER = "https://datadis.es/api-private/api/get-max-power-v2" | ||
| GET_MAX_POWER_MANDATORY_FIELDS = ["time", "date", "maxPower"] | ||
@@ -75,3 +69,3 @@ | ||
| def migrate_storage(storage_dir): | ||
| def migrate_storage(storage_dir: str) -> None: | ||
| """Migrate storage from older versions.""" | ||
@@ -110,5 +104,4 @@ with contextlib.suppress(FileNotFoundError): | ||
| def _get_hash(self, item: str): | ||
| def _get_hash(self, item: str) -> str: | ||
| """Return a hash.""" | ||
| return hashlib.md5(item.encode()).hexdigest() | ||
@@ -130,3 +123,3 @@ | ||
| def _get_cache(self, key: str): | ||
| def _get_cache(self, key: str) -> dict | None: | ||
| """Return cached response for a query (diskcache).""" | ||
@@ -139,3 +132,3 @@ hash_query = self._get_hash(key) | ||
| async def _async_get_token(self): | ||
| async def _async_get_token(self) -> bool: | ||
| """Private async method that fetches a new token if needed.""" | ||
@@ -169,7 +162,7 @@ _LOGGER.debug("No token found, fetching a new one") | ||
| async def async_login(self): | ||
| async def async_login(self) -> bool: | ||
| """Test to login with provided credentials (async).""" | ||
| return await self._async_get_token() | ||
| def login(self): | ||
| def login(self) -> bool: | ||
| """Test to login with provided credentials (sync wrapper).""" | ||
@@ -300,2 +293,3 @@ return asyncio.run(self.async_login()) | ||
| ) -> list[Supply]: | ||
| """Datadis 'get_supplies' query.""" | ||
| data = {} | ||
@@ -309,3 +303,3 @@ if authorized_nif is not None: | ||
| tomorrow_str = (datetime.today() + timedelta(days=1)).strftime("%Y/%m/%d") | ||
| for i in response: | ||
| for i in response.get("supplies", []): | ||
| if all(k in i for k in GET_SUPPLIES_MANDATORY_FIELDS): | ||
@@ -336,4 +330,4 @@ supplies.append( | ||
| distributor=i.get("distributor", None), | ||
| pointType=i["pointType"], | ||
| distributorCode=i["distributorCode"], | ||
| point_type=i["pointType"], | ||
| distributor_code=i["distributorCode"], | ||
| ) | ||
@@ -348,3 +342,3 @@ ) | ||
| def get_supplies(self, authorized_nif: str | None = None): | ||
| def get_supplies(self, authorized_nif: str | None = None) -> list[Supply]: | ||
| """Datadis 'get_supplies' query (sync wrapper).""" | ||
@@ -356,2 +350,3 @@ return asyncio.run(self.async_get_supplies(authorized_nif=authorized_nif)) | ||
| ) -> list[Contract]: | ||
| """Datadis 'get_contract_detail' query.""" | ||
| data = {"cups": cups, "distributorCode": distributor_code} | ||
@@ -365,3 +360,3 @@ if authorized_nif is not None: | ||
| tomorrow_str = (datetime.today() + timedelta(days=1)).strftime("%Y/%m/%d") | ||
| for i in response: | ||
| for i in response.get("contract", []): | ||
| if all(k in i for k in GET_CONTRACT_DETAIL_MANDATORY_FIELDS): | ||
@@ -379,13 +374,4 @@ contracts.append( | ||
| marketer=i["marketer"], | ||
| distributorCode=distributor_code, | ||
| power_p1=( | ||
| i["contractedPowerkW"][0] | ||
| if isinstance(i["contractedPowerkW"], list) | ||
| else None | ||
| ), | ||
| power_p2=( | ||
| i["contractedPowerkW"][1] | ||
| if (len(i["contractedPowerkW"]) > 1) | ||
| else None | ||
| ), | ||
| distributor_code=distributor_code, | ||
| power=i["contractedPowerkW"], | ||
| ) | ||
@@ -402,3 +388,3 @@ ) | ||
| self, cups: str, distributor_code: str, authorized_nif: str | None = None | ||
| ): | ||
| ) -> list[Contract]: | ||
| """Datadis get_contract_detail query (sync wrapper).""" | ||
@@ -419,3 +405,3 @@ return asyncio.run( | ||
| ) -> list[Energy]: | ||
| """Datadis 'get_consumption_data' query.""" | ||
| data = { | ||
@@ -435,3 +421,3 @@ "cups": cups, | ||
| consumptions = [] | ||
| for i in response: | ||
| for i in response.get("timeCurve", []): | ||
| if "consumptionKWh" in i: | ||
@@ -445,5 +431,14 @@ if all(k in i for k in GET_CONSUMPTION_DATA_MANDATORY_FIELDS): | ||
| continue # skip element if dt is out of range | ||
| _surplus = i.get("surplusEnergyKWh", 0) | ||
| if _surplus is None: | ||
| _surplus = 0 | ||
| # sanitize these values | ||
| _surplus_kwh = i.get("surplusEnergyKWh", 0) | ||
| if _surplus_kwh is None: | ||
| _surplus_kwh = 0 | ||
| _generation_kwh = i.get("generationEnergyKWh", 0) | ||
| if _generation_kwh is None: | ||
| _generation_kwh = 0 | ||
| _selfconsumption_kwh = i.get("selfConsumptionEnergyKWh", 0) | ||
| if _selfconsumption_kwh is None: | ||
| _selfconsumption_kwh = 0 | ||
| consumptions.append( | ||
@@ -453,4 +448,6 @@ Energy( | ||
| delta_h=1, | ||
| value_kWh=i["consumptionKWh"], | ||
| surplus_kWh=_surplus, | ||
| consumption_kwh=i["consumptionKWh"], | ||
| surplus_kwh=_surplus_kwh, | ||
| generation_kwh=_generation_kwh, | ||
| selfconsumption_kwh=_selfconsumption_kwh, | ||
| real=i["obtainMethod"] == "Real", | ||
@@ -475,3 +472,3 @@ ) | ||
| authorized_nif: str | None = None, | ||
| ): | ||
| ) -> list[Energy]: | ||
| """Datadis get_consumption_data query (sync wrapper).""" | ||
@@ -498,2 +495,3 @@ return asyncio.run( | ||
| ) -> list[Power]: | ||
| """Datadis 'get_max_power' query.""" | ||
| data = { | ||
@@ -509,3 +507,3 @@ "cups": cups, | ||
| maxpower_values = [] | ||
| for i in response: | ||
| for i in response.get("maxPower", []): | ||
| if all(k in i for k in GET_MAX_POWER_MANDATORY_FIELDS): | ||
@@ -517,3 +515,3 @@ maxpower_values.append( | ||
| ), | ||
| value_kW=i["maxPower"], | ||
| value_kw=i["maxPower"], | ||
| ) | ||
@@ -535,3 +533,3 @@ ) | ||
| authorized_nif: str | None = None, | ||
| ): | ||
| ) -> list[Power]: | ||
| """Datadis get_max_power query (sync wrapper).""" | ||
@@ -538,0 +536,0 @@ return asyncio.run( |
@@ -65,3 +65,3 @@ """A REData API connector""" | ||
| ), | ||
| value_eur_kWh=element["value"] / 1000, | ||
| value_eur_kwh=element["value"] / 1000, | ||
| delta_h=1, | ||
@@ -83,3 +83,3 @@ ) | ||
| self, dt_from: dt.datetime, dt_to: dt.datetime, is_ceuta_melilla: bool = False | ||
| ) -> list: | ||
| ) -> list[EnergyPrice]: | ||
| """GET query to fetch realtime pvpc prices, historical data is limited to current month (sync wrapper)""" | ||
@@ -86,0 +86,0 @@ return asyncio.run( |
@@ -118,3 +118,3 @@ import asyncio | ||
| bills = await asyncio.to_thread( | ||
| self._compile_pvpc, contracts, energy, pvpc, billing_rules | ||
| self.simulate_pvpc, contracts, energy, pvpc, billing_rules | ||
| ) | ||
@@ -124,3 +124,3 @@ confighash = f"pvpc-{hash(billing_rules.model_dump_json())}" | ||
| bills = await asyncio.to_thread( | ||
| self._compile_j2, contracts, energy, billing_rules | ||
| self.simulate_custom, contracts, energy, billing_rules | ||
| ) | ||
@@ -148,3 +148,3 @@ confighash = f"custom-{hash(billing_rules.model_dump_json())}" | ||
| async def _update_daily_statistics(self, start: datetime, end: datetime): | ||
| async def _update_daily_statistics(self, start: datetime, end: datetime) -> None: | ||
| """Update daily statistics within a date range.""" | ||
@@ -177,3 +177,3 @@ | ||
| async def _update_monthly_statistics(self, start: datetime, end: datetime): | ||
| async def _update_monthly_statistics(self, start: datetime, end: datetime) -> None: | ||
| """Update monthly statistics within a date range.""" | ||
@@ -209,3 +209,3 @@ | ||
| async def _find_missing_stats(self): | ||
| async def _find_missing_stats(self) -> list[datetime]: | ||
| """Return the list of days that are missing billing data.""" | ||
@@ -255,3 +255,3 @@ | ||
| async def fix_missing_statistics(self): | ||
| async def fix_missing_statistics(self) -> None: | ||
| """Recompile statistics to fix missing data.""" | ||
@@ -273,3 +273,3 @@ | ||
| def _compile_pvpc( | ||
| def simulate_pvpc( | ||
| self, | ||
@@ -316,4 +316,4 @@ contracts: list[Contract], | ||
| * rules.iva_tax | ||
| * p[dt].value_eur_kWh | ||
| * e[dt].value_kWh | ||
| * p[dt].value_eur_kwh | ||
| * e[dt].consumption_kwh | ||
| ) | ||
@@ -337,3 +337,3 @@ bill.power_term = ( | ||
| def _compile_j2( | ||
| def simulate_custom( | ||
| self, | ||
@@ -343,3 +343,3 @@ contracts: list[Contract], | ||
| rules: BillingRules, | ||
| ): | ||
| ) -> list[Bill]: | ||
| """Compile bills from custom rules.""" | ||
@@ -371,3 +371,3 @@ | ||
| params["p2_kw"] = p2_kw | ||
| params["kwh"] = e[dt].value_kWh | ||
| params["kwh"] = e[dt].consumption_kwh | ||
@@ -402,3 +402,3 @@ tariff = get_tariff(dt) | ||
| async def _get_contracts(self) -> list[Contract]: | ||
| """Get contracts.""" | ||
| res = await self.db.list_contracts(self._cups) | ||
@@ -410,2 +410,3 @@ return [x.data for x in res] | ||
| ) -> list[Energy]: | ||
| """Get energy.""" | ||
| res = await self.db.list_energy(self._cups, start, end) | ||
@@ -417,2 +418,3 @@ return [x.data for x in res] | ||
| ) -> list[EnergyPrice]: | ||
| """Get PVPC.""" | ||
| res = await self.db.list_pvpc(start, end) | ||
@@ -423,5 +425,4 @@ return [x.data for x in res] | ||
| """Return the timestamp of the latest bill record.""" | ||
| last_record = await self.db.get_last_bill(self._cups) | ||
| if last_record: | ||
| return last_record.datetime |
@@ -112,3 +112,3 @@ """Definition of a service for telemetry data handling.""" | ||
| async def fix_missing_statistics(self): | ||
| async def fix_missing_statistics(self) -> None: | ||
| """Recompile statistics to fix missing data.""" | ||
@@ -240,3 +240,3 @@ | ||
| async def login(self): | ||
| async def login(self) -> bool: | ||
| """Test login at Datadis.""" | ||
@@ -246,3 +246,3 @@ | ||
| async def update_supplies(self): | ||
| async def update_supplies(self) -> None: | ||
| """Update the list of supplies for the configured user.""" | ||
@@ -261,3 +261,3 @@ | ||
| self._contracts = await self.datadis.async_get_contract_detail( | ||
| cups, supply.distributorCode, self._authorized_nif | ||
| cups, supply.distributor_code, self._authorized_nif | ||
| ) | ||
@@ -270,3 +270,3 @@ for c in self._contracts: | ||
| async def update_energy(self, start: datetime, end: datetime): | ||
| async def update_energy(self, start: datetime, end: datetime) -> bool: | ||
| """Update the list of energy consumptions for the selected cups.""" | ||
@@ -279,7 +279,7 @@ | ||
| cups, | ||
| supply.distributorCode, | ||
| supply.distributor_code, | ||
| start, | ||
| end, | ||
| self._measurement_type, | ||
| supply.pointType, | ||
| supply.point_type, | ||
| self._authorized_nif, | ||
@@ -292,3 +292,3 @@ ) | ||
| async def update_power(self, start: datetime, end: datetime): | ||
| async def update_power(self, start: datetime, end: datetime) -> bool: | ||
| """Update the list of power peaks for the selected cups.""" | ||
@@ -300,3 +300,3 @@ cups = self._cups | ||
| cups, | ||
| supply.distributorCode, | ||
| supply.distributor_code, | ||
| start, | ||
@@ -311,3 +311,3 @@ end, | ||
| async def update_pvpc(self, start: datetime, end: datetime): | ||
| async def update_pvpc(self, start: datetime, end: datetime) -> bool: | ||
| """Update recent pvpc prices.""" | ||
@@ -336,3 +336,3 @@ | ||
| async def update_statistics(self, start: datetime, end: datetime): | ||
| async def update_statistics(self, start: datetime, end: datetime) -> None: | ||
| """Update the statistics during a period.""" | ||
@@ -343,3 +343,3 @@ | ||
| async def _update_daily_statistics(self, start: datetime, end: datetime): | ||
| async def _update_daily_statistics(self, start: datetime, end: datetime) -> None: | ||
| """Update daily statistics within a date range.""" | ||
@@ -372,3 +372,3 @@ | ||
| async def _update_monthly_statistics(self, start: datetime, end: datetime): | ||
| async def _update_monthly_statistics(self, start: datetime, end: datetime) -> None: | ||
| """Update monthly statistics within a date range.""" | ||
@@ -435,10 +435,10 @@ | ||
| delta_h=0, | ||
| value_kWh=0, | ||
| value_p1_kWh=0, | ||
| value_p2_kWh=0, | ||
| value_p3_kWh=0, | ||
| surplus_kWh=0, | ||
| surplus_p1_kWh=0, | ||
| surplus_p2_kWh=0, | ||
| surplus_p3_kWh=0, | ||
| value_kwh=0, | ||
| consumption_by_tariff=[0.0, 0.0, 0.0], | ||
| surplus_kwh=0, | ||
| surplus_by_tariff=[0.0, 0.0, 0.0], | ||
| generation_kwh=0, | ||
| generation_by_tariff=[0.0, 0.0, 0.0], | ||
| selfconsumption_kwh=0, | ||
| selfconsumption_by_tariff=[0.0, 0.0, 0.0], | ||
| ) | ||
@@ -448,17 +448,18 @@ | ||
| ref.delta_h += item.delta_h | ||
| ref.value_kWh += item.value_kWh | ||
| ref.surplus_kWh += item.surplus_kWh | ||
| if 1 == tariff: | ||
| ref.value_p1_kWh += item.value_kWh | ||
| ref.surplus_p1_kWh += item.surplus_kWh | ||
| elif 2 == tariff: | ||
| ref.value_p2_kWh += item.value_kWh | ||
| ref.surplus_p2_kWh += item.surplus_kWh | ||
| elif 3 == tariff: | ||
| ref.value_p3_kWh += item.value_kWh | ||
| ref.surplus_p3_kWh += item.surplus_kWh | ||
| ref.consumption_kwh += item.consumption_kwh | ||
| ref.surplus_kwh += item.surplus_kwh | ||
| ref.generation_kwh += item.generation_kwh | ||
| ref.selfconsumption_kwh += item.selfconsumption_kwh | ||
| if 1 <= tariff <= 3: | ||
| idx = tariff - 1 | ||
| ref.consumption_by_tariff[idx] += item.consumption_kwh | ||
| ref.surplus_by_tariff[idx] += item.surplus_kwh | ||
| ref.generation_by_tariff[idx] += item.generation_kwh | ||
| ref.selfconsumption_by_tariff[idx] += item.selfconsumption_kwh | ||
| return [agg_data[x] for x in agg_data] | ||
| async def _find_missing_stats(self, agg: typing.Literal["day", "month"] = "day"): | ||
| async def _find_missing_stats( | ||
| self, agg: typing.Literal["day", "month"] = "day" | ||
| ) -> list[datetime]: | ||
| """Return the list of days that are missing energy data.""" | ||
@@ -490,3 +491,3 @@ | ||
| async def _sync(self): | ||
| async def _sync(self) -> None: | ||
| """Load state.""" | ||
@@ -493,0 +494,0 @@ |
@@ -11,61 +11,69 @@ """Tests for DatadisConnector (offline).""" | ||
| SUPPLIES_RESPONSE = [ | ||
| { | ||
| "cups": "ESXXXXXXXXXXXXXXXXTEST", | ||
| "validDateFrom": "2022/03/09", | ||
| "validDateTo": "2022/10/28", | ||
| "address": "-", | ||
| "postalCode": "-", | ||
| "province": "-", | ||
| "municipality": "-", | ||
| "distributor": "-", | ||
| "pointType": 5, | ||
| "distributorCode": "2", | ||
| } | ||
| ] | ||
| SUPPLIES_RESPONSE = { | ||
| "supplies": [ | ||
| { | ||
| "cups": "ESXXXXXXXXXXXXXXXXTEST", | ||
| "validDateFrom": "2022/03/09", | ||
| "validDateTo": "2022/10/28", | ||
| "address": "-", | ||
| "postalCode": "-", | ||
| "province": "-", | ||
| "municipality": "-", | ||
| "distributor": "-", | ||
| "pointType": 5, | ||
| "distributorCode": "2", | ||
| } | ||
| ] | ||
| } | ||
| CONTRACTS_RESPONSE = [ | ||
| { | ||
| "startDate": "2022/03/09", | ||
| "endDate": "2022/10/28", | ||
| "marketer": "MARKETER", | ||
| "distributorCode": "2", | ||
| "contractedPowerkW": [4.4, 4.4], | ||
| } | ||
| ] | ||
| CONTRACTS_RESPONSE = { | ||
| "contract": [ | ||
| { | ||
| "startDate": "2022/03/09", | ||
| "endDate": "2022/10/28", | ||
| "marketer": "MARKETER", | ||
| "distributorCode": "2", | ||
| "contractedPowerkW": [4.4, 4.4], | ||
| } | ||
| ] | ||
| } | ||
| CONSUMPTIONS_RESPONSE = [ | ||
| { | ||
| "date": "2022/10/22", | ||
| "time": "01:00", | ||
| "consumptionKWh": 0.203, | ||
| "surplusEnergyKWh": 0, | ||
| "obtainMethod": "Real", | ||
| }, | ||
| { | ||
| "date": "2022/10/22", | ||
| "time": "02:00", | ||
| "consumptionKWh": 0.163, | ||
| "surplusEnergyKWh": 0, | ||
| "obtainMethod": "Real", | ||
| }, | ||
| ] | ||
| CONSUMPTIONS_RESPONSE = { | ||
| "timeCurve": [ | ||
| { | ||
| "date": "2022/10/22", | ||
| "time": "01:00", | ||
| "consumptionKWh": 0.203, | ||
| "surplusEnergyKWh": 0, | ||
| "obtainMethod": "Real", | ||
| }, | ||
| { | ||
| "date": "2022/10/22", | ||
| "time": "02:00", | ||
| "consumptionKWh": 0.163, | ||
| "surplusEnergyKWh": 0, | ||
| "obtainMethod": "Real", | ||
| }, | ||
| ] | ||
| } | ||
| MAXIMETER_RESPONSE = [ | ||
| { | ||
| "date": "2022/03/10", | ||
| "time": "14:15", | ||
| "maxPower": 2.436, | ||
| }, | ||
| { | ||
| "date": "2022/03/14", | ||
| "time": "13:15", | ||
| "maxPower": 3.008, | ||
| }, | ||
| { | ||
| "date": "2022/03/27", | ||
| "time": "10:30", | ||
| "maxPower": 3.288, | ||
| }, | ||
| ] | ||
| MAXIMETER_RESPONSE = { | ||
| "maxPower": [ | ||
| { | ||
| "date": "2022/03/10", | ||
| "time": "14:15", | ||
| "maxPower": 2.436, | ||
| }, | ||
| { | ||
| "date": "2022/03/14", | ||
| "time": "13:15", | ||
| "maxPower": 3.008, | ||
| }, | ||
| { | ||
| "date": "2022/03/27", | ||
| "time": "10:30", | ||
| "maxPower": 3.288, | ||
| }, | ||
| ] | ||
| } | ||
@@ -161,3 +169,3 @@ | ||
| mock_response.text = AsyncMock(return_value="text") | ||
| mock_response.json = AsyncMock(return_value=[]) | ||
| mock_response.json = AsyncMock(return_value={"supplies": []}) | ||
| mock_get.return_value.__aenter__.return_value = mock_response | ||
@@ -174,3 +182,3 @@ connector = DatadisConnector(MOCK_USERNAME, MOCK_PASSWORD) | ||
| """Test get_supplies with malformed response (missing required fields, syrupy snapshot).""" | ||
| malformed = [{"validDateFrom": "2022/03/09"}] # missing 'cups', etc. | ||
| malformed = {"supplies": [{"validDateFrom": "2022/03/09"}]} # missing 'cups', etc. | ||
| mock_response = MagicMock() | ||
@@ -191,6 +199,6 @@ mock_response.status = 200 | ||
| """Test get_supplies with partial valid/invalid response.""" | ||
| partial = [ | ||
| SUPPLIES_RESPONSE[0], | ||
| partial = {"supplies": [ | ||
| SUPPLIES_RESPONSE["supplies"][0], | ||
| {"validDateFrom": "2022/03/09"}, # invalid | ||
| ] | ||
| ]} | ||
| mock_response = MagicMock() | ||
@@ -251,16 +259,18 @@ mock_response.status = 200 | ||
| """Test get_supplies with optional fields as None.""" | ||
| response = [ | ||
| { | ||
| "cups": "ESXXXXXXXXXXXXXXXXTEST", | ||
| "validDateFrom": "2022/03/09", | ||
| "validDateTo": "2022/10/28", | ||
| "address": None, | ||
| "postalCode": None, | ||
| "province": None, | ||
| "municipality": None, | ||
| "distributor": None, | ||
| "pointType": 5, | ||
| "distributorCode": "2", | ||
| } | ||
| ] | ||
| response = { | ||
| "supplies": [ | ||
| { | ||
| "cups": "ESXXXXXXXXXXXXXXXXTEST", | ||
| "validDateFrom": "2022/03/09", | ||
| "validDateTo": "2022/10/28", | ||
| "address": None, | ||
| "postalCode": None, | ||
| "province": None, | ||
| "municipality": None, | ||
| "distributor": None, | ||
| "pointType": 5, | ||
| "distributorCode": "2", | ||
| } | ||
| ] | ||
| } | ||
| mock_response = MagicMock() | ||
@@ -267,0 +277,0 @@ mock_response.status = 200 |
+3
-3
| Metadata-Version: 2.4 | ||
| Name: e-data | ||
| Version: 2.0.0.dev112 | ||
| Version: 2.0.0.dev120 | ||
| Summary: Python library for managing spanish energy data from various web providers | ||
@@ -826,3 +826,3 @@ Author-email: VMG <vmayorg@outlook.es> | ||
| print(f"Coste total: {total_cost:.2f} €") | ||
| print(f"Consumo total: {sum(e.value_kWh for e in energy):.2f} kWh") | ||
| print(f"Consumo total: {sum(e.value_kwh for e in energy):.2f} kWh") | ||
@@ -845,3 +845,3 @@ # Ejecutar | ||
| # Actualizar facturación con tarifa fija | ||
| python -m edata.cli update-bill \ | ||
| python -m edata.cli update-custom-bill \ | ||
| --cups ES0000000000000000XX \ | ||
@@ -848,0 +848,0 @@ --p1-kw-year-eur 30.67 \ |
+1
-1
@@ -7,3 +7,3 @@ [build-system] | ||
| name = "e-data" | ||
| version = "2.0.0.dev112" | ||
| version = "2.0.0.dev120" | ||
| description = "Python library for managing spanish energy data from various web providers" | ||
@@ -10,0 +10,0 @@ readme = "README.md" |
+2
-2
@@ -122,3 +122,3 @@ [](https://pepy.tech/project/e-data) | ||
| print(f"Coste total: {total_cost:.2f} €") | ||
| print(f"Consumo total: {sum(e.value_kWh for e in energy):.2f} kWh") | ||
| print(f"Consumo total: {sum(e.value_kwh for e in energy):.2f} kWh") | ||
@@ -141,3 +141,3 @@ # Ejecutar | ||
| # Actualizar facturación con tarifa fija | ||
| python -m edata.cli update-bill \ | ||
| python -m edata.cli update-custom-bill \ | ||
| --cups ES0000000000000000XX \ | ||
@@ -144,0 +144,0 @@ --p1-kw-year-eur 30.67 \ |
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
249371
1.39%2697
2.39%