e-data 1.3.1__tar.gz → 1.3.3__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.
- {e_data-1.3.1/edata/e_data.egg-info → e_data-1.3.3}/PKG-INFO +1 -1
- {e_data-1.3.1 → e_data-1.3.3/e_data.egg-info}/PKG-INFO +1 -1
- {e_data-1.3.1/edata → e_data-1.3.3}/e_data.egg-info/SOURCES.txt +11 -5
- e_data-1.3.3/e_data.egg-info/top_level.txt +2 -0
- e_data-1.3.3/edata/cli.py +24 -0
- {e_data-1.3.1 → e_data-1.3.3}/edata/connectors/datadis.py +4 -4
- e_data-1.3.3/edata/const.py +3 -0
- e_data-1.3.3/edata/definitions.py +329 -0
- e_data-1.3.3/edata/helpers.py +769 -0
- e_data-1.3.3/edata/storage.py +46 -0
- e_data-1.3.3/edata/tests/__init__.py +0 -0
- {e_data-1.3.1 → e_data-1.3.3}/pyproject.toml +2 -2
- e_data-1.3.1/edata/e_data.egg-info/top_level.txt +0 -3
- {e_data-1.3.1 → e_data-1.3.3}/LICENSE +0 -0
- {e_data-1.3.1 → e_data-1.3.3}/MANIFEST.in +0 -0
- {e_data-1.3.1 → e_data-1.3.3}/README.md +0 -0
- {e_data-1.3.1/edata → e_data-1.3.3}/e_data.egg-info/dependency_links.txt +0 -0
- {e_data-1.3.1/edata → e_data-1.3.3}/e_data.egg-info/requires.txt +0 -0
- {e_data-1.3.1/edata/connectors → e_data-1.3.3/edata}/__init__.py +0 -0
- {e_data-1.3.1/edata/processors → e_data-1.3.3/edata/connectors}/__init__.py +0 -0
- {e_data-1.3.1 → e_data-1.3.3}/edata/connectors/redata.py +0 -0
- {e_data-1.3.1/edata/tests → e_data-1.3.3/edata/processors}/__init__.py +0 -0
- {e_data-1.3.1 → e_data-1.3.3}/edata/processors/base.py +0 -0
- {e_data-1.3.1 → e_data-1.3.3}/edata/processors/billing.py +0 -0
- {e_data-1.3.1 → e_data-1.3.3}/edata/processors/consumption.py +0 -0
- {e_data-1.3.1 → e_data-1.3.3}/edata/processors/maximeter.py +0 -0
- {e_data-1.3.1 → e_data-1.3.3}/edata/processors/utils.py +0 -0
- {e_data-1.3.1 → e_data-1.3.3}/edata/tests/test_datadis_connector.py +0 -0
- {e_data-1.3.1 → e_data-1.3.3}/edata/tests/test_helpers.py +0 -0
- {e_data-1.3.1 → e_data-1.3.3}/edata/tests/test_processors.py +0 -0
- {e_data-1.3.1 → e_data-1.3.3}/edata/tests/test_redata_connector.py +0 -0
- {e_data-1.3.1 → e_data-1.3.3}/setup.cfg +0 -0
|
@@ -2,14 +2,20 @@ 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/cli.py
|
|
12
|
+
edata/const.py
|
|
13
|
+
edata/definitions.py
|
|
14
|
+
edata/helpers.py
|
|
15
|
+
edata/storage.py
|
|
5
16
|
edata/connectors/__init__.py
|
|
6
17
|
edata/connectors/datadis.py
|
|
7
18
|
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
19
|
edata/processors/__init__.py
|
|
14
20
|
edata/processors/base.py
|
|
15
21
|
edata/processors/billing.py
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from getpass import getpass
|
|
3
|
+
from edata.connectors.datadis import DatadisConnector
|
|
4
|
+
import json
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
"""CLI básica para mostrar supplies y contracts de Datadis."""
|
|
8
|
+
username = typer.prompt("Usuario (NIF)")
|
|
9
|
+
password = getpass("Contraseña: ")
|
|
10
|
+
connector = DatadisConnector(username, password)
|
|
11
|
+
supplies = connector.get_supplies()
|
|
12
|
+
typer.echo("\nSupplies:")
|
|
13
|
+
typer.echo(json.dumps(supplies, indent=2, default=str))
|
|
14
|
+
if supplies:
|
|
15
|
+
cups = supplies[0]["cups"]
|
|
16
|
+
distributor = supplies[0]["distributorCode"]
|
|
17
|
+
contracts = connector.get_contract_detail(cups, distributor)
|
|
18
|
+
typer.echo("\nContracts:")
|
|
19
|
+
typer.echo(json.dumps(contracts, indent=2, default=str))
|
|
20
|
+
else:
|
|
21
|
+
typer.echo("No se encontraron supplies.")
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
typer.run(main)
|
|
@@ -143,8 +143,8 @@ class DatadisConnector:
|
|
|
143
143
|
async with session.post(
|
|
144
144
|
URL_TOKEN,
|
|
145
145
|
data={
|
|
146
|
-
TOKEN_USERNAME: self._usr
|
|
147
|
-
TOKEN_PASSWD: self._pwd
|
|
146
|
+
TOKEN_USERNAME: self._usr,
|
|
147
|
+
TOKEN_PASSWD: self._pwd,
|
|
148
148
|
},
|
|
149
149
|
) as response:
|
|
150
150
|
text = await response.text()
|
|
@@ -223,7 +223,7 @@ class DatadisConnector:
|
|
|
223
223
|
if reply.status == 200:
|
|
224
224
|
_LOGGER.info("Got 200 OK")
|
|
225
225
|
try:
|
|
226
|
-
json_data = await reply.json()
|
|
226
|
+
json_data = await reply.json(content_type=None)
|
|
227
227
|
if json_data:
|
|
228
228
|
response = json_data
|
|
229
229
|
if not ignore_recent_queries:
|
|
@@ -232,7 +232,7 @@ class DatadisConnector:
|
|
|
232
232
|
_LOGGER.info("Got an empty response")
|
|
233
233
|
if not ignore_recent_queries:
|
|
234
234
|
self._update_recent_queries(url + params)
|
|
235
|
-
except Exception:
|
|
235
|
+
except Exception as e:
|
|
236
236
|
_LOGGER.warning("Failed to parse JSON response")
|
|
237
237
|
elif reply.status == 401 and not refresh_token:
|
|
238
238
|
response = await self._async_get(
|
|
@@ -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.
|
|
7
|
+
version = "1.3.3"
|
|
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 = ["
|
|
40
|
+
where = ["."]
|
|
41
41
|
|
|
42
42
|
[tool.setuptools]
|
|
43
43
|
include-package-data = true
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|