e-data 1.3.1__tar.gz → 1.3.2__tar.gz

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.
Files changed (31) hide show
  1. {e_data-1.3.1/edata/e_data.egg-info → e_data-1.3.2}/PKG-INFO +1 -1
  2. {e_data-1.3.1 → e_data-1.3.2/e_data.egg-info}/PKG-INFO +1 -1
  3. {e_data-1.3.1/edata → e_data-1.3.2}/e_data.egg-info/SOURCES.txt +10 -5
  4. e_data-1.3.2/e_data.egg-info/top_level.txt +2 -0
  5. e_data-1.3.2/edata/const.py +3 -0
  6. e_data-1.3.2/edata/definitions.py +329 -0
  7. e_data-1.3.2/edata/helpers.py +769 -0
  8. e_data-1.3.2/edata/storage.py +46 -0
  9. e_data-1.3.2/edata/tests/__init__.py +0 -0
  10. {e_data-1.3.1 → e_data-1.3.2}/pyproject.toml +2 -2
  11. e_data-1.3.1/edata/e_data.egg-info/top_level.txt +0 -3
  12. {e_data-1.3.1 → e_data-1.3.2}/LICENSE +0 -0
  13. {e_data-1.3.1 → e_data-1.3.2}/MANIFEST.in +0 -0
  14. {e_data-1.3.1 → e_data-1.3.2}/README.md +0 -0
  15. {e_data-1.3.1/edata → e_data-1.3.2}/e_data.egg-info/dependency_links.txt +0 -0
  16. {e_data-1.3.1/edata → e_data-1.3.2}/e_data.egg-info/requires.txt +0 -0
  17. {e_data-1.3.1/edata/connectors → e_data-1.3.2/edata}/__init__.py +0 -0
  18. {e_data-1.3.1/edata/processors → e_data-1.3.2/edata/connectors}/__init__.py +0 -0
  19. {e_data-1.3.1 → e_data-1.3.2}/edata/connectors/datadis.py +0 -0
  20. {e_data-1.3.1 → e_data-1.3.2}/edata/connectors/redata.py +0 -0
  21. {e_data-1.3.1/edata/tests → e_data-1.3.2/edata/processors}/__init__.py +0 -0
  22. {e_data-1.3.1 → e_data-1.3.2}/edata/processors/base.py +0 -0
  23. {e_data-1.3.1 → e_data-1.3.2}/edata/processors/billing.py +0 -0
  24. {e_data-1.3.1 → e_data-1.3.2}/edata/processors/consumption.py +0 -0
  25. {e_data-1.3.1 → e_data-1.3.2}/edata/processors/maximeter.py +0 -0
  26. {e_data-1.3.1 → e_data-1.3.2}/edata/processors/utils.py +0 -0
  27. {e_data-1.3.1 → e_data-1.3.2}/edata/tests/test_datadis_connector.py +0 -0
  28. {e_data-1.3.1 → e_data-1.3.2}/edata/tests/test_helpers.py +0 -0
  29. {e_data-1.3.1 → e_data-1.3.2}/edata/tests/test_processors.py +0 -0
  30. {e_data-1.3.1 → e_data-1.3.2}/edata/tests/test_redata_connector.py +0 -0
  31. {e_data-1.3.1 → e_data-1.3.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: e-data
3
- Version: 1.3.1
3
+ Version: 1.3.2
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: e-data
3
- Version: 1.3.1
3
+ Version: 1.3.2
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
@@ -2,14 +2,19 @@ LICENSE
2
2
  MANIFEST.in
3
3
  README.md
4
4
  pyproject.toml
5
+ e_data.egg-info/PKG-INFO
6
+ e_data.egg-info/SOURCES.txt
7
+ e_data.egg-info/dependency_links.txt
8
+ e_data.egg-info/requires.txt
9
+ e_data.egg-info/top_level.txt
10
+ edata/__init__.py
11
+ edata/const.py
12
+ edata/definitions.py
13
+ edata/helpers.py
14
+ edata/storage.py
5
15
  edata/connectors/__init__.py
6
16
  edata/connectors/datadis.py
7
17
  edata/connectors/redata.py
8
- edata/e_data.egg-info/PKG-INFO
9
- edata/e_data.egg-info/SOURCES.txt
10
- edata/e_data.egg-info/dependency_links.txt
11
- edata/e_data.egg-info/requires.txt
12
- edata/e_data.egg-info/top_level.txt
13
18
  edata/processors/__init__.py
14
19
  edata/processors/base.py
15
20
  edata/processors/billing.py
@@ -0,0 +1,2 @@
1
+ dist
2
+ edata
@@ -0,0 +1,3 @@
1
+ """Constants file."""
2
+
3
+ PROG_NAME = "edata"
@@ -0,0 +1,329 @@
1
+ """Definitions for data structures."""
2
+
3
+ import voluptuous as vol
4
+ import datetime as dt
5
+ from typing import TypedDict
6
+
7
+ ATTRIBUTES = {
8
+ "cups": None,
9
+ "contract_p1_kW": "kW",
10
+ "contract_p2_kW": "kW",
11
+ "yesterday_kWh": "kWh",
12
+ "yesterday_hours": "h",
13
+ "yesterday_p1_kWh": "kWh",
14
+ "yesterday_p2_kWh": "kWh",
15
+ "yesterday_p3_kWh": "kWh",
16
+ "yesterday_surplus_kWh": "kWh",
17
+ "yesterday_surplus_p1_kWh": "kWh",
18
+ "yesterday_surplus_p2_kWh": "kWh",
19
+ "yesterday_surplus_p3_kWh": "kWh",
20
+ "last_registered_date": None,
21
+ "last_registered_day_kWh": "kWh",
22
+ "last_registered_day_hours": "h",
23
+ "last_registered_day_p1_kWh": "kWh",
24
+ "last_registered_day_p2_kWh": "kWh",
25
+ "last_registered_day_p3_kWh": "kWh",
26
+ "last_registered_day_surplus_kWh": "kWh",
27
+ "last_registered_day_surplus_p1_kWh": "kWh",
28
+ "last_registered_day_surplus_p2_kWh": "kWh",
29
+ "last_registered_day_surplus_p3_kWh": "kWh",
30
+ "month_kWh": "kWh",
31
+ "month_daily_kWh": "kWh",
32
+ "month_days": "d",
33
+ "month_p1_kWh": "kWh",
34
+ "month_p2_kWh": "kWh",
35
+ "month_p3_kWh": "kWh",
36
+ "month_surplus_kWh": "kWh",
37
+ "month_surplus_p1_kWh": "kWh",
38
+ "month_surplus_p2_kWh": "kWh",
39
+ "month_surplus_p3_kWh": "kWh",
40
+ "month_€": "€",
41
+ "last_month_kWh": "kWh",
42
+ "last_month_daily_kWh": "kWh",
43
+ "last_month_days": "d",
44
+ "last_month_p1_kWh": "kWh",
45
+ "last_month_p2_kWh": "kWh",
46
+ "last_month_p3_kWh": "kWh",
47
+ "last_month_surplus_kWh": "kWh",
48
+ "last_month_surplus_p1_kWh": "kWh",
49
+ "last_month_surplus_p2_kWh": "kWh",
50
+ "last_month_surplus_p3_kWh": "kWh",
51
+ "last_month_€": "€",
52
+ "max_power_kW": "kW",
53
+ "max_power_date": None,
54
+ "max_power_mean_kW": "kW",
55
+ "max_power_90perc_kW": "kW",
56
+ }
57
+
58
+ # Energy term with taxes
59
+ DEFAULT_BILLING_ENERGY_FORMULA = "electricity_tax * iva_tax * kwh_eur * kwh"
60
+
61
+ # Power term with taxes
62
+ DEFAULT_BILLING_POWER_FORMULA = "electricity_tax * iva_tax * (p1_kw * (p1_kw_year_eur + market_kw_year_eur) + p2_kw * p2_kw_year_eur) / 365 / 24"
63
+
64
+ # Others term with taxes
65
+ DEFAULT_BILLING_OTHERS_FORMULA = "iva_tax * meter_month_eur / 30 / 24"
66
+
67
+ # Surplus term with taxes
68
+ DEFAULT_BILLING_SURPLUS_FORMULA = (
69
+ "electricity_tax * iva_tax * surplus_kwh * surplus_kwh_eur"
70
+ )
71
+
72
+ # Sum energy and power terms, and substract surplus until 0.
73
+ # An alternative would be "[(energy_term + power_term - surplus_term), 0]|max + others_term"
74
+ DEFAULT_BILLING_MAIN_FORMULA = "energy_term + power_term + others_term"
75
+
76
+
77
+ class SupplyData(TypedDict):
78
+ """Data structure to represent a supply."""
79
+
80
+ cups: str
81
+ date_start: dt.datetime
82
+ date_end: dt.datetime
83
+ address: str | None
84
+ postal_code: str | None
85
+ province: str | None
86
+ municipality: str | None
87
+ distributor: str | None
88
+ pointType: int
89
+ distributorCode: str
90
+
91
+
92
+ SupplySchema = vol.Schema(
93
+ {
94
+ vol.Required("cups"): str,
95
+ vol.Required("date_start"): dt.datetime,
96
+ vol.Required("date_end"): dt.datetime,
97
+ vol.Required("address"): vol.Union(str, None),
98
+ vol.Required("postal_code"): vol.Union(str, None),
99
+ vol.Required("province"): vol.Union(str, None),
100
+ vol.Required("municipality"): vol.Union(str, None),
101
+ vol.Required("distributor"): vol.Union(str, None),
102
+ vol.Required("pointType"): int,
103
+ vol.Required("distributorCode"): str,
104
+ }
105
+ )
106
+
107
+
108
+ class ContractData(TypedDict):
109
+ """Data structure to represent a contract."""
110
+
111
+ date_start: dt.datetime
112
+ date_end: dt.datetime
113
+ marketer: str
114
+ distributorCode: str
115
+ power_p1: float | None
116
+ power_p2: float | None
117
+
118
+
119
+ ContractSchema = vol.Schema(
120
+ {
121
+ vol.Required("date_start"): dt.datetime,
122
+ vol.Required("date_end"): dt.datetime,
123
+ vol.Required("marketer"): str,
124
+ vol.Required("distributorCode"): str,
125
+ vol.Required("power_p1"): vol.Union(vol.Coerce(float), None),
126
+ vol.Required("power_p2"): vol.Union(vol.Coerce(float), None),
127
+ }
128
+ )
129
+
130
+
131
+ class ConsumptionData(TypedDict):
132
+ """Data structure to represent a consumption."""
133
+
134
+ datetime: dt.datetime
135
+ delta_h: float
136
+ value_kWh: float
137
+ surplus_kWh: float
138
+ real: bool
139
+
140
+
141
+ ConsumptionSchema = vol.Schema(
142
+ {
143
+ vol.Required("datetime"): dt.datetime,
144
+ vol.Required("delta_h"): vol.Coerce(float),
145
+ vol.Required("value_kWh"): vol.Coerce(float),
146
+ vol.Optional("surplus_kWh", default=0): vol.Coerce(float),
147
+ vol.Required("real"): bool,
148
+ }
149
+ )
150
+
151
+
152
+ class MaxPowerData(TypedDict):
153
+ """Data structure to represent a MaxPower."""
154
+
155
+ datetime: dt.datetime
156
+ value_kW: float
157
+
158
+
159
+ MaxPowerSchema = vol.Schema(
160
+ {
161
+ vol.Required("datetime"): dt.datetime,
162
+ vol.Required("value_kW"): vol.Coerce(float),
163
+ }
164
+ )
165
+
166
+
167
+ class PricingData(TypedDict):
168
+ """Data structure to represent pricing data."""
169
+
170
+ datetime: dt.datetime
171
+ value_eur_kWh: float
172
+ delta_h: float
173
+
174
+
175
+ PricingSchema = vol.Schema(
176
+ {
177
+ vol.Required("datetime"): dt.datetime,
178
+ vol.Required("value_eur_kWh"): vol.Coerce(float),
179
+ vol.Required("delta_h"): vol.Coerce(float),
180
+ }
181
+ )
182
+
183
+
184
+ class PricingRules(TypedDict):
185
+ """Data structure to represent custom pricing rules."""
186
+
187
+ p1_kw_year_eur: float
188
+ p2_kw_year_eur: float
189
+ p1_kwh_eur: float | None
190
+ p2_kwh_eur: float | None
191
+ p3_kwh_eur: float | None
192
+ surplus_p1_kwh_eur: float | None
193
+ surplus_p2_kwh_eur: float | None
194
+ surplus_p3_kwh_eur: float | None
195
+ meter_month_eur: float
196
+ market_kw_year_eur: float
197
+ electricity_tax: float
198
+ iva_tax: float
199
+ energy_formula: str | None
200
+ power_formula: str | None
201
+ others_formula: str | None
202
+ surplus_formula: str | None
203
+ cycle_start_day: int | None
204
+
205
+
206
+ PricingRulesSchema = vol.Schema(
207
+ {
208
+ vol.Required("p1_kw_year_eur"): vol.Coerce(float),
209
+ vol.Required("p2_kw_year_eur"): vol.Coerce(float),
210
+ vol.Optional("p1_kwh_eur", default=None): vol.Union(vol.Coerce(float), None),
211
+ vol.Optional("p2_kwh_eur", default=None): vol.Union(vol.Coerce(float), None),
212
+ vol.Optional("p3_kwh_eur", default=None): vol.Union(vol.Coerce(float), None),
213
+ vol.Optional("surplus_p1_kwh_eur", default=None): vol.Union(
214
+ vol.Coerce(float), None
215
+ ),
216
+ vol.Optional("surplus_p2_kwh_eur", default=None): vol.Union(
217
+ vol.Coerce(float), None
218
+ ),
219
+ vol.Optional("surplus_p3_kwh_eur", default=None): vol.Union(
220
+ vol.Coerce(float), None
221
+ ),
222
+ vol.Required("meter_month_eur"): vol.Coerce(float),
223
+ vol.Required("market_kw_year_eur"): vol.Coerce(float),
224
+ vol.Required("electricity_tax"): vol.Coerce(float),
225
+ vol.Required("iva_tax"): vol.Coerce(float),
226
+ vol.Optional("energy_formula", default=DEFAULT_BILLING_ENERGY_FORMULA): str,
227
+ vol.Optional("power_formula", default=DEFAULT_BILLING_POWER_FORMULA): str,
228
+ vol.Optional("others_formula", default=DEFAULT_BILLING_OTHERS_FORMULA): str,
229
+ vol.Optional("surplus_formula", default=DEFAULT_BILLING_SURPLUS_FORMULA): str,
230
+ vol.Optional("main_formula", default=DEFAULT_BILLING_MAIN_FORMULA): str,
231
+ vol.Optional("cycle_start_day", default=1): vol.Range(1, 30),
232
+ }
233
+ )
234
+
235
+ DEFAULT_PVPC_RULES = PricingRules(
236
+ p1_kw_year_eur=30.67266,
237
+ p2_kw_year_eur=1.4243591,
238
+ meter_month_eur=0.81,
239
+ market_kw_year_eur=3.113,
240
+ electricity_tax=1.0511300560,
241
+ iva_tax=1.05,
242
+ )
243
+
244
+
245
+ class ConsumptionAggData(TypedDict):
246
+ """A dict holding a Consumption item."""
247
+
248
+ datetime: dt.datetime
249
+ value_kWh: float
250
+ value_p1_kWh: float
251
+ value_p2_kWh: float
252
+ value_p3_kWh: float
253
+ surplus_kWh: float
254
+ surplus_p1_kWh: float
255
+ surplus_p2_kWh: float
256
+ surplus_p3_kWh: float
257
+ delta_h: float
258
+
259
+
260
+ ConsumptionAggSchema = vol.Schema(
261
+ {
262
+ vol.Required("datetime"): dt.datetime,
263
+ vol.Required("value_kWh"): vol.Coerce(float),
264
+ vol.Required("value_p1_kWh"): vol.Coerce(float),
265
+ vol.Required("value_p2_kWh"): vol.Coerce(float),
266
+ vol.Required("value_p3_kWh"): vol.Coerce(float),
267
+ vol.Optional("surplus_kWh", default=0): vol.Coerce(float),
268
+ vol.Optional("surplus_p1_kWh", default=0): vol.Coerce(float),
269
+ vol.Optional("surplus_p2_kWh", default=0): vol.Coerce(float),
270
+ vol.Optional("surplus_p3_kWh", default=0): vol.Coerce(float),
271
+ vol.Required("delta_h"): vol.Coerce(float),
272
+ }
273
+ )
274
+
275
+
276
+ class PricingAggData(TypedDict):
277
+ """A dict holding a Billing item."""
278
+
279
+ datetime: dt.datetime
280
+ value_eur: float
281
+ energy_term: float
282
+ power_term: float
283
+ others_term: float
284
+ surplus_term: float
285
+ delta_h: float
286
+
287
+
288
+ PricingAggSchema = vol.Schema(
289
+ {
290
+ vol.Required("datetime"): dt.datetime,
291
+ vol.Required("value_eur"): vol.Coerce(float),
292
+ vol.Required("energy_term"): vol.Coerce(float),
293
+ vol.Required("power_term"): vol.Coerce(float),
294
+ vol.Required("others_term"): vol.Coerce(float),
295
+ vol.Optional("surplus_term", default=0): vol.Coerce(float),
296
+ vol.Optional("delta_h", default=1): vol.Coerce(float),
297
+ },
298
+ )
299
+
300
+
301
+ class EdataData(TypedDict):
302
+ """A Typed Dict to handle Edata Aggregated Data."""
303
+
304
+ supplies: list[SupplyData]
305
+ contracts: list[ContractData]
306
+ consumptions: list[ConsumptionData]
307
+ maximeter: list[MaxPowerData]
308
+ pvpc: list[PricingData]
309
+ consumptions_daily_sum: list[ConsumptionAggData]
310
+ consumptions_monthly_sum: list[ConsumptionAggData]
311
+ cost_hourly_sum: list[PricingAggData]
312
+ cost_daily_sum: list[PricingAggData]
313
+ cost_monthly_sum: list[PricingAggData]
314
+
315
+
316
+ EdataSchema = vol.Schema(
317
+ {
318
+ vol.Required("supplies"): [SupplySchema],
319
+ vol.Required("contracts"): [ContractSchema],
320
+ vol.Required("consumptions"): [ConsumptionSchema],
321
+ vol.Required("maximeter"): [MaxPowerSchema],
322
+ vol.Optional("pvpc", default=[]): [PricingSchema],
323
+ vol.Optional("consumptions_daily_sum", []): [ConsumptionAggSchema],
324
+ vol.Optional("consumptions_monthly_sum", []): [ConsumptionAggSchema],
325
+ vol.Optional("cost_hourly_sum", default=[]): [PricingAggSchema],
326
+ vol.Optional("cost_daily_sum", default=[]): [PricingAggSchema],
327
+ vol.Optional("cost_monthly_sum", default=[]): [PricingAggSchema],
328
+ }
329
+ )
@@ -0,0 +1,769 @@
1
+ """A module for edata helpers."""
2
+
3
+ import asyncio
4
+ import contextlib
5
+ from datetime import datetime, timedelta
6
+ import logging
7
+ import os
8
+
9
+ from dateutil.relativedelta import relativedelta
10
+ import requests
11
+
12
+ from . import const
13
+ from .connectors.datadis import DatadisConnector
14
+ from .connectors.redata import REDataConnector
15
+ from .definitions import ATTRIBUTES, EdataData, PricingRules
16
+ from .processors import utils
17
+ from .processors.billing import BillingInput, BillingProcessor
18
+ from .processors.consumption import ConsumptionProcessor
19
+ from .processors.maximeter import MaximeterProcessor
20
+ from .storage import check_storage_integrity, dump_storage, load_storage
21
+
22
+ _LOGGER = logging.getLogger(__name__)
23
+
24
+
25
+ def acups(cups):
26
+ """Print an abbreviated and anonymized CUPS."""
27
+
28
+ return cups[-5:]
29
+
30
+
31
+ class EdataHelper:
32
+ """Main EdataHelper class."""
33
+
34
+ UPDATE_INTERVAL = timedelta(hours=1)
35
+
36
+ def __init__(
37
+ self,
38
+ datadis_username: str,
39
+ datadis_password: str,
40
+ cups: str,
41
+ datadis_authorized_nif: str | None = None,
42
+ pricing_rules: PricingRules | None = None,
43
+ storage_dir_path: str | None = None,
44
+ data: EdataData | None = None,
45
+ ) -> None:
46
+ self.data = EdataData(
47
+ supplies=[],
48
+ contracts=[],
49
+ consumptions=[],
50
+ maximeter=[],
51
+ pvpc=[],
52
+ consumptions_daily_sum=[],
53
+ consumptions_monthly_sum=[],
54
+ cost_hourly_sum=[],
55
+ cost_daily_sum=[],
56
+ cost_monthly_sum=[],
57
+ )
58
+
59
+ self.attributes = {}
60
+ self._storage_dir = storage_dir_path
61
+ self._cups = cups
62
+ self._scups = acups(cups)
63
+ self._authorized_nif = datadis_authorized_nif
64
+ self.last_update = {x: datetime(1970, 1, 1) for x in self.data}
65
+ self._date_from = datetime(1970, 1, 1)
66
+ self._date_to = datetime.today()
67
+ self._must_dump = True
68
+ self._incremental_update = True
69
+
70
+ if data is not None:
71
+ data = check_storage_integrity(data)
72
+ self.data = data
73
+ else:
74
+ with contextlib.suppress(Exception):
75
+ self.data = load_storage(self._cups, self._storage_dir)
76
+
77
+ for attr in ATTRIBUTES:
78
+ self.attributes[attr] = None
79
+
80
+ self.datadis_api = DatadisConnector(
81
+ datadis_username,
82
+ datadis_password,
83
+ storage_path=(
84
+ os.path.join(storage_dir_path, const.PROG_NAME)
85
+ if storage_dir_path is not None
86
+ else None
87
+ ),
88
+ )
89
+ self.redata_api = REDataConnector()
90
+
91
+ self.pricing_rules = pricing_rules
92
+
93
+ if self.pricing_rules is not None:
94
+ self.enable_billing = True
95
+ if not all(
96
+ x in self.pricing_rules and self.pricing_rules[x] is not None
97
+ for x in ("p1_kwh_eur", "p2_kwh_eur", "p3_kwh_eur")
98
+ ):
99
+ self.is_pvpc = True
100
+ else:
101
+ self.is_pvpc = False
102
+ else:
103
+ self.enable_billing = False
104
+ self.is_pvpc = False
105
+
106
+ async def async_update(
107
+ self,
108
+ date_from: datetime = datetime(1970, 1, 1),
109
+ date_to: datetime = datetime.today(),
110
+ incremental_update: bool = True,
111
+ ):
112
+ """Async call of update method."""
113
+
114
+ _LOGGER.info(
115
+ "%s: update triggered",
116
+ self._scups,
117
+ )
118
+ self._date_from = date_from
119
+ self._date_to = date_to
120
+
121
+ # update datadis resources
122
+ await self.update_datadis(self._cups, date_from, date_to)
123
+
124
+ # update redata resources if pvpc is requested
125
+ if self.is_pvpc:
126
+ try:
127
+ await asyncio.to_thread(self.update_redata, date_from, date_to)
128
+ except requests.exceptions.Timeout:
129
+ _LOGGER.error("Timeout exception while updating from REData")
130
+
131
+ await asyncio.to_thread(self.process_data, incremental_update=incremental_update)
132
+
133
+ if self._must_dump:
134
+ await asyncio.to_thread(dump_storage, self._cups, self.data, self._storage_dir)
135
+
136
+ def update(
137
+ self,
138
+ date_from: datetime = datetime(1970, 1, 1),
139
+ date_to: datetime = datetime.today(),
140
+ incremental_update: bool = True,
141
+ ):
142
+ """Update synchronously."""
143
+
144
+ asyncio.run(self.async_update(date_from, date_to, incremental_update))
145
+
146
+ async def update_supplies(self):
147
+ """Update supplies."""
148
+
149
+ _LOGGER.debug("%s: supplies update triggered", self._scups)
150
+ if datetime.today().date() != self.last_update["supplies"].date():
151
+ # if supplies haven't been updated today
152
+ supplies = await self.datadis_api.async_get_supplies(
153
+ authorized_nif=self._authorized_nif
154
+ ) # fetch supplies
155
+ if len(supplies) > 0:
156
+ self.data["supplies"] = supplies
157
+ # if we got something, update last_update flag
158
+ self.last_update["supplies"] = datetime.now()
159
+ _LOGGER.info("%s: supplies update succeeded", self._scups)
160
+ else:
161
+ _LOGGER.info("%s: supplies are already updated (skipping)", self._scups)
162
+
163
+ async def update_contracts(self, cups: str, distributor_code: str):
164
+ """Update contracts."""
165
+
166
+ _LOGGER.debug("%s: contracts update triggered", self._scups)
167
+ if datetime.today().date() != self.last_update["contracts"].date():
168
+ # if contracts haven't been updated today
169
+ contracts = await self.datadis_api.async_get_contract_detail(
170
+ cups, distributor_code, authorized_nif=self._authorized_nif
171
+ )
172
+ if len(contracts) > 0:
173
+ self.data["contracts"] = utils.extend_by_key(
174
+ self.data["contracts"], contracts, "date_start"
175
+ ) # extend contracts data with new ones
176
+ # if we got something, update last_update flag
177
+ self.last_update["contracts"] = datetime.now()
178
+ _LOGGER.info("%s: contracts update succeeded", self._scups)
179
+ else:
180
+ _LOGGER.info("%s: contracts are already updated (skipping)", self._scups)
181
+
182
+ async def update_consumptions(
183
+ self,
184
+ cups: str,
185
+ distributor_code: str,
186
+ start_date: datetime,
187
+ end_date: datetime,
188
+ measurement_type: str,
189
+ point_type: int,
190
+ ):
191
+ """Update consumptions."""
192
+
193
+ _LOGGER.debug("%s: consumptions update triggered", self._scups)
194
+ if (datetime.now() - self.last_update["consumptions"]) > self.UPDATE_INTERVAL:
195
+ consumptions = await self.datadis_api.async_get_consumption_data(
196
+ cups,
197
+ distributor_code,
198
+ start_date,
199
+ end_date,
200
+ measurement_type,
201
+ point_type,
202
+ authorized_nif=self._authorized_nif,
203
+ )
204
+ if len(consumptions) > 0:
205
+ _LOGGER.info(
206
+ "%s: got consumptions from %s to %s",
207
+ self._scups,
208
+ consumptions[0]["datetime"].isoformat(),
209
+ consumptions[-1]["datetime"].isoformat(),
210
+ )
211
+ self.data["consumptions"] = utils.extend_by_key(
212
+ self.data["consumptions"], consumptions, "datetime"
213
+ )
214
+ self.last_update["consumptions"] = datetime.now()
215
+ else:
216
+ _LOGGER.info("%s: consumptions are up to date", self._scups)
217
+ else:
218
+ _LOGGER.info("%s: consumptions are already updated (skipping)", self._scups)
219
+
220
+ async def update_maximeter(self, cups, distributor_code, start_date, end_date):
221
+ """Update maximeter."""
222
+
223
+ _LOGGER.debug("%s: maximeter update triggered", self._scups)
224
+ if (datetime.now() - self.last_update["maximeter"]) > self.UPDATE_INTERVAL:
225
+ maximeter = await self.datadis_api.async_get_max_power(
226
+ cups,
227
+ distributor_code,
228
+ start_date,
229
+ end_date,
230
+ authorized_nif=self._authorized_nif,
231
+ )
232
+ if len(maximeter) > 0:
233
+ _LOGGER.info(
234
+ "%s: maximeter update succeeded",
235
+ self._scups,
236
+ )
237
+ self.data["maximeter"] = utils.extend_by_key(
238
+ self.data["maximeter"], maximeter, "datetime"
239
+ )
240
+ self.last_update["maximeter"] = datetime.now()
241
+ else:
242
+ _LOGGER.info("%s: maximeter is up to date", self._scups)
243
+ else:
244
+ _LOGGER.info("%s: maximeter is already updated (skipping)", self._scups)
245
+
246
+ async def update_datadis(
247
+ self,
248
+ cups: str,
249
+ date_from: datetime = datetime(1970, 1, 1),
250
+ date_to: datetime = datetime.today(),
251
+ ):
252
+ """Update all data from Datadis."""
253
+
254
+ _LOGGER.info(
255
+ "%s: datadis update triggered (from %s to %s)",
256
+ self._scups,
257
+ date_from.isoformat(),
258
+ date_to.isoformat(),
259
+ )
260
+
261
+ # update supplies and get distributorCode
262
+ await self.update_supplies()
263
+
264
+ if len(self.data["supplies"]) == 0:
265
+ # return if no supplies were discovered
266
+ _LOGGER.warning(
267
+ "%s: supplies update failed or no supplies found in the provided account",
268
+ self._scups,
269
+ )
270
+ return False
271
+
272
+ # find requested cups in supplies
273
+ supply = utils.get_by_key(self.data["supplies"], "cups", cups)
274
+ if supply is None:
275
+ # return if specified cups seems not valid
276
+ _LOGGER.error(
277
+ "%s: CUPS not found. Got: %s",
278
+ self._scups,
279
+ [acups(x["cups"]) for x in self.data["supplies"]],
280
+ )
281
+ return False
282
+ _LOGGER.info("%s: CUPS found in account", self._scups)
283
+
284
+ # get some supply-related data
285
+ supply_date_start = supply["date_start"]
286
+ distributor_code = supply["distributorCode"]
287
+ point_type = supply["pointType"]
288
+
289
+ _LOGGER.info(
290
+ "%s: CUPS start date is %s", self._scups, supply_date_start.isoformat()
291
+ )
292
+ _LOGGER.info(
293
+ "%s: CUPS end date is %s", self._scups, supply["date_end"].isoformat()
294
+ )
295
+
296
+ # update contracts to get valid periods
297
+ await self.update_contracts(cups, distributor_code)
298
+ if len(self.data["contracts"]) == 0:
299
+ _LOGGER.warning(
300
+ "%s: contracts update failed or no contracts found in the provided account",
301
+ self._scups,
302
+ )
303
+ # return False
304
+
305
+ # filter consumptions and maximeter, and log gaps
306
+ def sort_and_filter(dt_from, dt_to):
307
+ self.data["consumptions"], miss_cons = utils.extract_dt_ranges(
308
+ self.data["consumptions"],
309
+ dt_from,
310
+ dt_to,
311
+ gap_interval=timedelta(hours=6),
312
+ )
313
+ self.data["maximeter"], miss_maxim = utils.extract_dt_ranges(
314
+ self.data["maximeter"],
315
+ dt_from,
316
+ dt_to,
317
+ gap_interval=timedelta(days=60),
318
+ )
319
+ return miss_cons, miss_maxim
320
+
321
+ miss_cons, miss_maxim = sort_and_filter(date_from, date_to)
322
+
323
+ # update consumptions
324
+ _LOGGER.info(
325
+ "%s: missing consumptions: %s",
326
+ self._scups,
327
+ ", ".join(
328
+ [
329
+ "from "
330
+ + (x["from"] + timedelta(hours=1)).isoformat()
331
+ + " to "
332
+ + x["to"].isoformat()
333
+ for x in miss_cons
334
+ ]
335
+ ),
336
+ )
337
+ for gap in miss_cons:
338
+ if not (
339
+ gap["to"] < supply["date_start"] or gap["from"] > supply["date_end"]
340
+ ):
341
+ # fetch consumptions for each consumptions gap in valid periods
342
+ start = max([gap["from"] + timedelta(hours=1), supply["date_start"]])
343
+ end = min([gap["to"], supply["date_end"]])
344
+ _LOGGER.info(
345
+ "%s: requesting consumptions from %s to %s",
346
+ self._scups,
347
+ start.isoformat(),
348
+ end.isoformat(),
349
+ )
350
+ await self.update_consumptions(
351
+ cups,
352
+ distributor_code,
353
+ start,
354
+ end,
355
+ "0",
356
+ point_type,
357
+ )
358
+
359
+ # update maximeter
360
+ _LOGGER.info(
361
+ "%s: missing maximeter: %s",
362
+ self._scups,
363
+ ", ".join(
364
+ [
365
+ "from " + x["from"].isoformat() + " to " + x["to"].isoformat()
366
+ for x in miss_maxim
367
+ ]
368
+ ),
369
+ )
370
+ for gap in miss_maxim:
371
+ if not (date_to < supply["date_start"] or date_from > supply["date_end"]):
372
+ # fetch maximeter for each maximeter gap in valid periods
373
+ start = max(
374
+ [gap["from"], supply["date_start"] + relativedelta(months=1)]
375
+ )
376
+ end = min([gap["to"], supply["date_end"]])
377
+ start = min([start, end])
378
+ _LOGGER.info(
379
+ "%s: requesting maximeter from %s to %s",
380
+ self._scups,
381
+ start.isoformat(),
382
+ end.isoformat(),
383
+ )
384
+ await self.update_maximeter(cups, distributor_code, start, end)
385
+
386
+ miss_cons, miss_maxim = sort_and_filter(date_from, date_to)
387
+
388
+ return True
389
+
390
+ def update_redata(
391
+ self,
392
+ date_from: datetime = (datetime.today() - timedelta(days=30)).replace(
393
+ hour=0, minute=0
394
+ ),
395
+ date_to: datetime = (datetime.today() + timedelta(days=2)).replace(
396
+ hour=0, minute=0
397
+ ),
398
+ ):
399
+ """Fetch PVPC prices using REData API."""
400
+
401
+ _LOGGER.info(
402
+ "%s: updating PVPC prices",
403
+ self._scups,
404
+ )
405
+
406
+ self.data["pvpc"], missing = utils.extract_dt_ranges(
407
+ self.data["pvpc"],
408
+ date_from,
409
+ date_to,
410
+ gap_interval=timedelta(hours=1),
411
+ )
412
+ for gap in missing:
413
+ prices = []
414
+ gap["from"] = max(
415
+ (datetime.today() - timedelta(days=30)).replace(hour=0, minute=0),
416
+ gap["from"],
417
+ )
418
+ while len(prices) == 0 and gap["from"] < gap["to"]:
419
+ prices = self.redata_api.get_realtime_prices(gap["from"], gap["to"])
420
+ gap["from"] = gap["from"] + timedelta(days=1)
421
+ self.data["pvpc"] = utils.extend_by_key(
422
+ self.data["pvpc"], prices, "datetime"
423
+ )
424
+
425
+ return True
426
+
427
+ def process_data(self, incremental_update: bool = True):
428
+ """Process all raw data."""
429
+
430
+ self._incremental_update = incremental_update
431
+
432
+ for process_method in [
433
+ self.process_supplies,
434
+ self.process_contracts,
435
+ self.process_consumptions,
436
+ self.process_maximeter,
437
+ self.process_cost,
438
+ ]:
439
+ try:
440
+ process_method()
441
+ except Exception as ex: # pylint: disable=broad-except
442
+ _LOGGER.error("Unhandled exception while updating attributes")
443
+ _LOGGER.exception(ex)
444
+
445
+ for attribute in self.attributes:
446
+ if attribute in ATTRIBUTES and ATTRIBUTES[attribute] is not None:
447
+ self.attributes[attribute] = (
448
+ round(self.attributes[attribute], 2)
449
+ if self.attributes[attribute] is not None
450
+ else None
451
+ )
452
+
453
+ if not incremental_update:
454
+ dump_storage(self._cups, self.data, self._storage_dir)
455
+
456
+ def process_supplies(self):
457
+ """Process supplies data."""
458
+ for i in self.data["supplies"]:
459
+ if i["cups"] == self._cups:
460
+ self.attributes["cups"] = self._cups
461
+ break
462
+
463
+ def process_contracts(self):
464
+ """Process contracts data."""
465
+ most_recent_date = datetime(1970, 1, 1)
466
+ for i in self.data["contracts"]:
467
+ if i["date_end"] > most_recent_date:
468
+ most_recent_date = i["date_end"]
469
+ self.attributes["contract_p1_kW"] = i.get("power_p1", None)
470
+ self.attributes["contract_p2_kW"] = i.get("power_p2", None)
471
+ break
472
+
473
+ def process_consumptions(self):
474
+ """Process consumptions data."""
475
+ if len(self.data["consumptions"]) > 0:
476
+ new_data_from = self._date_from
477
+ if self._incremental_update:
478
+ with contextlib.suppress(Exception):
479
+ new_data_from = self.data["consumptions_monthly_sum"][-1][
480
+ "datetime"
481
+ ]
482
+
483
+ proc = ConsumptionProcessor(
484
+ {
485
+ "consumptions": [
486
+ x
487
+ for x in self.data["consumptions"]
488
+ if x["datetime"] >= new_data_from
489
+ ],
490
+ "cycle_start_day": 1,
491
+ }
492
+ )
493
+ today_starts = datetime(
494
+ datetime.today().year,
495
+ datetime.today().month,
496
+ datetime.today().day,
497
+ 0,
498
+ 0,
499
+ 0,
500
+ )
501
+
502
+ month_starts = datetime(
503
+ datetime.today().year, datetime.today().month, 1, 0, 0, 0
504
+ )
505
+
506
+ # append new data
507
+ self.data["consumptions_daily_sum"] = utils.extend_and_filter(
508
+ self.data["consumptions_daily_sum"],
509
+ proc.output["daily"],
510
+ "datetime",
511
+ self._date_from,
512
+ self._date_to,
513
+ )
514
+ self.data["consumptions_monthly_sum"] = utils.extend_and_filter(
515
+ self.data["consumptions_monthly_sum"],
516
+ proc.output["monthly"],
517
+ "datetime",
518
+ self._date_from,
519
+ self._date_to,
520
+ )
521
+
522
+ yday = utils.get_by_key(
523
+ self.data["consumptions_daily_sum"],
524
+ "datetime",
525
+ today_starts - timedelta(days=1),
526
+ )
527
+ self.attributes["yesterday_kWh"] = (
528
+ yday.get("value_kWh", None) if yday is not None else None
529
+ )
530
+
531
+ for tariff in (1, 2, 3):
532
+ self.attributes[f"yesterday_p{tariff}_kWh"] = (
533
+ yday.get(f"value_p{tariff}_kWh", None) if yday is not None else None
534
+ )
535
+
536
+ self.attributes["yesterday_surplus_kWh"] = (
537
+ yday.get("surplus_kWh", None) if yday is not None else None
538
+ )
539
+
540
+ for tariff in (1, 2, 3):
541
+ self.attributes[f"yesterday_surplus_p{tariff}_kWh"] = (
542
+ yday.get(f"surplus_p{tariff}_kWh", None)
543
+ if yday is not None
544
+ else None
545
+ )
546
+
547
+ self.attributes["yesterday_hours"] = (
548
+ yday.get("delta_h", None) if yday is not None else None
549
+ )
550
+
551
+ month = utils.get_by_key(
552
+ self.data["consumptions_monthly_sum"], "datetime", month_starts
553
+ )
554
+ self.attributes["month_kWh"] = (
555
+ month.get("value_kWh", None) if month is not None else None
556
+ )
557
+ self.attributes["month_surplus_kWh"] = (
558
+ month.get("surplus_kWh", None) if month is not None else None
559
+ )
560
+ self.attributes["month_days"] = (
561
+ month.get("delta_h", 0) / 24 if month is not None else None
562
+ )
563
+ self.attributes["month_daily_kWh"] = (
564
+ (
565
+ (self.attributes["month_kWh"] / self.attributes["month_days"])
566
+ if self.attributes["month_days"] > 0
567
+ else 0
568
+ )
569
+ if month is not None
570
+ else None
571
+ )
572
+
573
+ for tariff in (1, 2, 3):
574
+ self.attributes[f"month_p{tariff}_kWh"] = (
575
+ month.get(f"value_p{tariff}_kWh", None)
576
+ if month is not None
577
+ else None
578
+ )
579
+ self.attributes[f"month_surplus_p{tariff}_kWh"] = (
580
+ month.get(f"surplus_p{tariff}_kWh", None)
581
+ if month is not None
582
+ else None
583
+ )
584
+
585
+ last_month = utils.get_by_key(
586
+ self.data["consumptions_monthly_sum"],
587
+ "datetime",
588
+ (month_starts - relativedelta(months=1)),
589
+ )
590
+ self.attributes["last_month_kWh"] = (
591
+ last_month.get("value_kWh", None) if last_month is not None else None
592
+ )
593
+ self.attributes["last_month_surplus_kWh"] = (
594
+ last_month.get("surplus_kWh", None) if last_month is not None else None
595
+ )
596
+ self.attributes["last_month_days"] = (
597
+ last_month.get("delta_h", 0) / 24 if last_month is not None else None
598
+ )
599
+ self.attributes["last_month_daily_kWh"] = (
600
+ (
601
+ (
602
+ self.attributes["last_month_kWh"]
603
+ / self.attributes["last_month_days"]
604
+ )
605
+ if self.attributes["last_month_days"] > 0
606
+ else 0
607
+ )
608
+ if last_month is not None
609
+ else None
610
+ )
611
+ for tariff in (1, 2, 3):
612
+ self.attributes[f"last_month_p{tariff}_kWh"] = (
613
+ last_month.get(f"value_p{tariff}_kWh", None)
614
+ if last_month is not None
615
+ else None
616
+ )
617
+ self.attributes[f"last_month_surplus_p{tariff}_kWh"] = (
618
+ last_month.get(f"surplus_p{tariff}_kWh", None)
619
+ if last_month is not None
620
+ else None
621
+ )
622
+
623
+ if len(self.data["consumptions"]) > 0:
624
+ self.attributes["last_registered_date"] = self.data["consumptions"][-1][
625
+ "datetime"
626
+ ]
627
+
628
+ if len(self.data["consumptions_daily_sum"]) > 0:
629
+ last_day = self.data["consumptions_daily_sum"][-1]
630
+ self.attributes["last_registered_day_kWh"] = (
631
+ last_day.get("value_kWh", None)
632
+ if last_day is not None
633
+ else None
634
+ )
635
+ self.attributes["last_registered_day_surplus_kWh"] = (
636
+ last_day.get("surplus_kWh", None)
637
+ if last_day is not None
638
+ else None
639
+ )
640
+
641
+ for tariff in (1, 2, 3):
642
+ self.attributes[f"last_registered_day_p{tariff}_kWh"] = (
643
+ last_day.get(f"value_p{tariff}_kWh", None)
644
+ if last_day is not None
645
+ else None
646
+ )
647
+
648
+ self.attributes[
649
+ f"last_registered_day_surplus_p{tariff}_kWh"
650
+ ] = (
651
+ last_day.get(f"surplus_p{tariff}_kWh", None)
652
+ if last_day is not None
653
+ else None
654
+ )
655
+
656
+ self.attributes["last_registered_day_hours"] = (
657
+ last_day.get("delta_h", None) if last_day is not None else None
658
+ )
659
+
660
+ def process_maximeter(self):
661
+ """Process maximeter data."""
662
+ if len(self.data["maximeter"]) > 0:
663
+ processor = MaximeterProcessor(self.data["maximeter"])
664
+ last_relative_year = processor.output["stats"]
665
+ self.attributes["max_power_kW"] = last_relative_year.get(
666
+ "value_max_kW", None
667
+ )
668
+ self.attributes["max_power_date"] = last_relative_year.get("date_max", None)
669
+ self.attributes["max_power_mean_kW"] = last_relative_year.get(
670
+ "value_mean_kW", None
671
+ )
672
+ self.attributes["max_power_90perc_kW"] = last_relative_year.get(
673
+ "value_tile90_kW", None
674
+ )
675
+
676
+ def process_cost(self):
677
+ """Process costs."""
678
+ if self.enable_billing:
679
+ new_data_from = self._date_from
680
+ if self._incremental_update:
681
+ with contextlib.suppress(Exception):
682
+ new_data_from = self.data["cost_monthly_sum"][-1]["datetime"]
683
+
684
+ proc = BillingProcessor(
685
+ BillingInput(
686
+ contracts=self.data["contracts"],
687
+ consumptions=[
688
+ x
689
+ for x in self.data["consumptions"]
690
+ if x["datetime"] >= new_data_from
691
+ ],
692
+ prices=(
693
+ [x for x in self.data["pvpc"] if x["datetime"] >= new_data_from]
694
+ if self.is_pvpc
695
+ else None
696
+ ),
697
+ rules=self.pricing_rules,
698
+ )
699
+ )
700
+ month_starts = datetime(
701
+ datetime.today().year, datetime.today().month, 1, 0, 0, 0
702
+ )
703
+
704
+ # append new data
705
+ hourly = proc.output["hourly"]
706
+ self.data["cost_hourly_sum"] = utils.extend_and_filter(
707
+ self.data["cost_hourly_sum"],
708
+ hourly,
709
+ "datetime",
710
+ self._date_from,
711
+ self._date_to,
712
+ )
713
+
714
+ daily = proc.output["daily"]
715
+ self.data["cost_daily_sum"] = utils.extend_and_filter(
716
+ self.data["cost_daily_sum"],
717
+ daily,
718
+ "datetime",
719
+ self._date_from,
720
+ self._date_to,
721
+ )
722
+
723
+ monthly = proc.output["monthly"]
724
+ self.data["cost_monthly_sum"] = utils.extend_and_filter(
725
+ self.data["cost_monthly_sum"],
726
+ monthly,
727
+ "datetime",
728
+ self._date_from,
729
+ self._date_to,
730
+ )
731
+
732
+ this_month = utils.get_by_key(
733
+ self.data["cost_monthly_sum"],
734
+ "datetime",
735
+ month_starts,
736
+ )
737
+
738
+ last_month = utils.get_by_key(
739
+ self.data["cost_monthly_sum"],
740
+ "datetime",
741
+ (month_starts - relativedelta(months=1)),
742
+ )
743
+
744
+ if this_month is not None:
745
+ self.attributes["month_€"] = this_month.get("value_eur", None)
746
+
747
+ if last_month is not None:
748
+ self.attributes["last_month_€"] = last_month.get("value_eur", None)
749
+
750
+ def reset(self):
751
+ """Reset in-mem objects."""
752
+
753
+ self.data = EdataData(
754
+ supplies=[],
755
+ contracts=[],
756
+ consumptions=[],
757
+ maximeter=[],
758
+ pvpc=[],
759
+ consumptions_daily_sum=[],
760
+ consumptions_monthly_sum=[],
761
+ cost_hourly_sum=[],
762
+ cost_daily_sum=[],
763
+ cost_monthly_sum=[],
764
+ )
765
+
766
+ for attr in ATTRIBUTES:
767
+ self.attributes[attr] = None
768
+
769
+ self.last_update = {x: datetime(1970, 1, 1) for x in self.data}
@@ -0,0 +1,46 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ from .processors import utils
5
+ from . import const as const
6
+ from . import definitions as defs
7
+
8
+ _LOGGER = logging.getLogger(__name__)
9
+
10
+ DEFAULT_STORAGE_DIR = os.getenv("HOME")
11
+ RECENT_CACHE_FILENAME = "edata_{id}.json"
12
+
13
+ compile_storage_id = lambda cups: cups.lower()
14
+
15
+
16
+ def check_storage_integrity(data: defs.EdataData):
17
+ """Check if an EdataData object follows a schema."""
18
+ return defs.EdataSchema(data)
19
+
20
+
21
+ def load_storage(cups: str, storage_dir: str | None = None):
22
+ """Load EdataData storage from its config dir."""
23
+ if storage_dir is None:
24
+ storage_dir = DEFAULT_STORAGE_DIR
25
+ _subdir = os.path.join(storage_dir, const.PROG_NAME)
26
+ _recent_cache = os.path.join(
27
+ _subdir, RECENT_CACHE_FILENAME.format(id=compile_storage_id(cups))
28
+ )
29
+ os.makedirs(_subdir, exist_ok=True)
30
+
31
+ with open(_recent_cache, encoding="utf-8") as f:
32
+ return check_storage_integrity(utils.deserialize_dict(json.load(f)))
33
+
34
+
35
+ def dump_storage(cups: str, storage: defs.EdataData, storage_dir: str | None = None):
36
+ """Update EdataData storage."""
37
+ if storage_dir is None:
38
+ storage_dir = DEFAULT_STORAGE_DIR
39
+ _subdir = os.path.join(storage_dir, const.PROG_NAME)
40
+ _recent_cache = os.path.join(
41
+ _subdir, RECENT_CACHE_FILENAME.format(id=compile_storage_id(cups))
42
+ )
43
+ os.makedirs(_subdir, exist_ok=True)
44
+
45
+ with open(_recent_cache, "w", encoding="utf-8") as f:
46
+ json.dump(utils.serialize_dict(check_storage_integrity(storage)), f)
File without changes
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "e-data"
7
- version = "1.3.1"
7
+ version = "1.3.2"
8
8
  description = "Python library for managing spanish energy data from various web providers"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -37,7 +37,7 @@ dependencies = [
37
37
  Homepage = "https://github.com/uvejota/python-edata"
38
38
 
39
39
  [tool.setuptools.packages.find]
40
- where = ["edata"]
40
+ where = ["."]
41
41
 
42
42
  [tool.setuptools]
43
43
  include-package-data = true
@@ -1,3 +0,0 @@
1
- connectors
2
- processors
3
- tests
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes