e-data 1.2.7__tar.gz → 1.2.8__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.2.7/e_data.egg-info → e-data-1.2.8}/PKG-INFO +1 -1
- {e-data-1.2.7 → e-data-1.2.8/e_data.egg-info}/PKG-INFO +1 -1
- {e-data-1.2.7 → e-data-1.2.8}/edata/connectors/datadis.py +124 -47
- {e-data-1.2.7 → e-data-1.2.8}/edata/definitions.py +25 -1
- {e-data-1.2.7 → e-data-1.2.8}/edata/helpers.py +53 -38
- {e-data-1.2.7 → e-data-1.2.8}/edata/processors/billing.py +5 -5
- {e-data-1.2.7 → e-data-1.2.8}/edata/processors/consumption.py +47 -22
- {e-data-1.2.7 → e-data-1.2.8}/setup.py +1 -1
- {e-data-1.2.7 → e-data-1.2.8}/LICENSE +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/MANIFEST.in +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/README.md +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/e_data.egg-info/SOURCES.txt +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/e_data.egg-info/dependency_links.txt +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/e_data.egg-info/requires.txt +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/e_data.egg-info/top_level.txt +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/edata/__init__.py +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/edata/connectors/__init__.py +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/edata/connectors/redata.py +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/edata/const.py +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/edata/processors/__init__.py +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/edata/processors/base.py +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/edata/processors/maximeter.py +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/edata/processors/utils.py +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/edata/storage.py +0 -0
- {e-data-1.2.7 → e-data-1.2.8}/setup.cfg +0 -0
|
@@ -1,5 +1,13 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""Datadis API connector.
|
|
2
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
|
+
- TODO: gzip header checksum errors under weird circumstances
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import contextlib
|
|
3
11
|
import hashlib
|
|
4
12
|
import json
|
|
5
13
|
import logging
|
|
@@ -14,9 +22,12 @@ from ..processors import utils
|
|
|
14
22
|
|
|
15
23
|
_LOGGER = logging.getLogger(__name__)
|
|
16
24
|
|
|
25
|
+
# Token-related constants
|
|
17
26
|
URL_TOKEN = "https://datadis.es/nikola-auth/tokens/login"
|
|
18
27
|
TOKEN_USERNAME = "username"
|
|
19
28
|
TOKEN_PASSWD = "password"
|
|
29
|
+
|
|
30
|
+
# Supplies-related constants
|
|
20
31
|
URL_GET_SUPPLIES = "https://datadis.es/api-private/api/get-supplies"
|
|
21
32
|
GET_SUPPLIES_MANDATORY_FIELDS = [
|
|
22
33
|
"cups",
|
|
@@ -25,6 +36,8 @@ GET_SUPPLIES_MANDATORY_FIELDS = [
|
|
|
25
36
|
"pointType",
|
|
26
37
|
"distributorCode",
|
|
27
38
|
]
|
|
39
|
+
|
|
40
|
+
# Contracts-related constants
|
|
28
41
|
URL_GET_CONTRACT_DETAIL = "https://datadis.es/api-private/api/get-contract-detail"
|
|
29
42
|
GET_CONTRACT_DETAIL_MANDATORY_FIELDS = [
|
|
30
43
|
"startDate",
|
|
@@ -32,6 +45,8 @@ GET_CONTRACT_DETAIL_MANDATORY_FIELDS = [
|
|
|
32
45
|
"marketer",
|
|
33
46
|
"contractedPowerkW",
|
|
34
47
|
]
|
|
48
|
+
|
|
49
|
+
# Consumption-related constants
|
|
35
50
|
URL_GET_CONSUMPTION_DATA = "https://datadis.es/api-private/api/get-consumption-data"
|
|
36
51
|
GET_CONSUMPTION_DATA_MANDATORY_FIELDS = [
|
|
37
52
|
"time",
|
|
@@ -39,17 +54,23 @@ GET_CONSUMPTION_DATA_MANDATORY_FIELDS = [
|
|
|
39
54
|
"consumptionKWh",
|
|
40
55
|
"obtainMethod",
|
|
41
56
|
]
|
|
57
|
+
MAX_CONSUMPTIONS_MONTHS = (
|
|
58
|
+
1 # max consumptions in a single request (fixed to 1 due to datadis limitations)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
# Maximeter-related constants
|
|
42
62
|
URL_GET_MAX_POWER = "https://datadis.es/api-private/api/get-max-power"
|
|
43
63
|
GET_MAX_POWER_MANDATORY_FIELDS = ["time", "date", "maxPower"]
|
|
44
64
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
QUERY_LIMIT = timedelta(hours=24)
|
|
65
|
+
# Timing constants
|
|
66
|
+
TIMEOUT = 3 * 60 # requests timeout
|
|
67
|
+
QUERY_LIMIT = timedelta(hours=24) # a datadis limitation, again...
|
|
50
68
|
|
|
69
|
+
# Cache-related constants
|
|
51
70
|
RECENT_QUERIES_FILENAME = "edata_recent_queries.json"
|
|
52
|
-
|
|
71
|
+
RECENT_QUERIES_CACHE_FILENAME = "edata_recent_queries_cache.json"
|
|
72
|
+
DEFAULT_RECENT_QUERIES_FILE = f"/tmp/{RECENT_QUERIES_FILENAME}"
|
|
73
|
+
DEFAULT_RECENT_QUERIES_CACHE = f"/tmp/{RECENT_QUERIES_CACHE_FILENAME}"
|
|
53
74
|
|
|
54
75
|
|
|
55
76
|
class DatadisConnector:
|
|
@@ -62,54 +83,71 @@ class DatadisConnector:
|
|
|
62
83
|
enable_smart_fetch: bool = True,
|
|
63
84
|
storage_path: str | None = None,
|
|
64
85
|
) -> None:
|
|
65
|
-
"""
|
|
86
|
+
"""DatadisConnector constructor."""
|
|
66
87
|
|
|
88
|
+
# initialize some things
|
|
67
89
|
self._usr = username
|
|
68
90
|
self._pwd = password
|
|
69
91
|
self._session = requests.Session()
|
|
70
92
|
self._token = {}
|
|
71
93
|
self._smart_fetch = enable_smart_fetch
|
|
72
|
-
|
|
94
|
+
self._recent_queries = {}
|
|
95
|
+
self._recent_cache = {}
|
|
73
96
|
if storage_path is not None:
|
|
74
97
|
self._recent_queries_file = os.path.join(
|
|
75
98
|
storage_path, RECENT_QUERIES_FILENAME
|
|
76
99
|
)
|
|
77
|
-
|
|
100
|
+
self._recent_queries_cache_file = os.path.join(
|
|
101
|
+
storage_path, RECENT_QUERIES_CACHE_FILENAME
|
|
102
|
+
)
|
|
78
103
|
else:
|
|
79
|
-
self._recent_queries_file =
|
|
80
|
-
|
|
104
|
+
self._recent_queries_file = DEFAULT_RECENT_QUERIES_FILE
|
|
105
|
+
self._recent_queries_cache_file = DEFAULT_RECENT_QUERIES_CACHE
|
|
81
106
|
self._warned_queries = []
|
|
82
107
|
|
|
83
|
-
|
|
108
|
+
# load caches (to avoid query spam if the program restarts)
|
|
109
|
+
with contextlib.suppress(FileNotFoundError):
|
|
84
110
|
with open(self._recent_queries_file, encoding="utf8") as dst_file:
|
|
85
111
|
self._recent_queries = json.load(dst_file)
|
|
86
112
|
for query in self._recent_queries:
|
|
87
113
|
self._recent_queries[query] = datetime.fromisoformat(
|
|
88
114
|
self._recent_queries[query]
|
|
89
115
|
)
|
|
90
|
-
|
|
91
|
-
|
|
116
|
+
with open(self._recent_queries_cache_file, encoding="utf8") as dst_file:
|
|
117
|
+
self._recent_cache = json.load(dst_file)
|
|
92
118
|
|
|
93
|
-
def _update_recent_queries(self, query: str) -> None:
|
|
94
|
-
"""
|
|
119
|
+
def _update_recent_queries(self, query: str, data: dict | None = None) -> None:
|
|
120
|
+
"""Cache a successful query to avoid exceeding query limits."""
|
|
95
121
|
|
|
122
|
+
# identify the query by a md5 hash
|
|
96
123
|
hash_query = hashlib.md5(query.encode()).hexdigest()
|
|
124
|
+
# prepare key (hash) and values (timestamp and response)
|
|
97
125
|
self._recent_queries[hash_query] = datetime.now()
|
|
126
|
+
if data is not None:
|
|
127
|
+
self._recent_cache[hash_query] = data
|
|
98
128
|
|
|
99
|
-
# purge old
|
|
129
|
+
# purge old cache
|
|
100
130
|
to_delete = []
|
|
101
131
|
for _query in self._recent_queries:
|
|
102
132
|
if (datetime.now() - self._recent_queries[_query]) > QUERY_LIMIT:
|
|
103
133
|
to_delete.append(_query)
|
|
104
134
|
|
|
105
135
|
for key in to_delete:
|
|
106
|
-
|
|
136
|
+
with contextlib.suppress(KeyError):
|
|
137
|
+
self._recent_queries.pop(key, None)
|
|
138
|
+
self._recent_cache.pop(key, None)
|
|
107
139
|
|
|
140
|
+
# dump current cache to disk
|
|
108
141
|
try:
|
|
109
142
|
with open(self._recent_queries_file, "w", encoding="utf8") as dst_file:
|
|
110
143
|
json.dump(utils.serialize_dict(self._recent_queries), dst_file)
|
|
111
|
-
|
|
112
|
-
|
|
144
|
+
if data is not None:
|
|
145
|
+
with open(
|
|
146
|
+
self._recent_queries_cache_file, "w", encoding="utf8"
|
|
147
|
+
) as dst_file:
|
|
148
|
+
json.dump(self._recent_cache, dst_file)
|
|
149
|
+
except Exception as e:
|
|
150
|
+
_LOGGER.warning("Unknown error while updating cache: %s", e)
|
|
113
151
|
|
|
114
152
|
def _is_recent_query(self, query: str) -> bool:
|
|
115
153
|
"""Check if a query has been done recently to avoid exceeding query limits."""
|
|
@@ -119,6 +157,12 @@ class DatadisConnector:
|
|
|
119
157
|
return (datetime.now() - self._recent_queries[hash_query]) < QUERY_LIMIT
|
|
120
158
|
return False
|
|
121
159
|
|
|
160
|
+
def _get_cache_for_query(self, query: str) -> dict:
|
|
161
|
+
"""Return cached response for a query."""
|
|
162
|
+
hash_query = hashlib.md5(query.encode()).hexdigest()
|
|
163
|
+
|
|
164
|
+
return self._recent_cache.get(hash_query, None)
|
|
165
|
+
|
|
122
166
|
def _get_token(self):
|
|
123
167
|
"""Private method that fetches a new token if needed."""
|
|
124
168
|
|
|
@@ -137,7 +181,6 @@ class DatadisConnector:
|
|
|
137
181
|
self._token["encoded"] = response.text
|
|
138
182
|
# prepare session authorization bearer
|
|
139
183
|
self._session.headers["Authorization"] = "Bearer " + self._token["encoded"]
|
|
140
|
-
_LOGGER.debug("token received")
|
|
141
184
|
is_valid_token = True
|
|
142
185
|
else:
|
|
143
186
|
_LOGGER.error("Unknown error while retrieving token, got %s", response.text)
|
|
@@ -147,7 +190,7 @@ class DatadisConnector:
|
|
|
147
190
|
"""Test to login with provided credentials."""
|
|
148
191
|
return self._get_token()
|
|
149
192
|
|
|
150
|
-
def
|
|
193
|
+
def _get(
|
|
151
194
|
self,
|
|
152
195
|
url: str,
|
|
153
196
|
request_data: dict | None = None,
|
|
@@ -155,7 +198,7 @@ class DatadisConnector:
|
|
|
155
198
|
is_retry: bool = False,
|
|
156
199
|
ignore_recent_queries: bool = False,
|
|
157
200
|
):
|
|
158
|
-
"""
|
|
201
|
+
"""Get request for Datadis API."""
|
|
159
202
|
|
|
160
203
|
if request_data is None:
|
|
161
204
|
data = {}
|
|
@@ -174,31 +217,49 @@ class DatadisConnector:
|
|
|
174
217
|
key = param
|
|
175
218
|
value = data[param]
|
|
176
219
|
params = params + f"{key}={value}&"
|
|
177
|
-
# query
|
|
178
220
|
|
|
221
|
+
# check if query is already in cache
|
|
179
222
|
if not ignore_recent_queries and self._is_recent_query(url + params):
|
|
180
|
-
|
|
223
|
+
_cache = self._get_cache_for_query(url + params)
|
|
224
|
+
if _cache is not None:
|
|
225
|
+
return _cache
|
|
226
|
+
return []
|
|
181
227
|
|
|
228
|
+
# run the query
|
|
182
229
|
try:
|
|
183
|
-
_LOGGER.
|
|
184
|
-
reply = self._session.get(
|
|
230
|
+
_LOGGER.debug("GET %s", url + params)
|
|
231
|
+
reply = self._session.get(
|
|
232
|
+
url + params,
|
|
233
|
+
headers={"Accept-Encoding": "identity"},
|
|
234
|
+
timeout=TIMEOUT,
|
|
235
|
+
)
|
|
185
236
|
except requests.exceptions.Timeout:
|
|
186
237
|
_LOGGER.warning("Timeout at %s", url + params)
|
|
187
|
-
return
|
|
238
|
+
return []
|
|
188
239
|
|
|
189
240
|
# eval response
|
|
190
|
-
if reply.status_code == 200
|
|
241
|
+
if reply.status_code == 200:
|
|
242
|
+
# we're here if reply seems valid
|
|
191
243
|
_LOGGER.info("Got 200 OK at %s", url + params)
|
|
192
|
-
|
|
193
|
-
|
|
244
|
+
if reply.json():
|
|
245
|
+
response = reply.json()
|
|
246
|
+
self._update_recent_queries(url + params, response)
|
|
247
|
+
else:
|
|
248
|
+
# this mostly happens when datadis provides an empty response
|
|
249
|
+
_LOGGER.info(
|
|
250
|
+
"Datadis returned an empty response at %s", url + params
|
|
251
|
+
)
|
|
252
|
+
self._update_recent_queries(url + params)
|
|
194
253
|
elif reply.status_code == 401 and not refresh_token:
|
|
195
|
-
|
|
254
|
+
# we're here if we were unauthorized so we will refresh the token
|
|
255
|
+
response = self._get(
|
|
196
256
|
url,
|
|
197
257
|
request_data=data,
|
|
198
258
|
refresh_token=True,
|
|
199
259
|
ignore_recent_queries=ignore_recent_queries,
|
|
200
260
|
)
|
|
201
261
|
elif reply.status_code == 429:
|
|
262
|
+
# we're here if we exceeded datadis API rates (24h)
|
|
202
263
|
_LOGGER.warning(
|
|
203
264
|
"%s %s at %s",
|
|
204
265
|
reply.status_code,
|
|
@@ -206,12 +267,8 @@ class DatadisConnector:
|
|
|
206
267
|
url + params,
|
|
207
268
|
)
|
|
208
269
|
self._update_recent_queries(url + params)
|
|
209
|
-
elif reply.status_code == 200:
|
|
210
|
-
_LOGGER.info(
|
|
211
|
-
"%s returned an empty response, try again later", url + params
|
|
212
|
-
)
|
|
213
|
-
self._update_recent_queries(url + params)
|
|
214
270
|
elif is_retry:
|
|
271
|
+
# otherwise, if this was a retried request... warn the user
|
|
215
272
|
if (url + params) not in self._warned_queries:
|
|
216
273
|
_LOGGER.warning(
|
|
217
274
|
"%s %s at %s. %s. %s",
|
|
@@ -224,41 +281,61 @@ class DatadisConnector:
|
|
|
224
281
|
self._update_recent_queries(url + params)
|
|
225
282
|
self._warned_queries.append(url + params)
|
|
226
283
|
else:
|
|
227
|
-
|
|
284
|
+
# finally, retry since an unexpected error took place (mostly 500 errors - server fault)
|
|
285
|
+
response = self._get(
|
|
286
|
+
url,
|
|
287
|
+
request_data,
|
|
288
|
+
is_retry=True,
|
|
289
|
+
ignore_recent_queries=ignore_recent_queries,
|
|
290
|
+
)
|
|
291
|
+
|
|
228
292
|
return response
|
|
229
293
|
|
|
230
294
|
def get_supplies(self, authorized_nif: str | None = None):
|
|
231
|
-
"""Datadis get_supplies query."""
|
|
295
|
+
"""Datadis 'get_supplies' query."""
|
|
296
|
+
|
|
232
297
|
data = {}
|
|
298
|
+
|
|
299
|
+
# If authorized_nif is provided, we have to include it as parameter
|
|
233
300
|
if authorized_nif is not None:
|
|
234
301
|
data["authorizedNif"] = authorized_nif
|
|
235
|
-
|
|
302
|
+
|
|
303
|
+
# Request the resource
|
|
304
|
+
response = self._get(
|
|
236
305
|
URL_GET_SUPPLIES, request_data=data, ignore_recent_queries=False
|
|
237
306
|
)
|
|
307
|
+
|
|
308
|
+
# Response is a list of serialized supplies.
|
|
309
|
+
# We will iter through them to transform them into SupplyData objects
|
|
238
310
|
supplies = []
|
|
311
|
+
# Build tomorrow Y/m/d string since we will use it as the 'date_end' of
|
|
312
|
+
# active supplies
|
|
239
313
|
tomorrow_str = (datetime.today() + timedelta(days=1)).strftime("%Y/%m/%d")
|
|
240
314
|
for i in response:
|
|
315
|
+
# check data integrity (maybe this can be supressed if datadis proves to be reliable)
|
|
241
316
|
if all(k in i for k in GET_SUPPLIES_MANDATORY_FIELDS):
|
|
242
317
|
supplies.append(
|
|
243
318
|
SupplyData(
|
|
244
|
-
cups=i["cups"],
|
|
319
|
+
cups=i["cups"], # the supply identifier
|
|
245
320
|
date_start=datetime.strptime(
|
|
246
321
|
i["validDateFrom"]
|
|
247
322
|
if i["validDateFrom"] != ""
|
|
248
323
|
else "1970/01/01",
|
|
249
324
|
"%Y/%m/%d",
|
|
250
|
-
),
|
|
325
|
+
), # start date of the supply. 1970/01/01 if unset.
|
|
251
326
|
date_end=datetime.strptime(
|
|
252
327
|
i["validDateTo"]
|
|
253
328
|
if i["validDateTo"] != ""
|
|
254
329
|
else tomorrow_str,
|
|
255
330
|
"%Y/%m/%d",
|
|
256
|
-
),
|
|
331
|
+
), # end date of the supply, tomorrow if unset
|
|
332
|
+
# the following parameters are not crucial, so they can be none
|
|
257
333
|
address=i["address"] if "address" in i else None,
|
|
258
334
|
postal_code=i["postalCode"] if "postalCode" in i else None,
|
|
259
335
|
province=i["province"] if "province" in i else None,
|
|
260
336
|
municipality=i["municipality"] if "municipality" in i else None,
|
|
261
337
|
distributor=i["distributor"] if "distributor" in i else None,
|
|
338
|
+
# these two are mandatory, we will use them to fetch contracts data
|
|
262
339
|
pointType=i["pointType"],
|
|
263
340
|
distributorCode=i["distributorCode"],
|
|
264
341
|
)
|
|
@@ -277,7 +354,7 @@ class DatadisConnector:
|
|
|
277
354
|
data = {"cups": cups, "distributorCode": distributor_code}
|
|
278
355
|
if authorized_nif is not None:
|
|
279
356
|
data["authorizedNif"] = authorized_nif
|
|
280
|
-
response = self.
|
|
357
|
+
response = self._get(
|
|
281
358
|
URL_GET_CONTRACT_DETAIL, request_data=data, ignore_recent_queries=False
|
|
282
359
|
)
|
|
283
360
|
contracts = []
|
|
@@ -359,7 +436,7 @@ class DatadisConnector:
|
|
|
359
436
|
if authorized_nif is not None:
|
|
360
437
|
data["authorizedNif"] = authorized_nif
|
|
361
438
|
|
|
362
|
-
response = self.
|
|
439
|
+
response = self._get(URL_GET_CONSUMPTION_DATA, request_data=data)
|
|
363
440
|
|
|
364
441
|
consumptions = []
|
|
365
442
|
for i in response:
|
|
@@ -405,7 +482,7 @@ class DatadisConnector:
|
|
|
405
482
|
}
|
|
406
483
|
if authorized_nif is not None:
|
|
407
484
|
data["authorizedNif"] = authorized_nif
|
|
408
|
-
response = self.
|
|
485
|
+
response = self._get(URL_GET_MAX_POWER, request_data=data)
|
|
409
486
|
maxpower_values = []
|
|
410
487
|
for i in response:
|
|
411
488
|
if all(k in i for k in GET_MAX_POWER_MANDATORY_FIELDS):
|
|
@@ -14,18 +14,30 @@ ATTRIBUTES = {
|
|
|
14
14
|
"yesterday_p1_kWh": "kWh",
|
|
15
15
|
"yesterday_p2_kWh": "kWh",
|
|
16
16
|
"yesterday_p3_kWh": "kWh",
|
|
17
|
+
"yesterday_surplus_kWh": "kWh",
|
|
18
|
+
"yesterday_surplus_p1_kWh": "kWh",
|
|
19
|
+
"yesterday_surplus_p2_kWh": "kWh",
|
|
20
|
+
"yesterday_surplus_p3_kWh": "kWh",
|
|
17
21
|
"last_registered_date": None,
|
|
18
22
|
"last_registered_day_kWh": "kWh",
|
|
19
23
|
"last_registered_day_hours": "h",
|
|
20
24
|
"last_registered_day_p1_kWh": "kWh",
|
|
21
25
|
"last_registered_day_p2_kWh": "kWh",
|
|
22
26
|
"last_registered_day_p3_kWh": "kWh",
|
|
27
|
+
"last_registered_day_surplus_kWh": "kWh",
|
|
28
|
+
"last_registered_day_surplus_p1_kWh": "kWh",
|
|
29
|
+
"last_registered_day_surplus_p2_kWh": "kWh",
|
|
30
|
+
"last_registered_day_surplus_p3_kWh": "kWh",
|
|
23
31
|
"month_kWh": "kWh",
|
|
24
32
|
"month_daily_kWh": "kWh",
|
|
25
33
|
"month_days": "d",
|
|
26
34
|
"month_p1_kWh": "kWh",
|
|
27
35
|
"month_p2_kWh": "kWh",
|
|
28
36
|
"month_p3_kWh": "kWh",
|
|
37
|
+
"month_surplus_kWh": "kWh",
|
|
38
|
+
"month_surplus_p1_kWh": "kWh",
|
|
39
|
+
"month_surplus_p2_kWh": "kWh",
|
|
40
|
+
"month_surplus_p3_kWh": "kWh",
|
|
29
41
|
"month_€": "€",
|
|
30
42
|
"last_month_kWh": "kWh",
|
|
31
43
|
"last_month_daily_kWh": "kWh",
|
|
@@ -33,6 +45,10 @@ ATTRIBUTES = {
|
|
|
33
45
|
"last_month_p1_kWh": "kWh",
|
|
34
46
|
"last_month_p2_kWh": "kWh",
|
|
35
47
|
"last_month_p3_kWh": "kWh",
|
|
48
|
+
"last_month_surplus_kWh": "kWh",
|
|
49
|
+
"last_month_surplus_p1_kWh": "kWh",
|
|
50
|
+
"last_month_surplus_p2_kWh": "kWh",
|
|
51
|
+
"last_month_surplus_p3_kWh": "kWh",
|
|
36
52
|
"last_month_€": "€",
|
|
37
53
|
"max_power_kW": "kW",
|
|
38
54
|
"max_power_date": None,
|
|
@@ -118,7 +134,7 @@ ConsumptionSchema = vol.Schema(
|
|
|
118
134
|
vol.Required("datetime"): dt.datetime,
|
|
119
135
|
vol.Required("delta_h"): vol.Coerce(float),
|
|
120
136
|
vol.Required("value_kWh"): vol.Coerce(float),
|
|
121
|
-
vol.Optional("surplus_kWh", default=0): vol.
|
|
137
|
+
vol.Optional("surplus_kWh", default=0): vol.Coerce(float),
|
|
122
138
|
vol.Required("real"): bool,
|
|
123
139
|
}
|
|
124
140
|
)
|
|
@@ -224,6 +240,10 @@ class ConsumptionAggData(TypedDict):
|
|
|
224
240
|
value_p1_kWh: float
|
|
225
241
|
value_p2_kWh: float
|
|
226
242
|
value_p3_kWh: float
|
|
243
|
+
surplus_kWh: float
|
|
244
|
+
surplus_p1_kWh: float
|
|
245
|
+
surplus_p2_kWh: float
|
|
246
|
+
surplus_p3_kWh: float
|
|
227
247
|
delta_h: float
|
|
228
248
|
|
|
229
249
|
|
|
@@ -234,6 +254,10 @@ ConsumptionAggSchema = vol.Schema(
|
|
|
234
254
|
vol.Required("value_p1_kWh"): vol.Coerce(float),
|
|
235
255
|
vol.Required("value_p2_kWh"): vol.Coerce(float),
|
|
236
256
|
vol.Required("value_p3_kWh"): vol.Coerce(float),
|
|
257
|
+
vol.Optional("surplus_kWh", default=0): vol.Coerce(float),
|
|
258
|
+
vol.Optional("surplus_p1_kWh", default=0): vol.Coerce(float),
|
|
259
|
+
vol.Optional("surplus_p2_kWh", default=0): vol.Coerce(float),
|
|
260
|
+
vol.Optional("surplus_p3_kWh", default=0): vol.Coerce(float),
|
|
237
261
|
vol.Required("delta_h"): vol.Coerce(float),
|
|
238
262
|
}
|
|
239
263
|
)
|
|
@@ -479,15 +479,23 @@ class EdataHelper:
|
|
|
479
479
|
self.attributes["yesterday_kWh"] = (
|
|
480
480
|
yday.get("value_kWh", None) if yday is not None else None
|
|
481
481
|
)
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
self.attributes["
|
|
489
|
-
yday.get("
|
|
482
|
+
|
|
483
|
+
for tariff in (1, 2, 3):
|
|
484
|
+
self.attributes[f"yesterday_p{tariff}_kWh"] = (
|
|
485
|
+
yday.get(f"value_p{tariff}_kWh", None) if yday is not None else None
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
self.attributes["yesterday_surplus_kWh"] = (
|
|
489
|
+
yday.get("surplus_kWh", None) if yday is not None else None
|
|
490
490
|
)
|
|
491
|
+
|
|
492
|
+
for tariff in (1, 2, 3):
|
|
493
|
+
self.attributes[f"yesterday_surplus_p{tariff}_kWh"] = (
|
|
494
|
+
yday.get(f"surplus_p{tariff}_kWh", None)
|
|
495
|
+
if yday is not None
|
|
496
|
+
else None
|
|
497
|
+
)
|
|
498
|
+
|
|
491
499
|
self.attributes["yesterday_hours"] = (
|
|
492
500
|
yday.get("delta_h", None) if yday is not None else None
|
|
493
501
|
)
|
|
@@ -498,6 +506,9 @@ class EdataHelper:
|
|
|
498
506
|
self.attributes["month_kWh"] = (
|
|
499
507
|
month.get("value_kWh", None) if month is not None else None
|
|
500
508
|
)
|
|
509
|
+
self.attributes["month_surplus_kWh"] = (
|
|
510
|
+
month.get("surplus_kWh", None) if month is not None else None
|
|
511
|
+
)
|
|
501
512
|
self.attributes["month_days"] = (
|
|
502
513
|
month.get("delta_h", 0) / 24 if month is not None else None
|
|
503
514
|
)
|
|
@@ -510,15 +521,13 @@ class EdataHelper:
|
|
|
510
521
|
if month is not None
|
|
511
522
|
else None
|
|
512
523
|
)
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
month.get("value_p3_kWh", None) if month is not None else None
|
|
521
|
-
)
|
|
524
|
+
|
|
525
|
+
for tariff in (1, 2, 3):
|
|
526
|
+
self.attributes[f"month_surplus_p{tariff}_kWh"] = (
|
|
527
|
+
month.get(f"surplus_p{tariff}_kWh", None)
|
|
528
|
+
if month is not None
|
|
529
|
+
else None
|
|
530
|
+
)
|
|
522
531
|
|
|
523
532
|
last_month = utils.get_by_key(
|
|
524
533
|
self.data["consumptions_monthly_sum"],
|
|
@@ -528,6 +537,9 @@ class EdataHelper:
|
|
|
528
537
|
self.attributes["last_month_kWh"] = (
|
|
529
538
|
last_month.get("value_kWh", None) if last_month is not None else None
|
|
530
539
|
)
|
|
540
|
+
self.attributes["last_month_surplus_kWh"] = (
|
|
541
|
+
last_month.get("surplus_kWh", None) if last_month is not None else None
|
|
542
|
+
)
|
|
531
543
|
self.attributes["last_month_days"] = (
|
|
532
544
|
last_month.get("delta_h", 0) / 24 if last_month is not None else None
|
|
533
545
|
)
|
|
@@ -543,15 +555,18 @@ class EdataHelper:
|
|
|
543
555
|
if last_month is not None
|
|
544
556
|
else None
|
|
545
557
|
)
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
558
|
+
for tariff in (1, 2, 3):
|
|
559
|
+
self.attributes[f"last_month_p{tariff}_kWh"] = (
|
|
560
|
+
last_month.get(f"value_p{tariff}_kWh", None)
|
|
561
|
+
if last_month is not None
|
|
562
|
+
else None
|
|
563
|
+
)
|
|
564
|
+
for tariff in (1, 2, 3):
|
|
565
|
+
self.attributes[f"last_month_surplus_p{tariff}_kWh"] = (
|
|
566
|
+
last_month.get(f"surplus_p{tariff}_kWh", None)
|
|
567
|
+
if last_month is not None
|
|
568
|
+
else None
|
|
569
|
+
)
|
|
555
570
|
|
|
556
571
|
if len(self.data["consumptions"]) > 0:
|
|
557
572
|
self.attributes["last_registered_date"] = self.data["consumptions"][-1][
|
|
@@ -565,21 +580,21 @@ class EdataHelper:
|
|
|
565
580
|
if last_day is not None
|
|
566
581
|
else None
|
|
567
582
|
)
|
|
568
|
-
self.attributes["
|
|
569
|
-
last_day.get("
|
|
570
|
-
if last_day is not None
|
|
571
|
-
else None
|
|
572
|
-
)
|
|
573
|
-
self.attributes["last_registered_day_p2_kWh"] = (
|
|
574
|
-
last_day.get("value_p2_kWh", None)
|
|
575
|
-
if last_day is not None
|
|
576
|
-
else None
|
|
577
|
-
)
|
|
578
|
-
self.attributes["last_registered_day_p3_kWh"] = (
|
|
579
|
-
last_day.get("value_p3_kWh", None)
|
|
583
|
+
self.attributes["last_registered_day_surplus_kWh"] = (
|
|
584
|
+
last_day.get("surplus_kWh", None)
|
|
580
585
|
if last_day is not None
|
|
581
586
|
else None
|
|
582
587
|
)
|
|
588
|
+
|
|
589
|
+
for tariff in (1, 2, 3):
|
|
590
|
+
self.attributes[
|
|
591
|
+
f"last_registered_day_surplus_p{tariff}_kWh"
|
|
592
|
+
] = (
|
|
593
|
+
last_day.get(f"surplus_p{tariff}_kWh", None)
|
|
594
|
+
if last_day is not None
|
|
595
|
+
else None
|
|
596
|
+
)
|
|
597
|
+
|
|
583
598
|
self.attributes["last_registered_day_hours"] = (
|
|
584
599
|
last_day.get("delta_h", None) if last_day is not None else None
|
|
585
600
|
)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Billing data processors"""
|
|
1
|
+
"""Billing data processors."""
|
|
2
2
|
|
|
3
3
|
import logging
|
|
4
4
|
from datetime import datetime, timedelta
|
|
@@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
26
26
|
|
|
27
27
|
|
|
28
28
|
class BillingOutput(TypedDict):
|
|
29
|
-
"""A dict holding BillingProcessor output property"""
|
|
29
|
+
"""A dict holding BillingProcessor output property."""
|
|
30
30
|
|
|
31
31
|
hourly: list[PricingAggData]
|
|
32
32
|
daily: list[PricingAggData]
|
|
@@ -34,7 +34,7 @@ class BillingOutput(TypedDict):
|
|
|
34
34
|
|
|
35
35
|
|
|
36
36
|
class BillingInput(TypedDict):
|
|
37
|
-
"""A dict holding BillingProcessor input data"""
|
|
37
|
+
"""A dict holding BillingProcessor input data."""
|
|
38
38
|
|
|
39
39
|
contracts: list[ContractData]
|
|
40
40
|
consumptions: list[ConsumptionData]
|
|
@@ -43,10 +43,10 @@ class BillingInput(TypedDict):
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class BillingProcessor(Processor):
|
|
46
|
-
"""A billing processor for edata"""
|
|
46
|
+
"""A billing processor for edata."""
|
|
47
47
|
|
|
48
48
|
def do_process(self):
|
|
49
|
-
"""Main method for the BillingProcessor"""
|
|
49
|
+
"""Main method for the BillingProcessor."""
|
|
50
50
|
self._output = BillingOutput(hourly=[], daily=[], monthly=[])
|
|
51
51
|
|
|
52
52
|
_schema = voluptuous.Schema(
|
|
@@ -53,19 +53,22 @@ class ConsumptionProcessor(Processor):
|
|
|
53
53
|
|
|
54
54
|
tariff = utils.get_pvpc_tariff(curr_hour_dt)
|
|
55
55
|
kwh = consumption["value_kWh"]
|
|
56
|
+
surplus_kwh = consumption["surplus_kWh"]
|
|
56
57
|
delta_h = consumption["delta_h"]
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
kwh_p3 = 0
|
|
59
|
+
kwh_by_tariff = [0, 0, 0]
|
|
60
|
+
surplus_kwh_by_tariff = [0, 0, 0]
|
|
61
61
|
|
|
62
62
|
match tariff:
|
|
63
63
|
case "p1":
|
|
64
|
-
|
|
64
|
+
kwh_by_tariff[0] = kwh
|
|
65
|
+
surplus_kwh_by_tariff[0] = surplus_kwh
|
|
65
66
|
case "p2":
|
|
66
|
-
|
|
67
|
+
kwh_by_tariff[1] = kwh
|
|
68
|
+
surplus_kwh_by_tariff[1] = surplus_kwh
|
|
67
69
|
case "p3":
|
|
68
|
-
|
|
70
|
+
kwh_by_tariff[2] = kwh
|
|
71
|
+
surplus_kwh_by_tariff[2] = surplus_kwh
|
|
69
72
|
|
|
70
73
|
if last_day_dt is None or curr_day_dt != last_day_dt:
|
|
71
74
|
self._output["daily"].append(
|
|
@@ -73,16 +76,24 @@ class ConsumptionProcessor(Processor):
|
|
|
73
76
|
datetime=curr_day_dt,
|
|
74
77
|
value_kWh=kwh,
|
|
75
78
|
delta_h=delta_h,
|
|
76
|
-
value_p1_kWh=
|
|
77
|
-
value_p2_kWh=
|
|
78
|
-
value_p3_kWh=
|
|
79
|
+
value_p1_kWh=kwh_by_tariff[0],
|
|
80
|
+
value_p2_kWh=kwh_by_tariff[1],
|
|
81
|
+
value_p3_kWh=kwh_by_tariff[2],
|
|
82
|
+
surplus_kWh=surplus_kwh,
|
|
83
|
+
surplus_p1_kWh=surplus_kwh_by_tariff[0],
|
|
84
|
+
surplus_p2_kWh=surplus_kwh_by_tariff[1],
|
|
85
|
+
surplus_p3_kWh=surplus_kwh_by_tariff[2],
|
|
79
86
|
)
|
|
80
87
|
)
|
|
81
88
|
else:
|
|
82
89
|
self._output["daily"][-1]["value_kWh"] += kwh
|
|
83
|
-
self._output["daily"][-1]["value_p1_kWh"] +=
|
|
84
|
-
self._output["daily"][-1]["value_p2_kWh"] +=
|
|
85
|
-
self._output["daily"][-1]["value_p3_kWh"] +=
|
|
90
|
+
self._output["daily"][-1]["value_p1_kWh"] += kwh_by_tariff[0]
|
|
91
|
+
self._output["daily"][-1]["value_p2_kWh"] += kwh_by_tariff[1]
|
|
92
|
+
self._output["daily"][-1]["value_p3_kWh"] += kwh_by_tariff[2]
|
|
93
|
+
self._output["daily"][-1]["surplus_kWh"] += surplus_kwh
|
|
94
|
+
self._output["daily"][-1]["surplus_p1_kWh"] += surplus_kwh_by_tariff[0]
|
|
95
|
+
self._output["daily"][-1]["surplus_p2_kWh"] += surplus_kwh_by_tariff[1]
|
|
96
|
+
self._output["daily"][-1]["surplus_p3_kWh"] += surplus_kwh_by_tariff[2]
|
|
86
97
|
self._output["daily"][-1]["delta_h"] += delta_h
|
|
87
98
|
|
|
88
99
|
if last_month_dt is None or curr_month_dt != last_month_dt:
|
|
@@ -91,24 +102,38 @@ class ConsumptionProcessor(Processor):
|
|
|
91
102
|
datetime=curr_month_dt,
|
|
92
103
|
value_kWh=kwh,
|
|
93
104
|
delta_h=delta_h,
|
|
94
|
-
value_p1_kWh=
|
|
95
|
-
value_p2_kWh=
|
|
96
|
-
value_p3_kWh=
|
|
105
|
+
value_p1_kWh=kwh_by_tariff[0],
|
|
106
|
+
value_p2_kWh=kwh_by_tariff[1],
|
|
107
|
+
value_p3_kWh=kwh_by_tariff[2],
|
|
108
|
+
surplus_kWh=surplus_kwh,
|
|
109
|
+
surplus_p1_kWh=surplus_kwh_by_tariff[0],
|
|
110
|
+
surplus_p2_kWh=surplus_kwh_by_tariff[1],
|
|
111
|
+
surplus_p3_kWh=surplus_kwh_by_tariff[2],
|
|
97
112
|
)
|
|
98
113
|
)
|
|
99
114
|
else:
|
|
100
115
|
self._output["monthly"][-1]["value_kWh"] += kwh
|
|
101
|
-
self._output["monthly"][-1]["value_p1_kWh"] +=
|
|
102
|
-
self._output["monthly"][-1]["value_p2_kWh"] +=
|
|
103
|
-
self._output["monthly"][-1]["value_p3_kWh"] +=
|
|
116
|
+
self._output["monthly"][-1]["value_p1_kWh"] += kwh_by_tariff[0]
|
|
117
|
+
self._output["monthly"][-1]["value_p2_kWh"] += kwh_by_tariff[1]
|
|
118
|
+
self._output["monthly"][-1]["value_p3_kWh"] += kwh_by_tariff[2]
|
|
119
|
+
self._output["monthly"][-1]["surplus_kWh"] += surplus_kwh
|
|
120
|
+
self._output["monthly"][-1]["surplus_p1_kWh"] += surplus_kwh_by_tariff[
|
|
121
|
+
0
|
|
122
|
+
]
|
|
123
|
+
self._output["monthly"][-1]["surplus_p2_kWh"] += surplus_kwh_by_tariff[
|
|
124
|
+
1
|
|
125
|
+
]
|
|
126
|
+
self._output["monthly"][-1]["surplus_p3_kWh"] += surplus_kwh_by_tariff[
|
|
127
|
+
2
|
|
128
|
+
]
|
|
104
129
|
self._output["monthly"][-1]["delta_h"] += delta_h
|
|
105
130
|
|
|
106
131
|
last_day_dt = curr_day_dt
|
|
107
132
|
last_month_dt = curr_month_dt
|
|
108
133
|
|
|
134
|
+
# Round to two decimals
|
|
109
135
|
for item in self._output:
|
|
110
136
|
for cons in self._output[item]:
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
cons["value_p3_kWh"] = round(cons["value_p3_kWh"], 2)
|
|
137
|
+
for key in cons:
|
|
138
|
+
if isinstance(cons[key], float):
|
|
139
|
+
cons[key] = round(cons[key], 2)
|
|
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
|