e-data 1.2.6__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.6/e_data.egg-info → e-data-1.2.8}/PKG-INFO +1 -1
- {e-data-1.2.6 → e-data-1.2.8/e_data.egg-info}/PKG-INFO +1 -1
- {e-data-1.2.6 → e-data-1.2.8}/edata/connectors/datadis.py +124 -47
- {e-data-1.2.6 → e-data-1.2.8}/edata/definitions.py +33 -10
- {e-data-1.2.6 → e-data-1.2.8}/edata/helpers.py +61 -39
- {e-data-1.2.6 → e-data-1.2.8}/edata/processors/base.py +5 -5
- {e-data-1.2.6 → e-data-1.2.8}/edata/processors/billing.py +26 -10
- e-data-1.2.8/edata/processors/consumption.py +139 -0
- {e-data-1.2.6 → e-data-1.2.8}/setup.py +1 -1
- e-data-1.2.6/edata/processors/consumption.py +0 -103
- {e-data-1.2.6 → e-data-1.2.8}/LICENSE +0 -0
- {e-data-1.2.6 → e-data-1.2.8}/MANIFEST.in +0 -0
- {e-data-1.2.6 → e-data-1.2.8}/README.md +0 -0
- {e-data-1.2.6 → e-data-1.2.8}/e_data.egg-info/SOURCES.txt +0 -0
- {e-data-1.2.6 → e-data-1.2.8}/e_data.egg-info/dependency_links.txt +0 -0
- {e-data-1.2.6 → e-data-1.2.8}/e_data.egg-info/requires.txt +0 -0
- {e-data-1.2.6 → e-data-1.2.8}/e_data.egg-info/top_level.txt +0 -0
- {e-data-1.2.6 → e-data-1.2.8}/edata/__init__.py +0 -0
- {e-data-1.2.6 → e-data-1.2.8}/edata/connectors/__init__.py +0 -0
- {e-data-1.2.6 → e-data-1.2.8}/edata/connectors/redata.py +0 -0
- {e-data-1.2.6 → e-data-1.2.8}/edata/const.py +0 -0
- {e-data-1.2.6 → e-data-1.2.8}/edata/processors/__init__.py +0 -0
- {e-data-1.2.6 → e-data-1.2.8}/edata/processors/maximeter.py +0 -0
- {e-data-1.2.6 → e-data-1.2.8}/edata/processors/utils.py +0 -0
- {e-data-1.2.6 → e-data-1.2.8}/edata/storage.py +0 -0
- {e-data-1.2.6 → 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
|
)
|
|
@@ -175,22 +191,23 @@ class PricingRules(TypedDict):
|
|
|
175
191
|
power_formula: str | None
|
|
176
192
|
others_formula: str | None
|
|
177
193
|
surplus_formula: str | None
|
|
194
|
+
cycle_start_day: int | None
|
|
178
195
|
|
|
179
196
|
|
|
180
197
|
PricingRulesSchema = vol.Schema(
|
|
181
198
|
{
|
|
182
199
|
vol.Required("p1_kw_year_eur"): vol.Coerce(float),
|
|
183
200
|
vol.Required("p2_kw_year_eur"): vol.Coerce(float),
|
|
184
|
-
vol.
|
|
185
|
-
vol.
|
|
186
|
-
vol.
|
|
187
|
-
vol.Optional("surplus_p1_kwh_eur", default=
|
|
201
|
+
vol.Optional("p1_kwh_eur", default=None): vol.Union(vol.Coerce(float), None),
|
|
202
|
+
vol.Optional("p2_kwh_eur", default=None): vol.Union(vol.Coerce(float), None),
|
|
203
|
+
vol.Optional("p3_kwh_eur", default=None): vol.Union(vol.Coerce(float), None),
|
|
204
|
+
vol.Optional("surplus_p1_kwh_eur", default=None): vol.Union(
|
|
188
205
|
vol.Coerce(float), None
|
|
189
206
|
),
|
|
190
|
-
vol.Optional("surplus_p2_kwh_eur", default=
|
|
207
|
+
vol.Optional("surplus_p2_kwh_eur", default=None): vol.Union(
|
|
191
208
|
vol.Coerce(float), None
|
|
192
209
|
),
|
|
193
|
-
vol.Optional("surplus_p3_kwh_eur", default=
|
|
210
|
+
vol.Optional("surplus_p3_kwh_eur", default=None): vol.Union(
|
|
194
211
|
vol.Coerce(float), None
|
|
195
212
|
),
|
|
196
213
|
vol.Required("meter_month_eur"): vol.Coerce(float),
|
|
@@ -201,6 +218,7 @@ PricingRulesSchema = vol.Schema(
|
|
|
201
218
|
vol.Optional("power_formula", default=DEFAULT_BILLING_POWER_FORMULA): str,
|
|
202
219
|
vol.Optional("others_formula", default=DEFAULT_BILLING_OTHERS_FORMULA): str,
|
|
203
220
|
vol.Optional("surplus_formula", default=DEFAULT_BILLING_SURPLUS_FORMULA): str,
|
|
221
|
+
vol.Optional("cycle_start_day", default=1): vol.Range(1, 30),
|
|
204
222
|
}
|
|
205
223
|
)
|
|
206
224
|
|
|
@@ -211,9 +229,6 @@ DEFAULT_PVPC_RULES = PricingRules(
|
|
|
211
229
|
market_kw_year_eur=3.113,
|
|
212
230
|
electricity_tax=1.0511300560,
|
|
213
231
|
iva_tax=1.05,
|
|
214
|
-
p1_kwh_eur=None,
|
|
215
|
-
p2_kwh_eur=None,
|
|
216
|
-
p3_kwh_eur=None,
|
|
217
232
|
)
|
|
218
233
|
|
|
219
234
|
|
|
@@ -225,6 +240,10 @@ class ConsumptionAggData(TypedDict):
|
|
|
225
240
|
value_p1_kWh: float
|
|
226
241
|
value_p2_kWh: float
|
|
227
242
|
value_p3_kWh: float
|
|
243
|
+
surplus_kWh: float
|
|
244
|
+
surplus_p1_kWh: float
|
|
245
|
+
surplus_p2_kWh: float
|
|
246
|
+
surplus_p3_kWh: float
|
|
228
247
|
delta_h: float
|
|
229
248
|
|
|
230
249
|
|
|
@@ -235,6 +254,10 @@ ConsumptionAggSchema = vol.Schema(
|
|
|
235
254
|
vol.Required("value_p1_kWh"): vol.Coerce(float),
|
|
236
255
|
vol.Required("value_p2_kWh"): vol.Coerce(float),
|
|
237
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),
|
|
238
261
|
vol.Required("delta_h"): vol.Coerce(float),
|
|
239
262
|
}
|
|
240
263
|
)
|
|
@@ -433,7 +433,14 @@ class EdataHelper:
|
|
|
433
433
|
new_data_from = self._date_from
|
|
434
434
|
|
|
435
435
|
proc = ConsumptionProcessor(
|
|
436
|
-
|
|
436
|
+
{
|
|
437
|
+
"consumptions": [
|
|
438
|
+
x
|
|
439
|
+
for x in self.data["consumptions"]
|
|
440
|
+
if x["datetime"] >= new_data_from
|
|
441
|
+
],
|
|
442
|
+
"cycle_start_day": self.pricing_rules.get("cycle_start_day", 1),
|
|
443
|
+
}
|
|
437
444
|
)
|
|
438
445
|
today_starts = datetime(
|
|
439
446
|
datetime.today().year,
|
|
@@ -472,15 +479,23 @@ class EdataHelper:
|
|
|
472
479
|
self.attributes["yesterday_kWh"] = (
|
|
473
480
|
yday.get("value_kWh", None) if yday is not None else None
|
|
474
481
|
)
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
self.attributes["
|
|
482
|
-
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
|
|
483
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
|
+
|
|
484
499
|
self.attributes["yesterday_hours"] = (
|
|
485
500
|
yday.get("delta_h", None) if yday is not None else None
|
|
486
501
|
)
|
|
@@ -491,6 +506,9 @@ class EdataHelper:
|
|
|
491
506
|
self.attributes["month_kWh"] = (
|
|
492
507
|
month.get("value_kWh", None) if month is not None else None
|
|
493
508
|
)
|
|
509
|
+
self.attributes["month_surplus_kWh"] = (
|
|
510
|
+
month.get("surplus_kWh", None) if month is not None else None
|
|
511
|
+
)
|
|
494
512
|
self.attributes["month_days"] = (
|
|
495
513
|
month.get("delta_h", 0) / 24 if month is not None else None
|
|
496
514
|
)
|
|
@@ -503,15 +521,13 @@ class EdataHelper:
|
|
|
503
521
|
if month is not None
|
|
504
522
|
else None
|
|
505
523
|
)
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
month.get("value_p3_kWh", None) if month is not None else None
|
|
514
|
-
)
|
|
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
|
+
)
|
|
515
531
|
|
|
516
532
|
last_month = utils.get_by_key(
|
|
517
533
|
self.data["consumptions_monthly_sum"],
|
|
@@ -521,6 +537,9 @@ class EdataHelper:
|
|
|
521
537
|
self.attributes["last_month_kWh"] = (
|
|
522
538
|
last_month.get("value_kWh", None) if last_month is not None else None
|
|
523
539
|
)
|
|
540
|
+
self.attributes["last_month_surplus_kWh"] = (
|
|
541
|
+
last_month.get("surplus_kWh", None) if last_month is not None else None
|
|
542
|
+
)
|
|
524
543
|
self.attributes["last_month_days"] = (
|
|
525
544
|
last_month.get("delta_h", 0) / 24 if last_month is not None else None
|
|
526
545
|
)
|
|
@@ -536,15 +555,18 @@ class EdataHelper:
|
|
|
536
555
|
if last_month is not None
|
|
537
556
|
else None
|
|
538
557
|
)
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
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
|
+
)
|
|
548
570
|
|
|
549
571
|
if len(self.data["consumptions"]) > 0:
|
|
550
572
|
self.attributes["last_registered_date"] = self.data["consumptions"][-1][
|
|
@@ -558,21 +580,21 @@ class EdataHelper:
|
|
|
558
580
|
if last_day is not None
|
|
559
581
|
else None
|
|
560
582
|
)
|
|
561
|
-
self.attributes["
|
|
562
|
-
last_day.get("
|
|
563
|
-
if last_day is not None
|
|
564
|
-
else None
|
|
565
|
-
)
|
|
566
|
-
self.attributes["last_registered_day_p2_kWh"] = (
|
|
567
|
-
last_day.get("value_p2_kWh", None)
|
|
568
|
-
if last_day is not None
|
|
569
|
-
else None
|
|
570
|
-
)
|
|
571
|
-
self.attributes["last_registered_day_p3_kWh"] = (
|
|
572
|
-
last_day.get("value_p3_kWh", None)
|
|
583
|
+
self.attributes["last_registered_day_surplus_kWh"] = (
|
|
584
|
+
last_day.get("surplus_kWh", None)
|
|
573
585
|
if last_day is not None
|
|
574
586
|
else None
|
|
575
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
|
+
|
|
576
598
|
self.attributes["last_registered_day_hours"] = (
|
|
577
599
|
last_day.get("delta_h", None) if last_day is not None else None
|
|
578
600
|
)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from abc import ABC, abstractmethod
|
|
4
4
|
from copy import deepcopy
|
|
5
|
-
from typing import Any
|
|
5
|
+
from typing import Any
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class Processor(ABC):
|
|
@@ -10,8 +10,8 @@ class Processor(ABC):
|
|
|
10
10
|
|
|
11
11
|
_LABEL = "Processor"
|
|
12
12
|
|
|
13
|
-
def __init__(self, input_data:
|
|
14
|
-
"""Init method"""
|
|
13
|
+
def __init__(self, input_data: Any, auto: bool = True):
|
|
14
|
+
"""Init method."""
|
|
15
15
|
self._input = deepcopy(input_data)
|
|
16
16
|
self._output = None
|
|
17
17
|
if auto:
|
|
@@ -19,9 +19,9 @@ class Processor(ABC):
|
|
|
19
19
|
|
|
20
20
|
@abstractmethod
|
|
21
21
|
def do_process(self):
|
|
22
|
-
"""The processing method"""
|
|
22
|
+
"""The processing method."""
|
|
23
23
|
|
|
24
24
|
@property
|
|
25
25
|
def output(self):
|
|
26
|
-
"""An output property"""
|
|
26
|
+
"""An output property."""
|
|
27
27
|
return deepcopy(self._output)
|
|
@@ -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
|
|
@@ -20,12 +20,13 @@ from ..definitions import (
|
|
|
20
20
|
)
|
|
21
21
|
from ..processors import utils
|
|
22
22
|
from ..processors.base import Processor
|
|
23
|
+
import contextlib
|
|
23
24
|
|
|
24
25
|
_LOGGER = logging.getLogger(__name__)
|
|
25
26
|
|
|
26
27
|
|
|
27
28
|
class BillingOutput(TypedDict):
|
|
28
|
-
"""A dict holding BillingProcessor output property"""
|
|
29
|
+
"""A dict holding BillingProcessor output property."""
|
|
29
30
|
|
|
30
31
|
hourly: list[PricingAggData]
|
|
31
32
|
daily: list[PricingAggData]
|
|
@@ -33,7 +34,7 @@ class BillingOutput(TypedDict):
|
|
|
33
34
|
|
|
34
35
|
|
|
35
36
|
class BillingInput(TypedDict):
|
|
36
|
-
"""A dict holding BillingProcessor input data"""
|
|
37
|
+
"""A dict holding BillingProcessor input data."""
|
|
37
38
|
|
|
38
39
|
contracts: list[ContractData]
|
|
39
40
|
consumptions: list[ConsumptionData]
|
|
@@ -42,10 +43,10 @@ class BillingInput(TypedDict):
|
|
|
42
43
|
|
|
43
44
|
|
|
44
45
|
class BillingProcessor(Processor):
|
|
45
|
-
"""A billing processor for edata"""
|
|
46
|
+
"""A billing processor for edata."""
|
|
46
47
|
|
|
47
48
|
def do_process(self):
|
|
48
|
-
"""Main method for the BillingProcessor"""
|
|
49
|
+
"""Main method for the BillingProcessor."""
|
|
49
50
|
self._output = BillingOutput(hourly=[], daily=[], monthly=[])
|
|
50
51
|
|
|
51
52
|
_schema = voluptuous.Schema(
|
|
@@ -60,6 +61,8 @@ class BillingProcessor(Processor):
|
|
|
60
61
|
)
|
|
61
62
|
self._input = _schema(self._input)
|
|
62
63
|
|
|
64
|
+
self._cycle_offset = self._input["rules"]["cycle_start_day"] - 1
|
|
65
|
+
|
|
63
66
|
# joint data by datetime
|
|
64
67
|
_data = {
|
|
65
68
|
x["datetime"]: {
|
|
@@ -124,12 +127,23 @@ class BillingProcessor(Processor):
|
|
|
124
127
|
elif tariff == "p3":
|
|
125
128
|
x["surplus_kwh_eur"] = x["surplus_p3_kwh_eur"]
|
|
126
129
|
|
|
130
|
+
_energy_term = 0
|
|
131
|
+
_power_term = 0
|
|
132
|
+
_others_term = 0
|
|
133
|
+
_surplus_term = 0
|
|
134
|
+
|
|
135
|
+
with contextlib.suppress(Exception):
|
|
136
|
+
_energy_term = round(energy_expr(**x), 3)
|
|
137
|
+
_power_term = round(power_expr(**x), 3)
|
|
138
|
+
_others_term = round(others_expr(**x), 3)
|
|
139
|
+
_surplus_term = round(surplus_expr(**x), 3)
|
|
140
|
+
|
|
127
141
|
new_item = PricingAggData(
|
|
128
142
|
datetime=x["datetime"],
|
|
129
|
-
energy_term=
|
|
130
|
-
power_term=
|
|
131
|
-
others_term=
|
|
132
|
-
surplus_term=
|
|
143
|
+
energy_term=_energy_term,
|
|
144
|
+
power_term=_power_term,
|
|
145
|
+
others_term=_others_term,
|
|
146
|
+
surplus_term=_surplus_term,
|
|
133
147
|
value_eur=0,
|
|
134
148
|
delta_h=1,
|
|
135
149
|
)
|
|
@@ -151,7 +165,9 @@ class BillingProcessor(Processor):
|
|
|
151
165
|
for hour in hourly:
|
|
152
166
|
curr_hour_dt: datetime = hour["datetime"]
|
|
153
167
|
curr_day_dt = curr_hour_dt.replace(hour=0, minute=0, second=0)
|
|
154
|
-
curr_month_dt = curr_day_dt.replace(
|
|
168
|
+
curr_month_dt = (curr_day_dt - timedelta(days=self._cycle_offset)).replace(
|
|
169
|
+
day=1
|
|
170
|
+
)
|
|
155
171
|
|
|
156
172
|
if last_day_dt is None or curr_day_dt != last_day_dt:
|
|
157
173
|
self._output["daily"].append(
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Consumption data processors."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from collections.abc import Iterable
|
|
5
|
+
from typing import TypedDict
|
|
6
|
+
from datetime import datetime, timedelta
|
|
7
|
+
|
|
8
|
+
import voluptuous
|
|
9
|
+
|
|
10
|
+
from ..definitions import ConsumptionAggData, ConsumptionSchema
|
|
11
|
+
from . import utils
|
|
12
|
+
from .base import Processor
|
|
13
|
+
|
|
14
|
+
_LOGGER = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class ConsumptionOutput(TypedDict):
|
|
18
|
+
"""A dict holding ConsumptionProcessor output property."""
|
|
19
|
+
|
|
20
|
+
daily: Iterable[ConsumptionAggData]
|
|
21
|
+
monthly: Iterable[ConsumptionAggData]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ConsumptionProcessor(Processor):
|
|
25
|
+
"""A consumptions processor."""
|
|
26
|
+
|
|
27
|
+
def do_process(self):
|
|
28
|
+
"""Calculate daily and monthly consumption stats."""
|
|
29
|
+
|
|
30
|
+
self._output = ConsumptionOutput(daily=[], monthly=[])
|
|
31
|
+
|
|
32
|
+
last_day_dt = None
|
|
33
|
+
last_month_dt = None
|
|
34
|
+
|
|
35
|
+
_schema = voluptuous.Schema(
|
|
36
|
+
{
|
|
37
|
+
voluptuous.Required("consumptions"): [ConsumptionSchema],
|
|
38
|
+
voluptuous.Optional("cycle_start_day", default=1): voluptuous.Range(
|
|
39
|
+
1, 30
|
|
40
|
+
),
|
|
41
|
+
}
|
|
42
|
+
)
|
|
43
|
+
self._input = _schema(self._input)
|
|
44
|
+
|
|
45
|
+
self._cycle_offset = self._input["cycle_start_day"] - 1
|
|
46
|
+
|
|
47
|
+
for consumption in self._input["consumptions"]:
|
|
48
|
+
curr_hour_dt: datetime = consumption["datetime"]
|
|
49
|
+
curr_day_dt = curr_hour_dt.replace(hour=0, minute=0, second=0)
|
|
50
|
+
curr_month_dt = (curr_day_dt - timedelta(days=self._cycle_offset)).replace(
|
|
51
|
+
day=1
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
tariff = utils.get_pvpc_tariff(curr_hour_dt)
|
|
55
|
+
kwh = consumption["value_kWh"]
|
|
56
|
+
surplus_kwh = consumption["surplus_kWh"]
|
|
57
|
+
delta_h = consumption["delta_h"]
|
|
58
|
+
|
|
59
|
+
kwh_by_tariff = [0, 0, 0]
|
|
60
|
+
surplus_kwh_by_tariff = [0, 0, 0]
|
|
61
|
+
|
|
62
|
+
match tariff:
|
|
63
|
+
case "p1":
|
|
64
|
+
kwh_by_tariff[0] = kwh
|
|
65
|
+
surplus_kwh_by_tariff[0] = surplus_kwh
|
|
66
|
+
case "p2":
|
|
67
|
+
kwh_by_tariff[1] = kwh
|
|
68
|
+
surplus_kwh_by_tariff[1] = surplus_kwh
|
|
69
|
+
case "p3":
|
|
70
|
+
kwh_by_tariff[2] = kwh
|
|
71
|
+
surplus_kwh_by_tariff[2] = surplus_kwh
|
|
72
|
+
|
|
73
|
+
if last_day_dt is None or curr_day_dt != last_day_dt:
|
|
74
|
+
self._output["daily"].append(
|
|
75
|
+
ConsumptionAggData(
|
|
76
|
+
datetime=curr_day_dt,
|
|
77
|
+
value_kWh=kwh,
|
|
78
|
+
delta_h=delta_h,
|
|
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],
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
else:
|
|
89
|
+
self._output["daily"][-1]["value_kWh"] += 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]
|
|
97
|
+
self._output["daily"][-1]["delta_h"] += delta_h
|
|
98
|
+
|
|
99
|
+
if last_month_dt is None or curr_month_dt != last_month_dt:
|
|
100
|
+
self._output["monthly"].append(
|
|
101
|
+
ConsumptionAggData(
|
|
102
|
+
datetime=curr_month_dt,
|
|
103
|
+
value_kWh=kwh,
|
|
104
|
+
delta_h=delta_h,
|
|
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],
|
|
112
|
+
)
|
|
113
|
+
)
|
|
114
|
+
else:
|
|
115
|
+
self._output["monthly"][-1]["value_kWh"] += 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
|
+
]
|
|
129
|
+
self._output["monthly"][-1]["delta_h"] += delta_h
|
|
130
|
+
|
|
131
|
+
last_day_dt = curr_day_dt
|
|
132
|
+
last_month_dt = curr_month_dt
|
|
133
|
+
|
|
134
|
+
# Round to two decimals
|
|
135
|
+
for item in self._output:
|
|
136
|
+
for cons in self._output[item]:
|
|
137
|
+
for key in cons:
|
|
138
|
+
if isinstance(cons[key], float):
|
|
139
|
+
cons[key] = round(cons[key], 2)
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
"""Consumption data processors."""
|
|
2
|
-
|
|
3
|
-
import logging
|
|
4
|
-
from collections.abc import Iterable
|
|
5
|
-
from typing import TypedDict
|
|
6
|
-
from datetime import datetime
|
|
7
|
-
|
|
8
|
-
import voluptuous
|
|
9
|
-
|
|
10
|
-
from ..definitions import ConsumptionAggData, ConsumptionSchema
|
|
11
|
-
from . import utils
|
|
12
|
-
from .base import Processor
|
|
13
|
-
|
|
14
|
-
_LOGGER = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
class ConsumptionOutput(TypedDict):
|
|
18
|
-
"""A dict holding ConsumptionProcessor output property."""
|
|
19
|
-
|
|
20
|
-
daily: Iterable[ConsumptionAggData]
|
|
21
|
-
monthly: Iterable[ConsumptionAggData]
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
class ConsumptionProcessor(Processor):
|
|
25
|
-
"""A consumptions processor."""
|
|
26
|
-
|
|
27
|
-
def do_process(self):
|
|
28
|
-
"""Calculate daily and monthly consumption stats."""
|
|
29
|
-
|
|
30
|
-
self._output = ConsumptionOutput(daily=[], monthly=[])
|
|
31
|
-
|
|
32
|
-
last_day_dt = None
|
|
33
|
-
last_month_dt = None
|
|
34
|
-
|
|
35
|
-
_schema = voluptuous.Schema([ConsumptionSchema])
|
|
36
|
-
self._input = _schema(self._input)
|
|
37
|
-
|
|
38
|
-
for consumption in self._input:
|
|
39
|
-
curr_hour_dt: datetime = consumption["datetime"]
|
|
40
|
-
curr_day_dt = curr_hour_dt.replace(hour=0, minute=0, second=0)
|
|
41
|
-
curr_month_dt = curr_day_dt.replace(day=1)
|
|
42
|
-
|
|
43
|
-
tariff = utils.get_pvpc_tariff(curr_hour_dt)
|
|
44
|
-
kwh = consumption["value_kWh"]
|
|
45
|
-
delta_h = consumption["delta_h"]
|
|
46
|
-
|
|
47
|
-
kwh_p1 = 0
|
|
48
|
-
kwh_p2 = 0
|
|
49
|
-
kwh_p3 = 0
|
|
50
|
-
|
|
51
|
-
match tariff:
|
|
52
|
-
case "p1":
|
|
53
|
-
kwh_p1 = kwh
|
|
54
|
-
case "p2":
|
|
55
|
-
kwh_p2 = kwh
|
|
56
|
-
case "p3":
|
|
57
|
-
kwh_p3 = kwh
|
|
58
|
-
|
|
59
|
-
if last_day_dt is None or curr_day_dt != last_day_dt:
|
|
60
|
-
self._output["daily"].append(
|
|
61
|
-
ConsumptionAggData(
|
|
62
|
-
datetime=curr_day_dt,
|
|
63
|
-
value_kWh=kwh,
|
|
64
|
-
delta_h=delta_h,
|
|
65
|
-
value_p1_kWh=kwh_p1,
|
|
66
|
-
value_p2_kWh=kwh_p2,
|
|
67
|
-
value_p3_kWh=kwh_p3,
|
|
68
|
-
)
|
|
69
|
-
)
|
|
70
|
-
else:
|
|
71
|
-
self._output["daily"][-1]["value_kWh"] += kwh
|
|
72
|
-
self._output["daily"][-1]["value_p1_kWh"] += kwh_p1
|
|
73
|
-
self._output["daily"][-1]["value_p2_kWh"] += kwh_p2
|
|
74
|
-
self._output["daily"][-1]["value_p3_kWh"] += kwh_p3
|
|
75
|
-
self._output["daily"][-1]["delta_h"] += delta_h
|
|
76
|
-
|
|
77
|
-
if last_month_dt is None or curr_month_dt != last_month_dt:
|
|
78
|
-
self._output["monthly"].append(
|
|
79
|
-
ConsumptionAggData(
|
|
80
|
-
datetime=curr_month_dt,
|
|
81
|
-
value_kWh=kwh,
|
|
82
|
-
delta_h=delta_h,
|
|
83
|
-
value_p1_kWh=kwh_p1,
|
|
84
|
-
value_p2_kWh=kwh_p2,
|
|
85
|
-
value_p3_kWh=kwh_p3,
|
|
86
|
-
)
|
|
87
|
-
)
|
|
88
|
-
else:
|
|
89
|
-
self._output["monthly"][-1]["value_kWh"] += kwh
|
|
90
|
-
self._output["monthly"][-1]["value_p1_kWh"] += kwh_p1
|
|
91
|
-
self._output["monthly"][-1]["value_p2_kWh"] += kwh_p2
|
|
92
|
-
self._output["monthly"][-1]["value_p3_kWh"] += kwh_p3
|
|
93
|
-
self._output["monthly"][-1]["delta_h"] += delta_h
|
|
94
|
-
|
|
95
|
-
last_day_dt = curr_day_dt
|
|
96
|
-
last_month_dt = curr_month_dt
|
|
97
|
-
|
|
98
|
-
for item in self._output:
|
|
99
|
-
for cons in self._output[item]:
|
|
100
|
-
cons["value_kWh"] = round(cons["value_kWh"], 2)
|
|
101
|
-
cons["value_p1_kWh"] = round(cons["value_p1_kWh"], 2)
|
|
102
|
-
cons["value_p2_kWh"] = round(cons["value_p2_kWh"], 2)
|
|
103
|
-
cons["value_p3_kWh"] = round(cons["value_p3_kWh"], 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
|