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.
Files changed (25) hide show
  1. {e-data-1.2.7/e_data.egg-info → e-data-1.2.8}/PKG-INFO +1 -1
  2. {e-data-1.2.7 → e-data-1.2.8/e_data.egg-info}/PKG-INFO +1 -1
  3. {e-data-1.2.7 → e-data-1.2.8}/edata/connectors/datadis.py +124 -47
  4. {e-data-1.2.7 → e-data-1.2.8}/edata/definitions.py +25 -1
  5. {e-data-1.2.7 → e-data-1.2.8}/edata/helpers.py +53 -38
  6. {e-data-1.2.7 → e-data-1.2.8}/edata/processors/billing.py +5 -5
  7. {e-data-1.2.7 → e-data-1.2.8}/edata/processors/consumption.py +47 -22
  8. {e-data-1.2.7 → e-data-1.2.8}/setup.py +1 -1
  9. {e-data-1.2.7 → e-data-1.2.8}/LICENSE +0 -0
  10. {e-data-1.2.7 → e-data-1.2.8}/MANIFEST.in +0 -0
  11. {e-data-1.2.7 → e-data-1.2.8}/README.md +0 -0
  12. {e-data-1.2.7 → e-data-1.2.8}/e_data.egg-info/SOURCES.txt +0 -0
  13. {e-data-1.2.7 → e-data-1.2.8}/e_data.egg-info/dependency_links.txt +0 -0
  14. {e-data-1.2.7 → e-data-1.2.8}/e_data.egg-info/requires.txt +0 -0
  15. {e-data-1.2.7 → e-data-1.2.8}/e_data.egg-info/top_level.txt +0 -0
  16. {e-data-1.2.7 → e-data-1.2.8}/edata/__init__.py +0 -0
  17. {e-data-1.2.7 → e-data-1.2.8}/edata/connectors/__init__.py +0 -0
  18. {e-data-1.2.7 → e-data-1.2.8}/edata/connectors/redata.py +0 -0
  19. {e-data-1.2.7 → e-data-1.2.8}/edata/const.py +0 -0
  20. {e-data-1.2.7 → e-data-1.2.8}/edata/processors/__init__.py +0 -0
  21. {e-data-1.2.7 → e-data-1.2.8}/edata/processors/base.py +0 -0
  22. {e-data-1.2.7 → e-data-1.2.8}/edata/processors/maximeter.py +0 -0
  23. {e-data-1.2.7 → e-data-1.2.8}/edata/processors/utils.py +0 -0
  24. {e-data-1.2.7 → e-data-1.2.8}/edata/storage.py +0 -0
  25. {e-data-1.2.7 → e-data-1.2.8}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: e-data
3
- Version: 1.2.7
3
+ Version: 1.2.8
4
4
  Summary: Python library for managing spanish energy data from various web providers
5
5
  Home-page: https://github.com/uvejota/python-edata
6
6
  Author: VMG
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: e-data
3
- Version: 1.2.7
3
+ Version: 1.2.8
4
4
  Summary: Python library for managing spanish energy data from various web providers
5
5
  Home-page: https://github.com/uvejota/python-edata
6
6
  Author: VMG
@@ -1,5 +1,13 @@
1
- """Definitions for API connectors."""
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
- TIMEOUT = 3 * 60
46
-
47
- MAX_CONSUMPTIONS_MONTHS = 1
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
- RECENT_QUERIES_FILE = f"/tmp/{RECENT_QUERIES_FILENAME}"
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
- """Init method."""
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
- # TODO FIX this is not working after migration
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 = 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
- try:
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
- except FileNotFoundError:
91
- self._recent_queries = {}
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
- """Record a recent successful query to avoid exceeding query limits."""
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 queries
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
- self._recent_queries.pop(key, None)
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
- except Exception:
112
- pass
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 _send_cmd(
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
- """Common method for GET requests."""
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
- return response
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.info("GET %s", url + params)
184
- reply = self._session.get(url + params, timeout=TIMEOUT)
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 response
238
+ return []
188
239
 
189
240
  # eval response
190
- if reply.status_code == 200 and reply.json():
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
- response = reply.json()
193
- self._update_recent_queries(url + params)
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
- response = self._send_cmd(
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
- self._send_cmd(url, request_data, is_retry=True)
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
- response = self._send_cmd(
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._send_cmd(
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._send_cmd(URL_GET_CONSUMPTION_DATA, request_data=data)
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._send_cmd(URL_GET_MAX_POWER, request_data=data)
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.Union(vol.Coerce(float), None),
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
- self.attributes["yesterday_p1_kWh"] = (
483
- yday.get("value_p1_kWh", None) if yday is not None else None
484
- )
485
- self.attributes["yesterday_p2_kWh"] = (
486
- yday.get("value_p2_kWh", None) if yday is not None else None
487
- )
488
- self.attributes["yesterday_p3_kWh"] = (
489
- yday.get("value_p3_kWh", None) if yday is not None else None
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
- self.attributes["month_p1_kWh"] = (
514
- month.get("value_p1_kWh", None) if month is not None else None
515
- )
516
- self.attributes["month_p2_kWh"] = (
517
- month.get("value_p2_kWh", None) if month is not None else None
518
- )
519
- self.attributes["month_p3_kWh"] = (
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
- self.attributes["last_month_p1_kWh"] = (
547
- last_month.get("value_p1_kWh", None) if last_month is not None else None
548
- )
549
- self.attributes["last_month_p2_kWh"] = (
550
- last_month.get("value_p2_kWh", None) if last_month is not None else None
551
- )
552
- self.attributes["last_month_p3_kWh"] = (
553
- last_month.get("value_p3_kWh", None) if last_month is not None else None
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["last_registered_day_p1_kWh"] = (
569
- last_day.get("value_p1_kWh", None)
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
- kwh_p1 = 0
59
- kwh_p2 = 0
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
- kwh_p1 = kwh
64
+ kwh_by_tariff[0] = kwh
65
+ surplus_kwh_by_tariff[0] = surplus_kwh
65
66
  case "p2":
66
- kwh_p2 = kwh
67
+ kwh_by_tariff[1] = kwh
68
+ surplus_kwh_by_tariff[1] = surplus_kwh
67
69
  case "p3":
68
- kwh_p3 = kwh
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=kwh_p1,
77
- value_p2_kWh=kwh_p2,
78
- value_p3_kWh=kwh_p3,
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"] += kwh_p1
84
- self._output["daily"][-1]["value_p2_kWh"] += kwh_p2
85
- self._output["daily"][-1]["value_p3_kWh"] += kwh_p3
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=kwh_p1,
95
- value_p2_kWh=kwh_p2,
96
- value_p3_kWh=kwh_p3,
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"] += kwh_p1
102
- self._output["monthly"][-1]["value_p2_kWh"] += kwh_p2
103
- self._output["monthly"][-1]["value_p3_kWh"] += kwh_p3
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
- cons["value_kWh"] = round(cons["value_kWh"], 2)
112
- cons["value_p1_kWh"] = round(cons["value_p1_kWh"], 2)
113
- cons["value_p2_kWh"] = round(cons["value_p2_kWh"], 2)
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)
@@ -20,7 +20,7 @@ URL = "https://github.com/uvejota/python-edata"
20
20
  EMAIL = "vmayorg@outlook.es"
21
21
  AUTHOR = "VMG"
22
22
  REQUIRES_PYTHON = ">=3.6.0"
23
- VERSION = "1.2.7"
23
+ VERSION = "1.2.8"
24
24
 
25
25
  # What packages are required for this module to be executed?
26
26
  REQUIRED = [
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