e-data 2.0.0.dev112__py3-none-any.whl → 2.0.0.dev120__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: e-data
3
- Version: 2.0.0.dev112
3
+ Version: 2.0.0.dev120
4
4
  Summary: Python library for managing spanish energy data from various web providers
5
5
  Author-email: VMG <vmayorg@outlook.es>
6
6
  License: GNU GENERAL PUBLIC LICENSE
@@ -823,7 +823,7 @@ async def main():
823
823
  # Mostrar resumen
824
824
  total_cost = sum(b.value_eur for b in bills)
825
825
  print(f"Coste total: {total_cost:.2f} €")
826
- print(f"Consumo total: {sum(e.value_kWh for e in energy):.2f} kWh")
826
+ print(f"Consumo total: {sum(e.value_kwh for e in energy):.2f} kWh")
827
827
 
828
828
  # Ejecutar
829
829
  asyncio.run(main())
@@ -841,7 +841,7 @@ python -m edata.cli show-supplies <username>
841
841
  python -m edata.cli download-all --cups ES0000000000000000XX <username>
842
842
 
843
843
  # Actualizar facturación con tarifa fija
844
- python -m edata.cli update-bill \
844
+ python -m edata.cli update-custom-bill \
845
845
  --cups ES0000000000000000XX \
846
846
  --p1-kw-year-eur 30.67 \
847
847
  --p2-kw-year-eur 1.42 \
@@ -0,0 +1,28 @@
1
+ e_data-2.0.0.dev120.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
2
+ edata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ edata/cli.py,sha256=0pmlaaXPVfbxPVHMovsuK9qf8PGXl38AV3gtIsk95O4,4010
4
+ edata/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ edata/core/const.py,sha256=X9J2f7lt9jXJnghqz0JFi-J7USa3xlYvbuiki70eHNE,43
6
+ edata/core/utils.py,sha256=P4vEPVnymvBp8W2rwwc_XMvd8_6Vx6N0GKnVx0WxE0I,1938
7
+ edata/database/__init__.py,sha256=HP_pDbGLzeMY9Ufm_Dps5cxzW_3YTonT8w8dstG558c,46
8
+ edata/database/controller.py,sha256=_KQJALolxUTf5G405USCoSDxYebg2819aYYgiTZF1kU,16358
9
+ edata/database/models.py,sha256=4rlRpPErULqYr6QJxZar1NBpJPWTcHftDN1MB3bwGyA,5096
10
+ edata/database/queries.py,sha256=kGoTWs0VJ4KjMJqBbhQGUeio55sZAysKABYGBUUGBE8,6687
11
+ edata/database/utils.py,sha256=V-FJfyAh8YJBGHB85BbWtG3Ilq09pM9Gl-L86TffB4g,1062
12
+ edata/models/__init__.py,sha256=nMPPPkGjUsF51PBFlChgHyb5C3aQiccTRiVHUdJjNj4,153
13
+ edata/models/bill.py,sha256=-qZ4l9f8BnPFlykFRoNYnmRe6-ELWmuNh6usks_k4X0,2674
14
+ edata/models/data.py,sha256=O1aj0H_G92mnJjm6mYhC8fnl8ZOoatGiokRf8b7ziD8,2388
15
+ edata/models/supply.py,sha256=qM0Q22-7PfBN93-Tu3ljAE58p5gfob0YWw-o5FccjrI,878
16
+ edata/providers/__init__.py,sha256=5xaRKPttYM6EpVE1I_vhUAF25L9pEM_qK_5Ix6xnQe0,104
17
+ edata/providers/datadis.py,sha256=ChV7IDO2Rc9YxpwQ-b9Scq527i_z2z2UiQa52mxZYmA,20143
18
+ edata/providers/redata.py,sha256=uvxO2TTtSNSfPRWHZifLjbaP7zAr6O7CZ1oRQFNbIhQ,3122
19
+ edata/services/bill_service.py,sha256=bjBIL1jl0Lcb2TG5VLKZgqT55PXdMPtEQW6fIPjWrVk,13411
20
+ edata/services/data_service.py,sha256=65w-fVllJY8GFM9eUZ7uVF_XK-QgKeYl4wfrqBFpXjE,16772
21
+ edata/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
+ edata/tests/test_datadis_connector.py,sha256=O3mjI5uIOgFz3PzQsApcG6J2yXd8rlEtj8By-9dsDbg,9270
23
+ edata/tests/test_redata_connector.py,sha256=SBvxsma5t-qvz2drxNisNM2P1STTurqEs1TSDgwaPJ8,508
24
+ edata/tests/test_services.py,sha256=XiwppLWqxm5vjgRtNmBeDFUaV5vVcQxerGgjnBJgmmk,4521
25
+ e_data-2.0.0.dev120.dist-info/METADATA,sha256=YZ4hqEryOAJoiurPpRmvZ-CQ2F11Qxu4RS8XwOk5_u8,48075
26
+ e_data-2.0.0.dev120.dist-info/WHEEL,sha256=qELbo2s1Yzl39ZmrAibXA2jjPLUYfnVhUNTlyF1rq0Y,92
27
+ e_data-2.0.0.dev120.dist-info/top_level.txt,sha256=Ez-fReWtUVTMcFuH0dzCnoKIgMBHgjLaBuba8QiKC7g,6
28
+ e_data-2.0.0.dev120.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.1)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
edata/cli.py CHANGED
@@ -17,7 +17,7 @@ app = typer.Typer()
17
17
 
18
18
 
19
19
  @app.command()
20
- def show_supplies(username: str):
20
+ def show_supplies(username: str) -> None:
21
21
  """Show supplies and contracts for a given datadis user."""
22
22
 
23
23
  password = getpass()
@@ -28,7 +28,7 @@ def show_supplies(username: str):
28
28
  typer.echo(supply.model_dump_json())
29
29
  if supplies:
30
30
  cups = supplies[0].cups
31
- distributor = supplies[0].distributorCode
31
+ distributor = supplies[0].distributor_code
32
32
  contracts = connector.get_contract_detail(cups, distributor)
33
33
  typer.echo("\nContracts:")
34
34
  for contract in contracts:
@@ -41,7 +41,7 @@ async def _download_all(
41
41
  nif: str,
42
42
  cups: str,
43
43
  authorized_nif: str | None = None,
44
- ):
44
+ ) -> None:
45
45
  """Download all data for a given datadis account and CUPS."""
46
46
 
47
47
  password = getpass()
@@ -63,7 +63,7 @@ def download_all(
63
63
  nif: str,
64
64
  cups: Annotated[str, typer.Option(help="The identifier of the Supply")],
65
65
  authorized_nif: str | None = None,
66
- ):
66
+ ) -> None:
67
67
  """Download all data for a given datadis account and CUPS."""
68
68
 
69
69
  asyncio.run(_download_all(nif, cups, authorized_nif))
@@ -77,7 +77,7 @@ async def _update_custom_bill(
77
77
  p2_kwh_eur: float,
78
78
  p3_kwh_eur: float,
79
79
  meter_month_eur: float,
80
- ):
80
+ ) -> None:
81
81
  """Download all data for a given datadis account and CUPS."""
82
82
 
83
83
  bs = BillService(cups, storage_path="./edata_cli")
@@ -105,7 +105,7 @@ def update_custom_bill(
105
105
  p2_kwh_eur: Annotated[float, typer.Option(help="Price per kWh at P2 tariff")],
106
106
  p3_kwh_eur: Annotated[float, typer.Option(help="Price per kWh at P3 tariff")],
107
107
  meter_month_eur: Annotated[float, typer.Option(help="Monthly cost of the meter")],
108
- ):
108
+ ) -> None:
109
109
  """Download all data for a given datadis account and CUPS."""
110
110
 
111
111
  asyncio.run(
@@ -121,7 +121,7 @@ def update_custom_bill(
121
121
  )
122
122
 
123
123
 
124
- async def _update_pvpc_bill(cups: str):
124
+ async def _update_pvpc_bill(cups: str) -> None:
125
125
  """Download all data for a given datadis account and CUPS."""
126
126
 
127
127
  bs = BillService(cups, storage_path="./edata_cli")
@@ -131,7 +131,7 @@ async def _update_pvpc_bill(cups: str):
131
131
  @app.command()
132
132
  def update_pvpc_bill(
133
133
  cups: Annotated[str, typer.Option(help="The identifier of the Supply")],
134
- ):
134
+ ) -> None:
135
135
  """Download all data for a given datadis account and CUPS."""
136
136
 
137
137
  asyncio.run(
edata/core/utils.py CHANGED
@@ -43,7 +43,7 @@ def get_tariff(dt: datetime) -> int:
43
43
  return 0
44
44
 
45
45
 
46
- def get_contract_for_dt(contracts: list[Contract], date: datetime):
46
+ def get_contract_for_dt(contracts: list[Contract], date: datetime) -> Contract | None:
47
47
  """Return the active contract for a provided datetime."""
48
48
 
49
49
  for contract in contracts:
@@ -49,12 +49,12 @@ class EdataDB:
49
49
  return cls._instance
50
50
 
51
51
  @property
52
- def engine(self):
52
+ def engine(self) -> AsyncEngine | None:
53
53
  """Return the async database engine."""
54
54
 
55
55
  return self._engine
56
56
 
57
- async def _ensure_tables(self):
57
+ async def _ensure_tables(self) -> None:
58
58
  """Create tables if not already created (lazy init)."""
59
59
 
60
60
  if self._tables_initialized:
@@ -84,11 +84,11 @@ class EdataDB:
84
84
  self,
85
85
  session: AsyncSession,
86
86
  query: SelectOfScalar,
87
- data,
87
+ data: typing.Any,
88
88
  commit: bool = True,
89
89
  overrides: dict[str, typing.Any] | None = None,
90
90
  ) -> T | None: # type: ignore
91
- """Updates a single record in the database."""
91
+ """Update a single record in the database."""
92
92
 
93
93
  result = await session.exec(query)
94
94
  existing = result.first()
@@ -240,7 +240,7 @@ class EdataDB:
240
240
  session, q.get_energy(cups, energy.datetime), record
241
241
  )
242
242
 
243
- async def add_energy_list(self, cups: str, energy: list[Energy]):
243
+ async def add_energy_list(self, cups: str, energy: list[Energy]) -> None:
244
244
  """Add or update a list of energy records."""
245
245
 
246
246
  await self._ensure_tables()
@@ -264,7 +264,7 @@ class EdataDB:
264
264
  session, q.get_power(cups, power.datetime), record
265
265
  )
266
266
 
267
- async def add_power_list(self, cups: str, power: list[Power]):
267
+ async def add_power_list(self, cups: str, power: list[Power]) -> None:
268
268
  """Add or update a list of power records."""
269
269
 
270
270
  await self._ensure_tables()
@@ -285,7 +285,7 @@ class EdataDB:
285
285
  session, q.get_pvpc(pvpc.datetime), record
286
286
  )
287
287
 
288
- async def add_pvpc_list(self, pvpc: list[EnergyPrice]):
288
+ async def add_pvpc_list(self, pvpc: list[EnergyPrice]) -> None:
289
289
  """Add or update a list of pvpc records."""
290
290
 
291
291
  await self._ensure_tables()
@@ -351,7 +351,7 @@ class EdataDB:
351
351
  confhash: str,
352
352
  complete: bool,
353
353
  bill: list[Bill],
354
- ):
354
+ ) -> None:
355
355
  """Add or update a list of bill records."""
356
356
 
357
357
  await self._ensure_tables()
@@ -374,7 +374,7 @@ class EdataDB:
374
374
  session, queries, items, override=["complete", "confhash"]
375
375
  )
376
376
 
377
- async def list_supplies(self):
377
+ async def list_supplies(self) -> typing.Sequence[SupplyModel]:
378
378
  """List all supply records."""
379
379
 
380
380
  await self._ensure_tables()
@@ -382,7 +382,9 @@ class EdataDB:
382
382
  result = await session.exec(q.list_supply())
383
383
  return result.all()
384
384
 
385
- async def list_contracts(self, cups: str | None = None):
385
+ async def list_contracts(
386
+ self, cups: str | None = None
387
+ ) -> typing.Sequence[ContractModel]:
386
388
  """List all contract records."""
387
389
 
388
390
  await self._ensure_tables()
@@ -395,7 +397,7 @@ class EdataDB:
395
397
  cups: str,
396
398
  date_from: datetime | None = None,
397
399
  date_to: datetime | None = None,
398
- ):
400
+ ) -> typing.Sequence[EnergyModel]:
399
401
  """List energy records."""
400
402
 
401
403
  await self._ensure_tables()
@@ -408,7 +410,7 @@ class EdataDB:
408
410
  cups: str,
409
411
  date_from: datetime | None = None,
410
412
  date_to: datetime | None = None,
411
- ):
413
+ ) -> typing.Sequence[PowerModel]:
412
414
  """List power records."""
413
415
 
414
416
  await self._ensure_tables()
@@ -420,7 +422,7 @@ class EdataDB:
420
422
  self,
421
423
  date_from: datetime | None = None,
422
424
  date_to: datetime | None = None,
423
- ):
425
+ ) -> typing.Sequence[PVPCModel]:
424
426
  """List pvpc records."""
425
427
 
426
428
  await self._ensure_tables()
@@ -435,7 +437,7 @@ class EdataDB:
435
437
  date_from: datetime | None = None,
436
438
  date_to: datetime | None = None,
437
439
  complete: bool | None = None,
438
- ):
440
+ ) -> typing.Sequence[StatisticsModel]:
439
441
  """List statistics records filtered by type ('day' or 'month') and date range."""
440
442
 
441
443
  await self._ensure_tables()
@@ -452,7 +454,7 @@ class EdataDB:
452
454
  date_from: datetime | None = None,
453
455
  date_to: datetime | None = None,
454
456
  complete: bool | None = None,
455
- ):
457
+ ) -> typing.Sequence[BillModel]:
456
458
  """List bill records filtered by type ('hour', 'day' or 'month') and date range."""
457
459
 
458
460
  await self._ensure_tables()
edata/database/queries.py CHANGED
@@ -215,7 +215,9 @@ def list_pvpc(
215
215
  return query
216
216
 
217
217
 
218
- def get_bill(cups, type_, datetime_):
218
+ def get_bill(
219
+ cups: str, type_: typing.Literal["hour", "day", "month"], datetime_: datetime | None
220
+ ) -> SelectOfScalar[BillModel]:
219
221
  """Query that selects a bill."""
220
222
 
221
223
  query = select(BillModel).where(BillModel.cups == cups)
edata/database/utils.py CHANGED
@@ -7,10 +7,7 @@ T = TypeVar("T", bound=BaseModel)
7
7
 
8
8
 
9
9
  class PydanticJSON(TypeDecorator):
10
- """
11
- Tipo de SQLAlchemy para guardar modelos Pydantic como JSON.
12
- Automáticamente serializa al guardar y deserializa al leer.
13
- """
10
+ """SQLAlchemy type to guard Pydantic models as JSON."""
14
11
 
15
12
  impl = JSON
16
13
  cache_ok = True
@@ -20,6 +17,7 @@ class PydanticJSON(TypeDecorator):
20
17
  self.pydantic_model = pydantic_model
21
18
 
22
19
  def process_bind_param(self, value: T | None, dialect: Any) -> Any:
20
+ """Process binding parameter."""
23
21
  # Python -> Base de Datos
24
22
  if value is None:
25
23
  return None
@@ -27,6 +25,7 @@ class PydanticJSON(TypeDecorator):
27
25
  return value.model_dump(mode="json")
28
26
 
29
27
  def process_result_value(self, value: Any, dialect: Any) -> T | None:
28
+ """Process result value."""
30
29
  # Base de Datos -> Python
31
30
  if value is None:
32
31
  return None
edata/models/bill.py CHANGED
@@ -6,15 +6,15 @@ from pydantic import BaseModel, Field
6
6
 
7
7
 
8
8
  class EnergyPrice(BaseModel):
9
- """Data structure to represent pricing data."""
9
+ """Represent pricing data."""
10
10
 
11
11
  datetime: datetime
12
- value_eur_kWh: float
12
+ value_eur_kwh: float
13
13
  delta_h: float
14
14
 
15
15
 
16
16
  class Bill(BaseModel):
17
- """Data structure to represent a bill during a period."""
17
+ """Represent a bill during a period."""
18
18
 
19
19
  datetime: datetime
20
20
  delta_h: float
@@ -26,7 +26,7 @@ class Bill(BaseModel):
26
26
 
27
27
 
28
28
  class BillingRules(BaseModel):
29
- """Data structure to represent a generic billing rule."""
29
+ """Represent a generic billing rule."""
30
30
 
31
31
  p1_kw_year_eur: float
32
32
  p2_kw_year_eur: float
@@ -50,7 +50,7 @@ class BillingRules(BaseModel):
50
50
 
51
51
 
52
52
  class PVPCBillingRules(BillingRules):
53
- """Data structure to represent a PVPC billing rule."""
53
+ """Represent a PVPC billing rule."""
54
54
 
55
55
  p1_kw_year_eur: float = Field(default=30.67266)
56
56
  p2_kw_year_eur: float = Field(default=1.4243591)
edata/models/data.py CHANGED
@@ -6,32 +6,84 @@ from pydantic import BaseModel, Field
6
6
 
7
7
 
8
8
  class Energy(BaseModel):
9
- """Data structure to represent energy consumption and/or surplus measurements."""
9
+ """Represent energy consumption and/or surplus measurements."""
10
10
 
11
11
  datetime: datetime
12
12
  delta_h: float
13
- value_kWh: float
14
- surplus_kWh: float = Field(0)
13
+ consumption_kwh: float
14
+ surplus_kwh: float = Field(0)
15
+ generation_kwh: float = Field(0)
16
+ selfconsumption_kwh: float = Field(0)
15
17
  real: bool
16
18
 
17
19
 
18
20
  class Power(BaseModel):
19
- """Data structure to represent power measurements."""
21
+ """Represent power measurements."""
20
22
 
21
23
  datetime: datetime
22
- value_kW: float
24
+ value_kw: float
23
25
 
24
26
 
25
27
  class Statistics(BaseModel):
26
- """Data structure to represent aggregated energy/surplus data."""
28
+ """Represent aggregated energy/surplus data."""
27
29
 
28
30
  datetime: datetime
29
31
  delta_h: float = Field(0)
30
- value_kWh: float = Field(0)
31
- value_p1_kWh: float = Field(0)
32
- value_p2_kWh: float = Field(0)
33
- value_p3_kWh: float = Field(0)
34
- surplus_kWh: float = Field(0)
35
- surplus_p1_kWh: float = Field(0)
36
- surplus_p2_kWh: float = Field(0)
37
- surplus_p3_kWh: float = Field(0)
32
+ consumption_kwh: float = Field(0)
33
+ consumption_by_tariff: list[float] = Field(default_factory=lambda: [0.0, 0.0, 0.0])
34
+ surplus_kwh: float = Field(0)
35
+ surplus_by_tariff: list[float] = Field(default_factory=lambda: [0.0, 0.0, 0.0])
36
+ generation_kwh: float = Field(0)
37
+ generation_by_tariff: list[float] = Field(default_factory=lambda: [0.0, 0.0, 0.0])
38
+ selfconsumption_kwh: float = Field(0)
39
+ selfconsumption_by_tariff: list[float] = Field(
40
+ default_factory=lambda: [0.0, 0.0, 0.0]
41
+ )
42
+
43
+ @property
44
+ def consumption_p1_kwh(self) -> float:
45
+ return self.consumption_by_tariff[0]
46
+
47
+ @property
48
+ def consumption_p2_kwh(self) -> float:
49
+ return self.consumption_by_tariff[1]
50
+
51
+ @property
52
+ def consumption_p3_kwh(self) -> float:
53
+ return self.consumption_by_tariff[2]
54
+
55
+ @property
56
+ def surplus_p1_kwh(self) -> float:
57
+ return self.surplus_by_tariff[0]
58
+
59
+ @property
60
+ def surplus_p2_kwh(self) -> float:
61
+ return self.surplus_by_tariff[1]
62
+
63
+ @property
64
+ def surplus_p3_kwh(self) -> float:
65
+ return self.surplus_by_tariff[2]
66
+
67
+ @property
68
+ def generation_p1_kwh(self) -> float:
69
+ return self.generation_by_tariff[0]
70
+
71
+ @property
72
+ def generation_p2_kwh(self) -> float:
73
+ return self.generation_by_tariff[1]
74
+
75
+ @property
76
+ def generation_p3_kwh(self) -> float:
77
+ return self.generation_by_tariff[2]
78
+
79
+ @property
80
+ def selfconsumption_p1_kwh(self) -> float:
81
+ return self.selfconsumption_by_tariff[0]
82
+
83
+ @property
84
+ def selfconsumption_p2_kwh(self) -> float:
85
+ return self.selfconsumption_by_tariff[1]
86
+
87
+ @property
88
+ def selfconsumption_p3_kwh(self) -> float:
89
+ return self.selfconsumption_by_tariff[2]
edata/models/supply.py CHANGED
@@ -16,8 +16,8 @@ class Supply(BaseModel):
16
16
  province: str | None
17
17
  municipality: str | None
18
18
  distributor: str | None
19
- pointType: int
20
- distributorCode: str
19
+ point_type: int
20
+ distributor_code: str
21
21
 
22
22
 
23
23
  class Contract(BaseModel):
@@ -26,6 +26,15 @@ class Contract(BaseModel):
26
26
  date_start: datetime
27
27
  date_end: datetime
28
28
  marketer: str
29
- distributorCode: str
30
- power_p1: float | None
31
- power_p2: float | None
29
+ distributor_code: str
30
+ power: list[float]
31
+
32
+ @property
33
+ def power_p1(self) -> float | None:
34
+ """Return power P1."""
35
+ return self.power[0] if len(self.power) > 0 else None
36
+
37
+ @property
38
+ def power_p2(self) -> float | None:
39
+ """Return power P2."""
40
+ return self.power[1] if len(self.power) > 1 else None
@@ -1,10 +1,4 @@
1
- """Datadis API connector.
2
-
3
- To fetch data from datadis.es private API.
4
- There a few issues that are workarounded:
5
- - You have to wait 24h between two identical requests.
6
- - Datadis server does not like ranges greater than 1 month.
7
- """
1
+ """Datadis API connector."""
8
2
 
9
3
  import asyncio
10
4
  import contextlib
@@ -28,7 +22,7 @@ TOKEN_USERNAME = "username"
28
22
  TOKEN_PASSWD = "password"
29
23
 
30
24
  # Supplies-related constants
31
- URL_GET_SUPPLIES = "https://datadis.es/api-private/api/get-supplies"
25
+ URL_GET_SUPPLIES = "https://datadis.es/api-private/api/get-supplies-v2"
32
26
  GET_SUPPLIES_MANDATORY_FIELDS = [
33
27
  "cups",
34
28
  "validDateFrom",
@@ -38,7 +32,7 @@ GET_SUPPLIES_MANDATORY_FIELDS = [
38
32
  ]
39
33
 
40
34
  # Contracts-related constants
41
- URL_GET_CONTRACT_DETAIL = "https://datadis.es/api-private/api/get-contract-detail"
35
+ URL_GET_CONTRACT_DETAIL = "https://datadis.es/api-private/api/get-contract-detail-v2"
42
36
  GET_CONTRACT_DETAIL_MANDATORY_FIELDS = [
43
37
  "startDate",
44
38
  "endDate",
@@ -47,7 +41,7 @@ GET_CONTRACT_DETAIL_MANDATORY_FIELDS = [
47
41
  ]
48
42
 
49
43
  # Consumption-related constants
50
- URL_GET_CONSUMPTION_DATA = "https://datadis.es/api-private/api/get-consumption-data"
44
+ URL_GET_CONSUMPTION_DATA = "https://datadis.es/api-private/api/get-consumption-data-v2"
51
45
  GET_CONSUMPTION_DATA_MANDATORY_FIELDS = [
52
46
  "time",
53
47
  "date",
@@ -56,7 +50,7 @@ GET_CONSUMPTION_DATA_MANDATORY_FIELDS = [
56
50
  ]
57
51
 
58
52
  # Maximeter-related constants
59
- URL_GET_MAX_POWER = "https://datadis.es/api-private/api/get-max-power"
53
+ URL_GET_MAX_POWER = "https://datadis.es/api-private/api/get-max-power-v2"
60
54
  GET_MAX_POWER_MANDATORY_FIELDS = ["time", "date", "maxPower"]
61
55
 
62
56
  # Timing constants
@@ -68,7 +62,7 @@ QUERY_LIMIT = timedelta(hours=24) # a datadis limitation, again...
68
62
  RECENT_CACHE_SUBDIR = "cache"
69
63
 
70
64
 
71
- def migrate_storage(storage_dir):
65
+ def migrate_storage(storage_dir: str) -> None:
72
66
  """Migrate storage from older versions."""
73
67
  with contextlib.suppress(FileNotFoundError):
74
68
  os.remove(os.path.join(storage_dir, "edata_recent_queries.json"))
@@ -102,9 +96,8 @@ class DatadisConnector:
102
96
  os.makedirs(self._recent_cache_dir, exist_ok=True)
103
97
  self._cache = diskcache.Cache(self._recent_cache_dir)
104
98
 
105
- def _get_hash(self, item: str):
99
+ def _get_hash(self, item: str) -> str:
106
100
  """Return a hash."""
107
-
108
101
  return hashlib.md5(item.encode()).hexdigest()
109
102
 
110
103
  def _set_cache(self, key: str, data: dict | None = None) -> None:
@@ -121,7 +114,7 @@ class DatadisConnector:
121
114
  hash_query = self._get_hash(key)
122
115
  return hash_query in self._cache
123
116
 
124
- def _get_cache(self, key: str):
117
+ def _get_cache(self, key: str) -> dict | None:
125
118
  """Return cached response for a query (diskcache)."""
126
119
  hash_query = self._get_hash(key)
127
120
  try:
@@ -129,7 +122,7 @@ class DatadisConnector:
129
122
  except Exception:
130
123
  return None
131
124
 
132
- async def _async_get_token(self):
125
+ async def _async_get_token(self) -> bool:
133
126
  """Private async method that fetches a new token if needed."""
134
127
  _LOGGER.debug("No token found, fetching a new one")
135
128
  is_valid_token = False
@@ -158,11 +151,11 @@ class DatadisConnector:
158
151
  _LOGGER.error("Exception while retrieving token: %s", e)
159
152
  return is_valid_token
160
153
 
161
- async def async_login(self):
154
+ async def async_login(self) -> bool:
162
155
  """Test to login with provided credentials (async)."""
163
156
  return await self._async_get_token()
164
157
 
165
- def login(self):
158
+ def login(self) -> bool:
166
159
  """Test to login with provided credentials (sync wrapper)."""
167
160
  return asyncio.run(self.async_login())
168
161
 
@@ -288,6 +281,7 @@ class DatadisConnector:
288
281
  async def async_get_supplies(
289
282
  self, authorized_nif: str | None = None
290
283
  ) -> list[Supply]:
284
+ """Datadis 'get_supplies' query."""
291
285
  data = {}
292
286
  if authorized_nif is not None:
293
287
  data["authorizedNif"] = authorized_nif
@@ -296,7 +290,7 @@ class DatadisConnector:
296
290
  )
297
291
  supplies = []
298
292
  tomorrow_str = (datetime.today() + timedelta(days=1)).strftime("%Y/%m/%d")
299
- for i in response:
293
+ for i in response.get("supplies", []):
300
294
  if all(k in i for k in GET_SUPPLIES_MANDATORY_FIELDS):
301
295
  supplies.append(
302
296
  Supply(
@@ -322,8 +316,8 @@ class DatadisConnector:
322
316
  province=i.get("province", None),
323
317
  municipality=i.get("municipality", None),
324
318
  distributor=i.get("distributor", None),
325
- pointType=i["pointType"],
326
- distributorCode=i["distributorCode"],
319
+ point_type=i["pointType"],
320
+ distributor_code=i["distributorCode"],
327
321
  )
328
322
  )
329
323
  else:
@@ -333,13 +327,14 @@ class DatadisConnector:
333
327
  )
334
328
  return supplies
335
329
 
336
- def get_supplies(self, authorized_nif: str | None = None):
330
+ def get_supplies(self, authorized_nif: str | None = None) -> list[Supply]:
337
331
  """Datadis 'get_supplies' query (sync wrapper)."""
338
332
  return asyncio.run(self.async_get_supplies(authorized_nif=authorized_nif))
339
333
 
340
334
  async def async_get_contract_detail(
341
335
  self, cups: str, distributor_code: str, authorized_nif: str | None = None
342
336
  ) -> list[Contract]:
337
+ """Datadis 'get_contract_detail' query."""
343
338
  data = {"cups": cups, "distributorCode": distributor_code}
344
339
  if authorized_nif is not None:
345
340
  data["authorizedNif"] = authorized_nif
@@ -348,7 +343,7 @@ class DatadisConnector:
348
343
  )
349
344
  contracts = []
350
345
  tomorrow_str = (datetime.today() + timedelta(days=1)).strftime("%Y/%m/%d")
351
- for i in response:
346
+ for i in response.get("contract", []):
352
347
  if all(k in i for k in GET_CONTRACT_DETAIL_MANDATORY_FIELDS):
353
348
  contracts.append(
354
349
  Contract(
@@ -361,17 +356,8 @@ class DatadisConnector:
361
356
  "%Y/%m/%d",
362
357
  ),
363
358
  marketer=i["marketer"],
364
- distributorCode=distributor_code,
365
- power_p1=(
366
- i["contractedPowerkW"][0]
367
- if isinstance(i["contractedPowerkW"], list)
368
- else None
369
- ),
370
- power_p2=(
371
- i["contractedPowerkW"][1]
372
- if (len(i["contractedPowerkW"]) > 1)
373
- else None
374
- ),
359
+ distributor_code=distributor_code,
360
+ power=i["contractedPowerkW"],
375
361
  )
376
362
  )
377
363
  else:
@@ -383,7 +369,7 @@ class DatadisConnector:
383
369
 
384
370
  def get_contract_detail(
385
371
  self, cups: str, distributor_code: str, authorized_nif: str | None = None
386
- ):
372
+ ) -> list[Contract]:
387
373
  """Datadis get_contract_detail query (sync wrapper)."""
388
374
  return asyncio.run(
389
375
  self.async_get_contract_detail(cups, distributor_code, authorized_nif)
@@ -399,7 +385,7 @@ class DatadisConnector:
399
385
  point_type: int,
400
386
  authorized_nif: str | None = None,
401
387
  ) -> list[Energy]:
402
-
388
+ """Datadis 'get_consumption_data' query."""
403
389
  data = {
404
390
  "cups": cups,
405
391
  "distributorCode": distributor_code,
@@ -414,7 +400,7 @@ class DatadisConnector:
414
400
  response = await self._async_get(URL_GET_CONSUMPTION_DATA, request_data=data)
415
401
 
416
402
  consumptions = []
417
- for i in response:
403
+ for i in response.get("timeCurve", []):
418
404
  if "consumptionKWh" in i:
419
405
  if all(k in i for k in GET_CONSUMPTION_DATA_MANDATORY_FIELDS):
420
406
  hour = str(int(i["time"].split(":")[0]) - 1)
@@ -423,15 +409,26 @@ class DatadisConnector:
423
409
  )
424
410
  if not (start_date <= date_as_dt <= end_date):
425
411
  continue # skip element if dt is out of range
426
- _surplus = i.get("surplusEnergyKWh", 0)
427
- if _surplus is None:
428
- _surplus = 0
412
+
413
+ # sanitize these values
414
+ _surplus_kwh = i.get("surplusEnergyKWh", 0)
415
+ if _surplus_kwh is None:
416
+ _surplus_kwh = 0
417
+ _generation_kwh = i.get("generationEnergyKWh", 0)
418
+ if _generation_kwh is None:
419
+ _generation_kwh = 0
420
+ _selfconsumption_kwh = i.get("selfConsumptionEnergyKWh", 0)
421
+ if _selfconsumption_kwh is None:
422
+ _selfconsumption_kwh = 0
423
+
429
424
  consumptions.append(
430
425
  Energy(
431
426
  datetime=date_as_dt,
432
427
  delta_h=1,
433
- value_kWh=i["consumptionKWh"],
434
- surplus_kWh=_surplus,
428
+ consumption_kwh=i["consumptionKWh"],
429
+ surplus_kwh=_surplus_kwh,
430
+ generation_kwh=_generation_kwh,
431
+ selfconsumption_kwh=_selfconsumption_kwh,
435
432
  real=i["obtainMethod"] == "Real",
436
433
  )
437
434
  )
@@ -451,7 +448,7 @@ class DatadisConnector:
451
448
  measurement_type: str,
452
449
  point_type: int,
453
450
  authorized_nif: str | None = None,
454
- ):
451
+ ) -> list[Energy]:
455
452
  """Datadis get_consumption_data query (sync wrapper)."""
456
453
  return asyncio.run(
457
454
  self.async_get_consumption_data(
@@ -473,6 +470,7 @@ class DatadisConnector:
473
470
  end_date: datetime,
474
471
  authorized_nif: str | None = None,
475
472
  ) -> list[Power]:
473
+ """Datadis 'get_max_power' query."""
476
474
  data = {
477
475
  "cups": cups,
478
476
  "distributorCode": distributor_code,
@@ -483,14 +481,14 @@ class DatadisConnector:
483
481
  data["authorizedNif"] = authorized_nif
484
482
  response = await self._async_get(URL_GET_MAX_POWER, request_data=data)
485
483
  maxpower_values = []
486
- for i in response:
484
+ for i in response.get("maxPower", []):
487
485
  if all(k in i for k in GET_MAX_POWER_MANDATORY_FIELDS):
488
486
  maxpower_values.append(
489
487
  Power(
490
488
  datetime=datetime.strptime(
491
489
  f"{i['date']} {i['time']}", "%Y/%m/%d %H:%M"
492
490
  ),
493
- value_kW=i["maxPower"],
491
+ value_kw=i["maxPower"],
494
492
  )
495
493
  )
496
494
  else:
@@ -507,7 +505,7 @@ class DatadisConnector:
507
505
  start_date: datetime,
508
506
  end_date: datetime,
509
507
  authorized_nif: str | None = None,
510
- ):
508
+ ) -> list[Power]:
511
509
  """Datadis get_max_power query (sync wrapper)."""
512
510
  return asyncio.run(
513
511
  self.async_get_max_power(
edata/providers/redata.py CHANGED
@@ -62,7 +62,7 @@ class REDataConnector:
62
62
  datetime=parser.parse(element["datetime"]).replace(
63
63
  tzinfo=None
64
64
  ),
65
- value_eur_kWh=element["value"] / 1000,
65
+ value_eur_kwh=element["value"] / 1000,
66
66
  delta_h=1,
67
67
  )
68
68
  )
@@ -79,7 +79,7 @@ class REDataConnector:
79
79
 
80
80
  def get_realtime_prices(
81
81
  self, dt_from: dt.datetime, dt_to: dt.datetime, is_ceuta_melilla: bool = False
82
- ) -> list:
82
+ ) -> list[EnergyPrice]:
83
83
  """GET query to fetch realtime pvpc prices, historical data is limited to current month (sync wrapper)"""
84
84
  return asyncio.run(
85
85
  self.async_get_realtime_prices(dt_from, dt_to, is_ceuta_melilla)
@@ -115,12 +115,12 @@ class BillService:
115
115
  pvpc = await self._get_pvpc()
116
116
  billing_rules = PVPCBillingRules(**billing_rules.model_dump())
117
117
  bills = await asyncio.to_thread(
118
- self._compile_pvpc, contracts, energy, pvpc, billing_rules
118
+ self.simulate_pvpc, contracts, energy, pvpc, billing_rules
119
119
  )
120
120
  confighash = f"pvpc-{hash(billing_rules.model_dump_json())}"
121
121
  else:
122
122
  bills = await asyncio.to_thread(
123
- self._compile_j2, contracts, energy, billing_rules
123
+ self.simulate_custom, contracts, energy, billing_rules
124
124
  )
125
125
  confighash = f"custom-{hash(billing_rules.model_dump_json())}"
126
126
 
@@ -143,7 +143,7 @@ class BillService:
143
143
  await self._update_daily_statistics(start, end)
144
144
  await self._update_monthly_statistics(start, end)
145
145
 
146
- async def _update_daily_statistics(self, start: datetime, end: datetime):
146
+ async def _update_daily_statistics(self, start: datetime, end: datetime) -> None:
147
147
  """Update daily statistics within a date range."""
148
148
 
149
149
  day_start = get_day(start)
@@ -171,7 +171,7 @@ class BillService:
171
171
  stat.datetime.date(),
172
172
  )
173
173
 
174
- async def _update_monthly_statistics(self, start: datetime, end: datetime):
174
+ async def _update_monthly_statistics(self, start: datetime, end: datetime) -> None:
175
175
  """Update monthly statistics within a date range."""
176
176
 
177
177
  month_start = get_month(start)
@@ -202,7 +202,7 @@ class BillService:
202
202
  stat.datetime.date(),
203
203
  )
204
204
 
205
- async def _find_missing_stats(self):
205
+ async def _find_missing_stats(self) -> list[datetime]:
206
206
  """Return the list of days that are missing billing data."""
207
207
 
208
208
  stats = await self.db.list_bill(self._cups, "day", complete=False)
@@ -247,7 +247,7 @@ class BillService:
247
247
 
248
248
  return [agg_data[x] for x in agg_data]
249
249
 
250
- async def fix_missing_statistics(self):
250
+ async def fix_missing_statistics(self) -> None:
251
251
  """Recompile statistics to fix missing data."""
252
252
 
253
253
  missing = await self._find_missing_stats()
@@ -264,7 +264,7 @@ class BillService:
264
264
  end = month + relativedelta.relativedelta(months=1) - timedelta(minutes=1)
265
265
  await self._update_monthly_statistics(month, end)
266
266
 
267
- def _compile_pvpc(
267
+ def simulate_pvpc(
268
268
  self,
269
269
  contracts: list[Contract],
270
270
  energy: list[Energy],
@@ -306,8 +306,8 @@ class BillService:
306
306
  bill.energy_term = (
307
307
  rules.electricity_tax
308
308
  * rules.iva_tax
309
- * p[dt].value_eur_kWh
310
- * e[dt].value_kWh
309
+ * p[dt].value_eur_kwh
310
+ * e[dt].consumption_kwh
311
311
  )
312
312
  bill.power_term = (
313
313
  rules.electricity_tax
@@ -326,12 +326,12 @@ class BillService:
326
326
 
327
327
  return [x for x in b.values()]
328
328
 
329
- def _compile_j2(
329
+ def simulate_custom(
330
330
  self,
331
331
  contracts: list[Contract],
332
332
  energy: list[Energy],
333
333
  rules: BillingRules,
334
- ):
334
+ ) -> list[Bill]:
335
335
  """Compile bills from custom rules."""
336
336
 
337
337
  e = {x.datetime: x for x in energy}
@@ -358,7 +358,7 @@ class BillService:
358
358
  params = rules.model_dump()
359
359
  params["p1_kw"] = p1_kw
360
360
  params["p2_kw"] = p2_kw
361
- params["kwh"] = e[dt].value_kWh
361
+ params["kwh"] = e[dt].consumption_kwh
362
362
 
363
363
  tariff = get_tariff(dt)
364
364
  if tariff == 1:
@@ -388,25 +388,26 @@ class BillService:
388
388
  return [x for x in b.values()]
389
389
 
390
390
  async def _get_contracts(self) -> list[Contract]:
391
-
391
+ """Get contracts."""
392
392
  res = await self.db.list_contracts(self._cups)
393
393
  return [x.data for x in res]
394
394
 
395
395
  async def _get_energy(
396
396
  self, start: datetime | None = None, end: datetime | None = None
397
397
  ) -> list[Energy]:
398
+ """Get energy."""
398
399
  res = await self.db.list_energy(self._cups, start, end)
399
400
  return [x.data for x in res]
400
401
 
401
402
  async def _get_pvpc(
402
403
  self, start: datetime | None = None, end: datetime | None = None
403
404
  ) -> list[EnergyPrice]:
405
+ """Get PVPC."""
404
406
  res = await self.db.list_pvpc(start, end)
405
407
  return [x.data for x in res]
406
408
 
407
409
  async def _get_last_bill_dt(self) -> datetime | None:
408
410
  """Return the timestamp of the latest bill record."""
409
-
410
411
  last_record = await self.db.get_last_bill(self._cups)
411
412
  if last_record:
412
413
  return last_record.datetime
@@ -109,7 +109,7 @@ class DataService:
109
109
  data = await self.db.list_statistics(self._cups, type_, start, end, complete)
110
110
  return [x.data for x in data]
111
111
 
112
- async def fix_missing_statistics(self):
112
+ async def fix_missing_statistics(self) -> None:
113
113
  """Recompile statistics to fix missing data."""
114
114
 
115
115
  missing = await self._find_missing_stats()
@@ -236,12 +236,12 @@ class DataService:
236
236
 
237
237
  return True
238
238
 
239
- async def login(self):
239
+ async def login(self) -> bool:
240
240
  """Test login at Datadis."""
241
241
 
242
242
  return await self.datadis.async_login()
243
243
 
244
- async def update_supplies(self):
244
+ async def update_supplies(self) -> None:
245
245
  """Update the list of supplies for the configured user."""
246
246
 
247
247
  self._supplies = await self.datadis.async_get_supplies(self._authorized_nif)
@@ -255,7 +255,7 @@ class DataService:
255
255
  supply = self._find_supply_for_cups(cups)
256
256
  if supply:
257
257
  self._contracts = await self.datadis.async_get_contract_detail(
258
- cups, supply.distributorCode, self._authorized_nif
258
+ cups, supply.distributor_code, self._authorized_nif
259
259
  )
260
260
  for c in self._contracts:
261
261
  await self.db.add_contract(self._cups, c)
@@ -263,7 +263,7 @@ class DataService:
263
263
  _LOGGER.warning("Unable to fetch contract details for %s", self._scups)
264
264
  return False
265
265
 
266
- async def update_energy(self, start: datetime, end: datetime):
266
+ async def update_energy(self, start: datetime, end: datetime) -> bool:
267
267
  """Update the list of energy consumptions for the selected cups."""
268
268
 
269
269
  cups = self._cups
@@ -271,11 +271,11 @@ class DataService:
271
271
  if supply:
272
272
  data = await self.datadis.async_get_consumption_data(
273
273
  cups,
274
- supply.distributorCode,
274
+ supply.distributor_code,
275
275
  start,
276
276
  end,
277
277
  self._measurement_type,
278
- supply.pointType,
278
+ supply.point_type,
279
279
  self._authorized_nif,
280
280
  )
281
281
  await self.db.add_energy_list(self._cups, data)
@@ -283,14 +283,14 @@ class DataService:
283
283
  _LOGGER.warning("Unable to fetch energy data for %s", self._scups)
284
284
  return False
285
285
 
286
- async def update_power(self, start: datetime, end: datetime):
286
+ async def update_power(self, start: datetime, end: datetime) -> bool:
287
287
  """Update the list of power peaks for the selected cups."""
288
288
  cups = self._cups
289
289
  supply = self._find_supply_for_cups(cups)
290
290
  if supply:
291
291
  data = await self.datadis.async_get_max_power(
292
292
  cups,
293
- supply.distributorCode,
293
+ supply.distributor_code,
294
294
  start,
295
295
  end,
296
296
  self._authorized_nif,
@@ -300,7 +300,7 @@ class DataService:
300
300
  _LOGGER.warning("Unable to fetch power data for %s", self._scups)
301
301
  return False
302
302
 
303
- async def update_pvpc(self, start: datetime, end: datetime):
303
+ async def update_pvpc(self, start: datetime, end: datetime) -> bool:
304
304
  """Update recent pvpc prices."""
305
305
 
306
306
  cups = self._cups
@@ -324,13 +324,13 @@ class DataService:
324
324
  _LOGGER.warning("%s unable to fetch pvpc prices", self._scups)
325
325
  return False
326
326
 
327
- async def update_statistics(self, start: datetime, end: datetime):
327
+ async def update_statistics(self, start: datetime, end: datetime) -> None:
328
328
  """Update the statistics during a period."""
329
329
 
330
330
  await self._update_daily_statistics(start, end)
331
331
  await self._update_monthly_statistics(start, end)
332
332
 
333
- async def _update_daily_statistics(self, start: datetime, end: datetime):
333
+ async def _update_daily_statistics(self, start: datetime, end: datetime) -> None:
334
334
  """Update daily statistics within a date range."""
335
335
 
336
336
  day_start = get_day(start)
@@ -358,7 +358,7 @@ class DataService:
358
358
  stat.datetime.date(),
359
359
  )
360
360
 
361
- async def _update_monthly_statistics(self, start: datetime, end: datetime):
361
+ async def _update_monthly_statistics(self, start: datetime, end: datetime) -> None:
362
362
  """Update monthly statistics within a date range."""
363
363
 
364
364
  month_start = get_month(start)
@@ -420,33 +420,34 @@ class DataService:
420
420
  agg_data[agg_dt] = Statistics(
421
421
  datetime=agg_dt,
422
422
  delta_h=0,
423
- value_kWh=0,
424
- value_p1_kWh=0,
425
- value_p2_kWh=0,
426
- value_p3_kWh=0,
427
- surplus_kWh=0,
428
- surplus_p1_kWh=0,
429
- surplus_p2_kWh=0,
430
- surplus_p3_kWh=0,
423
+ value_kwh=0,
424
+ consumption_by_tariff=[0.0, 0.0, 0.0],
425
+ surplus_kwh=0,
426
+ surplus_by_tariff=[0.0, 0.0, 0.0],
427
+ generation_kwh=0,
428
+ generation_by_tariff=[0.0, 0.0, 0.0],
429
+ selfconsumption_kwh=0,
430
+ selfconsumption_by_tariff=[0.0, 0.0, 0.0],
431
431
  )
432
432
 
433
433
  ref = agg_data[agg_dt]
434
434
  ref.delta_h += item.delta_h
435
- ref.value_kWh += item.value_kWh
436
- ref.surplus_kWh += item.surplus_kWh
437
- if 1 == tariff:
438
- ref.value_p1_kWh += item.value_kWh
439
- ref.surplus_p1_kWh += item.surplus_kWh
440
- elif 2 == tariff:
441
- ref.value_p2_kWh += item.value_kWh
442
- ref.surplus_p2_kWh += item.surplus_kWh
443
- elif 3 == tariff:
444
- ref.value_p3_kWh += item.value_kWh
445
- ref.surplus_p3_kWh += item.surplus_kWh
435
+ ref.consumption_kwh += item.consumption_kwh
436
+ ref.surplus_kwh += item.surplus_kwh
437
+ ref.generation_kwh += item.generation_kwh
438
+ ref.selfconsumption_kwh += item.selfconsumption_kwh
439
+ if 1 <= tariff <= 3:
440
+ idx = tariff - 1
441
+ ref.consumption_by_tariff[idx] += item.consumption_kwh
442
+ ref.surplus_by_tariff[idx] += item.surplus_kwh
443
+ ref.generation_by_tariff[idx] += item.generation_kwh
444
+ ref.selfconsumption_by_tariff[idx] += item.selfconsumption_kwh
446
445
 
447
446
  return [agg_data[x] for x in agg_data]
448
447
 
449
- async def _find_missing_stats(self, agg: typing.Literal["day", "month"] = "day"):
448
+ async def _find_missing_stats(
449
+ self, agg: typing.Literal["day", "month"] = "day"
450
+ ) -> list[datetime]:
450
451
  """Return the list of days that are missing energy data."""
451
452
 
452
453
  stats = await self.get_statistics(agg, complete=False)
@@ -473,7 +474,7 @@ class DataService:
473
474
  if last_record:
474
475
  return last_record.datetime
475
476
 
476
- async def _sync(self):
477
+ async def _sync(self) -> None:
477
478
  """Load state."""
478
479
 
479
480
  self._supplies = await self.get_supplies()
@@ -8,65 +8,73 @@ from edata.providers.datadis import DatadisConnector
8
8
  MOCK_USERNAME = "USERNAME"
9
9
  MOCK_PASSWORD = "PASSWORD"
10
10
 
11
- SUPPLIES_RESPONSE = [
12
- {
13
- "cups": "ESXXXXXXXXXXXXXXXXTEST",
14
- "validDateFrom": "2022/03/09",
15
- "validDateTo": "2022/10/28",
16
- "address": "-",
17
- "postalCode": "-",
18
- "province": "-",
19
- "municipality": "-",
20
- "distributor": "-",
21
- "pointType": 5,
22
- "distributorCode": "2",
23
- }
24
- ]
11
+ SUPPLIES_RESPONSE = {
12
+ "supplies": [
13
+ {
14
+ "cups": "ESXXXXXXXXXXXXXXXXTEST",
15
+ "validDateFrom": "2022/03/09",
16
+ "validDateTo": "2022/10/28",
17
+ "address": "-",
18
+ "postalCode": "-",
19
+ "province": "-",
20
+ "municipality": "-",
21
+ "distributor": "-",
22
+ "pointType": 5,
23
+ "distributorCode": "2",
24
+ }
25
+ ]
26
+ }
25
27
 
26
- CONTRACTS_RESPONSE = [
27
- {
28
- "startDate": "2022/03/09",
29
- "endDate": "2022/10/28",
30
- "marketer": "MARKETER",
31
- "distributorCode": "2",
32
- "contractedPowerkW": [4.4, 4.4],
33
- }
34
- ]
28
+ CONTRACTS_RESPONSE = {
29
+ "contract": [
30
+ {
31
+ "startDate": "2022/03/09",
32
+ "endDate": "2022/10/28",
33
+ "marketer": "MARKETER",
34
+ "distributorCode": "2",
35
+ "contractedPowerkW": [4.4, 4.4],
36
+ }
37
+ ]
38
+ }
35
39
 
36
- CONSUMPTIONS_RESPONSE = [
37
- {
38
- "date": "2022/10/22",
39
- "time": "01:00",
40
- "consumptionKWh": 0.203,
41
- "surplusEnergyKWh": 0,
42
- "obtainMethod": "Real",
43
- },
44
- {
45
- "date": "2022/10/22",
46
- "time": "02:00",
47
- "consumptionKWh": 0.163,
48
- "surplusEnergyKWh": 0,
49
- "obtainMethod": "Real",
50
- },
51
- ]
40
+ CONSUMPTIONS_RESPONSE = {
41
+ "timeCurve": [
42
+ {
43
+ "date": "2022/10/22",
44
+ "time": "01:00",
45
+ "consumptionKWh": 0.203,
46
+ "surplusEnergyKWh": 0,
47
+ "obtainMethod": "Real",
48
+ },
49
+ {
50
+ "date": "2022/10/22",
51
+ "time": "02:00",
52
+ "consumptionKWh": 0.163,
53
+ "surplusEnergyKWh": 0,
54
+ "obtainMethod": "Real",
55
+ },
56
+ ]
57
+ }
52
58
 
53
- MAXIMETER_RESPONSE = [
54
- {
55
- "date": "2022/03/10",
56
- "time": "14:15",
57
- "maxPower": 2.436,
58
- },
59
- {
60
- "date": "2022/03/14",
61
- "time": "13:15",
62
- "maxPower": 3.008,
63
- },
64
- {
65
- "date": "2022/03/27",
66
- "time": "10:30",
67
- "maxPower": 3.288,
68
- },
69
- ]
59
+ MAXIMETER_RESPONSE = {
60
+ "maxPower": [
61
+ {
62
+ "date": "2022/03/10",
63
+ "time": "14:15",
64
+ "maxPower": 2.436,
65
+ },
66
+ {
67
+ "date": "2022/03/14",
68
+ "time": "13:15",
69
+ "maxPower": 3.008,
70
+ },
71
+ {
72
+ "date": "2022/03/27",
73
+ "time": "10:30",
74
+ "maxPower": 3.288,
75
+ },
76
+ ]
77
+ }
70
78
 
71
79
 
72
80
  @patch("aiohttp.ClientSession.get")
@@ -157,7 +165,7 @@ def test_get_supplies_empty_response(mock_token, mock_get, snapshot):
157
165
  mock_response = MagicMock()
158
166
  mock_response.status = 200
159
167
  mock_response.text = AsyncMock(return_value="text")
160
- mock_response.json = AsyncMock(return_value=[])
168
+ mock_response.json = AsyncMock(return_value={"supplies": []})
161
169
  mock_get.return_value.__aenter__.return_value = mock_response
162
170
  connector = DatadisConnector(MOCK_USERNAME, MOCK_PASSWORD)
163
171
  assert connector.get_supplies() == snapshot
@@ -169,7 +177,7 @@ def test_get_supplies_empty_response(mock_token, mock_get, snapshot):
169
177
  )
170
178
  def test_get_supplies_malformed_response(mock_token, mock_get, snapshot):
171
179
  """Test get_supplies with malformed response (missing required fields, syrupy snapshot)."""
172
- malformed = [{"validDateFrom": "2022/03/09"}] # missing 'cups', etc.
180
+ malformed = {"supplies": [{"validDateFrom": "2022/03/09"}]} # missing 'cups', etc.
173
181
  mock_response = MagicMock()
174
182
  mock_response.status = 200
175
183
  mock_response.text = AsyncMock(return_value="text")
@@ -185,10 +193,10 @@ def test_get_supplies_malformed_response(mock_token, mock_get, snapshot):
185
193
  )
186
194
  def test_get_supplies_partial_response(mock_token, mock_get, snapshot):
187
195
  """Test get_supplies with partial valid/invalid response."""
188
- partial = [
189
- SUPPLIES_RESPONSE[0],
196
+ partial = {"supplies": [
197
+ SUPPLIES_RESPONSE["supplies"][0],
190
198
  {"validDateFrom": "2022/03/09"}, # invalid
191
- ]
199
+ ]}
192
200
  mock_response = MagicMock()
193
201
  mock_response.status = 200
194
202
  mock_response.text = AsyncMock(return_value="text")
@@ -244,20 +252,22 @@ def test_get_consumption_data_cache(mock_token, mock_get, snapshot):
244
252
  )
245
253
  def test_get_supplies_optional_fields_none(mock_token, mock_get, snapshot):
246
254
  """Test get_supplies with optional fields as None."""
247
- response = [
248
- {
249
- "cups": "ESXXXXXXXXXXXXXXXXTEST",
250
- "validDateFrom": "2022/03/09",
251
- "validDateTo": "2022/10/28",
252
- "address": None,
253
- "postalCode": None,
254
- "province": None,
255
- "municipality": None,
256
- "distributor": None,
257
- "pointType": 5,
258
- "distributorCode": "2",
259
- }
260
- ]
255
+ response = {
256
+ "supplies": [
257
+ {
258
+ "cups": "ESXXXXXXXXXXXXXXXXTEST",
259
+ "validDateFrom": "2022/03/09",
260
+ "validDateTo": "2022/10/28",
261
+ "address": None,
262
+ "postalCode": None,
263
+ "province": None,
264
+ "municipality": None,
265
+ "distributor": None,
266
+ "pointType": 5,
267
+ "distributorCode": "2",
268
+ }
269
+ ]
270
+ }
261
271
  mock_response = MagicMock()
262
272
  mock_response.status = 200
263
273
  mock_response.text = AsyncMock(return_value="text")
@@ -1,28 +0,0 @@
1
- e_data-2.0.0.dev112.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
2
- edata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- edata/cli.py,sha256=BuYQF9kSJsZM8UbsTP3x4b0Y733ZyEimWpoto14tbW8,3953
4
- edata/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
- edata/core/const.py,sha256=X9J2f7lt9jXJnghqz0JFi-J7USa3xlYvbuiki70eHNE,43
6
- edata/core/utils.py,sha256=_zUsS116gIDJIOIUIeLFqb8_9g5knGObic_SyXTWtpg,1919
7
- edata/database/__init__.py,sha256=HP_pDbGLzeMY9Ufm_Dps5cxzW_3YTonT8w8dstG558c,46
8
- edata/database/controller.py,sha256=uGv7tR72Vuxb-kg8M_Q3uGSsYYgU1UPM8JQPUts_pyU,16046
9
- edata/database/models.py,sha256=4rlRpPErULqYr6QJxZar1NBpJPWTcHftDN1MB3bwGyA,5096
10
- edata/database/queries.py,sha256=EHRslAMQCAS9swIwKl6PvfqHHt4tclfLskcGDSFi6PI,6590
11
- edata/database/utils.py,sha256=8-QP9h-iol8NEs_qFdNAoHeHa0zQBMwQlgzJhMmZ0VE,1070
12
- edata/models/__init__.py,sha256=nMPPPkGjUsF51PBFlChgHyb5C3aQiccTRiVHUdJjNj4,153
13
- edata/models/bill.py,sha256=iJ5ccoNQdo8MMyh5WjVe6ffm-wBfZAb2PSlIVnWxaR0,2746
14
- edata/models/data.py,sha256=zHUD453aUq6xkE-qSUfgVS26kQugH1LI90eatIFNrF8,893
15
- edata/models/supply.py,sha256=i4Gj7mN7xyGfFURbsPRmEmjGT6vntLDvgNNjsDTlE8U,610
16
- edata/providers/__init__.py,sha256=5xaRKPttYM6EpVE1I_vhUAF25L9pEM_qK_5Ix6xnQe0,104
17
- edata/providers/datadis.py,sha256=g47D54E-1y_ITLiwMIvz5yKrY0fp4v5GaBK7TH8sOA8,19786
18
- edata/providers/redata.py,sha256=i5qcbWRPfAzuMd2r4gtcHMmw8SXjN--k1UcnnaTqhYo,3109
19
- edata/services/bill_service.py,sha256=fYqaJOiAem68l5RiNmHCMz-28FKyyJ6hLHbeyqAm5ug,13258
20
- edata/services/data_service.py,sha256=51-K-XhAshnlkpNT-GWm8fg4QahOOxWYocORoj764K0,16490
21
- edata/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
- edata/tests/test_datadis_connector.py,sha256=OaECc9GfZ1lt-DNirPbphdYXISInyKCL9feBkC24U_8,8847
23
- edata/tests/test_redata_connector.py,sha256=SBvxsma5t-qvz2drxNisNM2P1STTurqEs1TSDgwaPJ8,508
24
- edata/tests/test_services.py,sha256=XiwppLWqxm5vjgRtNmBeDFUaV5vVcQxerGgjnBJgmmk,4521
25
- e_data-2.0.0.dev112.dist-info/METADATA,sha256=r2t05BWQ1jDxcZc2nf_sqe99GTuKNrFBPXIMz-iwLQk,48068
26
- e_data-2.0.0.dev112.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
27
- e_data-2.0.0.dev112.dist-info/top_level.txt,sha256=Ez-fReWtUVTMcFuH0dzCnoKIgMBHgjLaBuba8QiKC7g,6
28
- e_data-2.0.0.dev112.dist-info/RECORD,,