e-data 1.2.18__tar.gz → 1.2.20__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 (27) hide show
  1. e_data-1.2.20/PKG-INFO +136 -0
  2. e_data-1.2.20/e_data.egg-info/PKG-INFO +136 -0
  3. {e-data-1.2.18 → e_data-1.2.20}/edata/connectors/datadis.py +88 -85
  4. {e-data-1.2.18 → e_data-1.2.20}/edata/definitions.py +1 -3
  5. {e-data-1.2.18 → e_data-1.2.20}/edata/helpers.py +82 -78
  6. {e-data-1.2.18 → e_data-1.2.20}/setup.py +1 -1
  7. e-data-1.2.18/PKG-INFO +0 -128
  8. e-data-1.2.18/e_data.egg-info/PKG-INFO +0 -128
  9. {e-data-1.2.18 → e_data-1.2.20}/LICENSE +0 -0
  10. {e-data-1.2.18 → e_data-1.2.20}/MANIFEST.in +0 -0
  11. {e-data-1.2.18 → e_data-1.2.20}/README.md +0 -0
  12. {e-data-1.2.18 → e_data-1.2.20}/e_data.egg-info/SOURCES.txt +0 -0
  13. {e-data-1.2.18 → e_data-1.2.20}/e_data.egg-info/dependency_links.txt +0 -0
  14. {e-data-1.2.18 → e_data-1.2.20}/e_data.egg-info/requires.txt +0 -0
  15. {e-data-1.2.18 → e_data-1.2.20}/e_data.egg-info/top_level.txt +0 -0
  16. {e-data-1.2.18 → e_data-1.2.20}/edata/__init__.py +0 -0
  17. {e-data-1.2.18 → e_data-1.2.20}/edata/connectors/__init__.py +0 -0
  18. {e-data-1.2.18 → e_data-1.2.20}/edata/connectors/redata.py +0 -0
  19. {e-data-1.2.18 → e_data-1.2.20}/edata/const.py +0 -0
  20. {e-data-1.2.18 → e_data-1.2.20}/edata/processors/__init__.py +0 -0
  21. {e-data-1.2.18 → e_data-1.2.20}/edata/processors/base.py +0 -0
  22. {e-data-1.2.18 → e_data-1.2.20}/edata/processors/billing.py +0 -0
  23. {e-data-1.2.18 → e_data-1.2.20}/edata/processors/consumption.py +0 -0
  24. {e-data-1.2.18 → e_data-1.2.20}/edata/processors/maximeter.py +0 -0
  25. {e-data-1.2.18 → e_data-1.2.20}/edata/processors/utils.py +0 -0
  26. {e-data-1.2.18 → e_data-1.2.20}/edata/storage.py +0 -0
  27. {e-data-1.2.18 → e_data-1.2.20}/setup.cfg +0 -0
e_data-1.2.20/PKG-INFO ADDED
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.1
2
+ Name: e-data
3
+ Version: 1.2.20
4
+ Summary: Python library for managing spanish energy data from various web providers
5
+ Home-page: https://github.com/uvejota/python-edata
6
+ Author: VMG
7
+ Author-email: vmayorg@outlook.es
8
+ License: GPLv3
9
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.6
13
+ Classifier: Programming Language :: Python :: 3.7
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: Implementation :: CPython
17
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
18
+ Requires-Python: >=3.6.0
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: dateparser>=1.1.2
22
+ Requires-Dist: freezegun>=1.2.1
23
+ Requires-Dist: holidays>=0.14.2
24
+ Requires-Dist: pytest>=7.1.2
25
+ Requires-Dist: python_dateutil>=2.8.2
26
+ Requires-Dist: requests>=2.28.1
27
+ Requires-Dist: voluptuous>=0.13.1
28
+ Requires-Dist: Jinja2>=3.1.2
29
+
30
+
31
+ [![Downloads](https://pepy.tech/badge/e-data)](https://pepy.tech/project/e-data)
32
+ [![Downloads](https://pepy.tech/badge/e-data/month)](https://pepy.tech/project/e-data)
33
+ [![Downloads](https://pepy.tech/badge/e-data/week)](https://pepy.tech/project/e-data)
34
+
35
+ # python-edata
36
+
37
+ Este paquete proporciona herramientas para la descarga de tus datos de consumo eléctrico (desde Datadis.es) y su posterior procesado. La motivación principal es que conocer el consumo puede ayudarnos a reducirlo, e incluso a elegir una tarifa que mejor se adapte a nuestras necesidades. A día de hoy sus capacidades de facturación (€) son limitadas, soporta PVPC (según disponibilidad de datos de REData) y tarificación fija por tramos. Es el corazón de la integración [homeassistant-edata](https://github.com/uvejota/homeassistant-edata).
38
+
39
+ _**Esta herramienta no mantiene ningún tipo de vinculación con los proveedores de datos anteriormente mencionados, simplemente consulta la información disponible y facilita su posterior análisis.**_
40
+
41
+ ## Instalación
42
+
43
+ Puedes instalar la última versión estable mediante:
44
+
45
+ ``` bash
46
+ pip install e-data
47
+ ```
48
+
49
+ Si quieres probar la versión `dev` o contribuir a su desarrollo, clona este repositorio e instala manualmente las dependencias:
50
+
51
+ ``` bash
52
+ pip install -r requirements.txt
53
+ ```
54
+
55
+ ## Estructura
56
+
57
+ El paquete consta de tres módulos diferenciados:
58
+
59
+ * **Conectores** (módulo `connectors`), para definir los métodos de consulta a los diferentes proveedores: Datadis y REData.
60
+ * **Procesadores** (módulo `processors`), para procesar datos de consumo, maxímetro, o coste (tarificación). Ahora mismo consta de tres procesadores: `billing`, `consumption` y `maximeter`, además de algunas utilidades ubicadas en `utils`. Los procesadores deben heredar de la clase Processor definida en `base.py`
61
+ * **Ayudantes** (módulo `helpers`), para ayudar en el uso y gestión de los anteriores, presentando de momento un único ayudante llamado `EdataHelper` que te permite recopilar `X` días de datos (por defecto 365) y automáticamente procesarlos. Los datos son almacenados en la variable `data`, mientras que los atributos autocalculados son almacenados en la variable `attributes`. Por lo general, primero utilizan los conectores y luego procesan los datos, gestionando varias tareas de recuperación (principalmente para Datadis).
62
+
63
+ Estos módulos corresponden a la siguiente estructura del paquete:
64
+
65
+ ```
66
+ edata/
67
+ · __init__.py
68
+ · connectors/
69
+ · __init__.py
70
+ · datadis.py
71
+ · redata.py
72
+ · processors/
73
+ · __init__.py
74
+ · base.py
75
+ · billing.py
76
+ · consumption.py
77
+ · maximeter.py
78
+ · utils.py
79
+ · helpers.py
80
+ ```
81
+
82
+ ## Ejemplo de uso
83
+
84
+ Partimos de que tenemos credenciales en Datadis.es. Algunas aclaraciones:
85
+ * No es necesario solicitar API pública en el registro (se utilizará la API privada habilitada por defecto)
86
+ * El username suele ser el NIF del titular
87
+ * Copie el CUPS de la web de Datadis, algunas comercializadoras adhieren caracteres adicionales en el CUPS mostrado en su factura.
88
+ * La herramienta acepta el uso de NIF autorizado para consultar el suministro de otro titular.
89
+
90
+ ``` python
91
+ from datetime import datetime
92
+ import json
93
+
94
+ # importamos definiciones de datos que nos interesen
95
+ from edata.definitions import PricingRules
96
+ # importamos el ayudante
97
+ from edata.helpers import EdataHelper
98
+ # importamos el procesador de utilidades
99
+ from edata.processors import utils
100
+
101
+ # Preparar reglas de tarificación (si se quiere)
102
+ PRICING_RULES_PVPC = PricingRules(
103
+ p1_kw_year_eur=30.67266,
104
+ p2_kw_year_eur=1.4243591,
105
+ meter_month_eur=0.81,
106
+ market_kw_year_eur=3.113,
107
+ electricity_tax=1.0511300560,
108
+ iva_tax=1.05,
109
+ # podemos rellenar los siguientes campos si quisiéramos precio fijo (y no pvpc)
110
+ p1_kwh_eur=None,
111
+ p2_kwh_eur=None,
112
+ p3_kwh_eur=None,
113
+ )
114
+
115
+ # Instanciar el helper
116
+ # 'authorized_nif' permite indicar el NIF de la persona que nos autoriza a consultar su CUPS.
117
+ # 'data' permite "cargar" al helper datos anteriores (resultado edata.data de una ejecución anterior), para evitar volver a consultar los mismos.
118
+ edata = EdataHelper(
119
+ "datadis_user",
120
+ "datadis_password",
121
+ "cups",
122
+ datadis_authorized_nif=None,
123
+ pricing_rules=PRICING_RULES_PVPC, # si se le pasa None, no aplica tarificación
124
+ data=None, # aquí podríamos cargar datos anteriores
125
+ )
126
+
127
+ # Solicitar actualización de todo el histórico (se almacena en edata.data)
128
+ edata.update(date_from=datetime(1970, 1, 1), date_to=datetime.today())
129
+
130
+ # volcamos todo lo obtenido a un fichero
131
+ with open("backup.json", "w") as file:
132
+ json.dump(utils.serialize_dict(edata.data), file) # se puede utilizar deserialize_dict para la posterior lectura del backup
133
+
134
+ # Imprimir atributos
135
+ print(edata.attributes)
136
+ ```
@@ -0,0 +1,136 @@
1
+ Metadata-Version: 2.1
2
+ Name: e-data
3
+ Version: 1.2.20
4
+ Summary: Python library for managing spanish energy data from various web providers
5
+ Home-page: https://github.com/uvejota/python-edata
6
+ Author: VMG
7
+ Author-email: vmayorg@outlook.es
8
+ License: GPLv3
9
+ Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.6
13
+ Classifier: Programming Language :: Python :: 3.7
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: Implementation :: CPython
17
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
18
+ Requires-Python: >=3.6.0
19
+ Description-Content-Type: text/markdown
20
+ License-File: LICENSE
21
+ Requires-Dist: dateparser>=1.1.2
22
+ Requires-Dist: freezegun>=1.2.1
23
+ Requires-Dist: holidays>=0.14.2
24
+ Requires-Dist: pytest>=7.1.2
25
+ Requires-Dist: python_dateutil>=2.8.2
26
+ Requires-Dist: requests>=2.28.1
27
+ Requires-Dist: voluptuous>=0.13.1
28
+ Requires-Dist: Jinja2>=3.1.2
29
+
30
+
31
+ [![Downloads](https://pepy.tech/badge/e-data)](https://pepy.tech/project/e-data)
32
+ [![Downloads](https://pepy.tech/badge/e-data/month)](https://pepy.tech/project/e-data)
33
+ [![Downloads](https://pepy.tech/badge/e-data/week)](https://pepy.tech/project/e-data)
34
+
35
+ # python-edata
36
+
37
+ Este paquete proporciona herramientas para la descarga de tus datos de consumo eléctrico (desde Datadis.es) y su posterior procesado. La motivación principal es que conocer el consumo puede ayudarnos a reducirlo, e incluso a elegir una tarifa que mejor se adapte a nuestras necesidades. A día de hoy sus capacidades de facturación (€) son limitadas, soporta PVPC (según disponibilidad de datos de REData) y tarificación fija por tramos. Es el corazón de la integración [homeassistant-edata](https://github.com/uvejota/homeassistant-edata).
38
+
39
+ _**Esta herramienta no mantiene ningún tipo de vinculación con los proveedores de datos anteriormente mencionados, simplemente consulta la información disponible y facilita su posterior análisis.**_
40
+
41
+ ## Instalación
42
+
43
+ Puedes instalar la última versión estable mediante:
44
+
45
+ ``` bash
46
+ pip install e-data
47
+ ```
48
+
49
+ Si quieres probar la versión `dev` o contribuir a su desarrollo, clona este repositorio e instala manualmente las dependencias:
50
+
51
+ ``` bash
52
+ pip install -r requirements.txt
53
+ ```
54
+
55
+ ## Estructura
56
+
57
+ El paquete consta de tres módulos diferenciados:
58
+
59
+ * **Conectores** (módulo `connectors`), para definir los métodos de consulta a los diferentes proveedores: Datadis y REData.
60
+ * **Procesadores** (módulo `processors`), para procesar datos de consumo, maxímetro, o coste (tarificación). Ahora mismo consta de tres procesadores: `billing`, `consumption` y `maximeter`, además de algunas utilidades ubicadas en `utils`. Los procesadores deben heredar de la clase Processor definida en `base.py`
61
+ * **Ayudantes** (módulo `helpers`), para ayudar en el uso y gestión de los anteriores, presentando de momento un único ayudante llamado `EdataHelper` que te permite recopilar `X` días de datos (por defecto 365) y automáticamente procesarlos. Los datos son almacenados en la variable `data`, mientras que los atributos autocalculados son almacenados en la variable `attributes`. Por lo general, primero utilizan los conectores y luego procesan los datos, gestionando varias tareas de recuperación (principalmente para Datadis).
62
+
63
+ Estos módulos corresponden a la siguiente estructura del paquete:
64
+
65
+ ```
66
+ edata/
67
+ · __init__.py
68
+ · connectors/
69
+ · __init__.py
70
+ · datadis.py
71
+ · redata.py
72
+ · processors/
73
+ · __init__.py
74
+ · base.py
75
+ · billing.py
76
+ · consumption.py
77
+ · maximeter.py
78
+ · utils.py
79
+ · helpers.py
80
+ ```
81
+
82
+ ## Ejemplo de uso
83
+
84
+ Partimos de que tenemos credenciales en Datadis.es. Algunas aclaraciones:
85
+ * No es necesario solicitar API pública en el registro (se utilizará la API privada habilitada por defecto)
86
+ * El username suele ser el NIF del titular
87
+ * Copie el CUPS de la web de Datadis, algunas comercializadoras adhieren caracteres adicionales en el CUPS mostrado en su factura.
88
+ * La herramienta acepta el uso de NIF autorizado para consultar el suministro de otro titular.
89
+
90
+ ``` python
91
+ from datetime import datetime
92
+ import json
93
+
94
+ # importamos definiciones de datos que nos interesen
95
+ from edata.definitions import PricingRules
96
+ # importamos el ayudante
97
+ from edata.helpers import EdataHelper
98
+ # importamos el procesador de utilidades
99
+ from edata.processors import utils
100
+
101
+ # Preparar reglas de tarificación (si se quiere)
102
+ PRICING_RULES_PVPC = PricingRules(
103
+ p1_kw_year_eur=30.67266,
104
+ p2_kw_year_eur=1.4243591,
105
+ meter_month_eur=0.81,
106
+ market_kw_year_eur=3.113,
107
+ electricity_tax=1.0511300560,
108
+ iva_tax=1.05,
109
+ # podemos rellenar los siguientes campos si quisiéramos precio fijo (y no pvpc)
110
+ p1_kwh_eur=None,
111
+ p2_kwh_eur=None,
112
+ p3_kwh_eur=None,
113
+ )
114
+
115
+ # Instanciar el helper
116
+ # 'authorized_nif' permite indicar el NIF de la persona que nos autoriza a consultar su CUPS.
117
+ # 'data' permite "cargar" al helper datos anteriores (resultado edata.data de una ejecución anterior), para evitar volver a consultar los mismos.
118
+ edata = EdataHelper(
119
+ "datadis_user",
120
+ "datadis_password",
121
+ "cups",
122
+ datadis_authorized_nif=None,
123
+ pricing_rules=PRICING_RULES_PVPC, # si se le pasa None, no aplica tarificación
124
+ data=None, # aquí podríamos cargar datos anteriores
125
+ )
126
+
127
+ # Solicitar actualización de todo el histórico (se almacena en edata.data)
128
+ edata.update(date_from=datetime(1970, 1, 1), date_to=datetime.today())
129
+
130
+ # volcamos todo lo obtenido a un fichero
131
+ with open("backup.json", "w") as file:
132
+ json.dump(utils.serialize_dict(edata.data), file) # se puede utilizar deserialize_dict para la posterior lectura del backup
133
+
134
+ # Imprimir atributos
135
+ print(edata.attributes)
136
+ ```
@@ -7,17 +7,17 @@ There a few issues that are workarounded:
7
7
  """
8
8
 
9
9
  import contextlib
10
+ from datetime import datetime, timedelta
11
+ import glob
10
12
  import hashlib
11
13
  import json
12
14
  import logging
13
- from datetime import datetime, timedelta
14
15
  import os
15
- from dateutil.relativedelta import relativedelta
16
+ import tempfile
16
17
 
18
+ from dateutil.relativedelta import relativedelta
17
19
  import requests
18
20
 
19
- from filelock import FileLock
20
-
21
21
  from ..definitions import ConsumptionData, ContractData, MaxPowerData, SupplyData
22
22
  from ..processors import utils
23
23
 
@@ -68,10 +68,15 @@ TIMEOUT = 3 * 60 # requests timeout
68
68
  QUERY_LIMIT = timedelta(hours=24) # a datadis limitation, again...
69
69
 
70
70
  # Cache-related constants
71
- RECENT_QUERIES_FILENAME = "edata_recent_queries.json"
72
- RECENT_QUERIES_CACHE_FILENAME = "edata_recent_queries_cache.json"
73
- DEFAULT_RECENT_QUERIES_FILE = f"/tmp/{RECENT_QUERIES_FILENAME}"
74
- DEFAULT_RECENT_QUERIES_CACHE = f"/tmp/{RECENT_QUERIES_CACHE_FILENAME}"
71
+ RECENT_CACHE_SUBDIR = "cache"
72
+
73
+
74
+ def migrate_storage(storage_dir):
75
+ """Migrate storage from older versions."""
76
+
77
+ with contextlib.suppress(FileNotFoundError):
78
+ os.remove(storage_dir, "edata_recent_queries.json")
79
+ os.remove(storage_dir, "edata_recent_queries_cache.json")
75
80
 
76
81
 
77
82
  class DatadisConnector:
@@ -94,84 +99,69 @@ class DatadisConnector:
94
99
  self._smart_fetch = enable_smart_fetch
95
100
  self._recent_queries = {}
96
101
  self._recent_cache = {}
102
+ self._warned_queries = []
97
103
  if storage_path is not None:
98
- self._recent_queries_file = os.path.join(
99
- storage_path, RECENT_QUERIES_FILENAME
100
- )
101
- self._recent_queries_cache_file = os.path.join(
102
- storage_path, RECENT_QUERIES_CACHE_FILENAME
103
- )
104
+ self._recent_cache_dir = os.path.join(storage_path, RECENT_CACHE_SUBDIR)
105
+ migrate_storage(storage_path)
104
106
  else:
105
- self._recent_queries_file = DEFAULT_RECENT_QUERIES_FILE
106
- self._recent_queries_cache_file = DEFAULT_RECENT_QUERIES_CACHE
107
- self._warned_queries = []
108
- self._load_recent_cache()
109
-
110
- def _load_recent_cache(self) -> None:
111
- """Load caches."""
112
-
113
- with contextlib.suppress(FileNotFoundError):
114
- with open(self._recent_queries_file, encoding="utf8") as dst_file:
115
- recent_queries = json.load(dst_file)
116
- for query in recent_queries:
117
- recent_queries[query] = datetime.fromisoformat(
118
- recent_queries[query]
119
- )
120
- self._recent_queries.update(recent_queries)
107
+ self._recent_cache_dir = os.path.join(
108
+ tempfile.gettempdir(), RECENT_CACHE_SUBDIR
109
+ )
121
110
 
122
- with open(self._recent_queries_cache_file, encoding="utf8") as dst_file:
123
- self._recent_cache.update(json.load(dst_file))
111
+ os.makedirs(self._recent_cache_dir, exist_ok=True)
124
112
 
125
113
  def _update_recent_queries(self, query: str, data: dict | None = None) -> None:
126
114
  """Cache a successful query to avoid exceeding query limits."""
127
115
 
128
- with FileLock(self._recent_queries_file + ".lock"):
129
- self._load_recent_cache()
130
- # identify the query by a md5 hash
131
- hash_query = hashlib.md5(query.encode()).hexdigest()
132
- # prepare key (hash) and values (timestamp and response)
133
- self._recent_queries[hash_query] = datetime.now()
134
- if data is not None:
135
- self._recent_cache[hash_query] = data
136
-
137
- # purge old cache
138
- to_delete = [
139
- x
140
- for x in self._recent_queries
141
- if (datetime.now() - self._recent_queries[x]) > QUERY_LIMIT
142
- ]
143
-
144
- for key in to_delete:
145
- with contextlib.suppress(KeyError):
146
- self._recent_queries.pop(key, None)
147
- self._recent_cache.pop(key, None)
148
-
149
- # dump current cache to disk
150
- try:
151
- with open(self._recent_queries_file, "w", encoding="utf8") as dst_file:
152
- json.dump(utils.serialize_dict(self._recent_queries), dst_file)
153
- if data is not None:
154
- with open(
155
- self._recent_queries_cache_file, "w", encoding="utf8"
156
- ) as dst_file:
157
- json.dump(self._recent_cache, dst_file)
158
- _LOGGER.info("Local cache updated")
159
- except Exception as e:
160
- _LOGGER.warning("Unknown error while updating cache: %s", e)
116
+ # identify the query by a md5 hash
117
+ hash_query = hashlib.md5(query.encode()).hexdigest()
118
+
119
+ # remove expired cache files
120
+ with contextlib.suppress(FileNotFoundError):
121
+ for cache_file in glob.glob(os.path.join(self._recent_cache_dir, "*")):
122
+ if (
123
+ datetime.now()
124
+ - datetime.fromtimestamp(os.path.getmtime(cache_file))
125
+ ) > QUERY_LIMIT:
126
+ _LOGGER.info("Removing cache item '%s'", cache_file)
127
+ os.remove(cache_file)
128
+
129
+ # dump current cache to disk
130
+ try:
131
+ with open(
132
+ os.path.join(self._recent_cache_dir, hash_query),
133
+ "w",
134
+ encoding="utf8",
135
+ ) as dst_file:
136
+ json.dump(data, dst_file)
137
+ _LOGGER.info("Updating cache item '%s'", hash_query)
138
+
139
+ except Exception as e:
140
+ _LOGGER.warning("Unknown error while updating cache: %s", e)
161
141
 
162
142
  def _is_recent_query(self, query: str) -> bool:
163
143
  """Check if a query has been done recently to avoid exceeding query limits."""
144
+
164
145
  hash_query = hashlib.md5(query.encode()).hexdigest()
146
+ cache_file = os.path.join(self._recent_cache_dir, hash_query)
165
147
 
166
- if hash_query in self._recent_queries:
167
- return (datetime.now() - self._recent_queries[hash_query]) < QUERY_LIMIT
168
- return False
148
+ return (
149
+ os.path.exists(cache_file)
150
+ and (datetime.now() - datetime.fromtimestamp(os.path.getmtime(cache_file)))
151
+ < QUERY_LIMIT
152
+ )
169
153
 
170
- def _get_cache_for_query(self, query: str) -> dict:
154
+ def _get_cache_for_query(self, query: str) -> dict | None:
171
155
  """Return cached response for a query."""
156
+
172
157
  hash_query = hashlib.md5(query.encode()).hexdigest()
173
- _LOGGER.info("Serving '%s' from cache", query)
174
- return self._recent_cache.get(hash_query, None)
158
+ cache_file = os.path.join(self._recent_cache_dir, hash_query)
159
+
160
+ try:
161
+ with open(cache_file, encoding="utf8") as cache:
162
+ return json.load(cache)
163
+ except FileNotFoundError:
164
+ return None
175
165
 
176
166
  def _get_token(self):
177
167
  """Private method that fetches a new token if needed."""
@@ -227,39 +217,52 @@ class DatadisConnector:
227
217
  key = param
228
218
  value = data[param]
229
219
  params = params + f"{key}={value}&"
220
+ anonym_params = "?" if len(data) > 0 else ""
221
+
222
+ # build anonymized params for logging
223
+ for anonym_param in data:
224
+ key = anonym_param
225
+ if key == "cups":
226
+ value = "xxxx" + data[anonym_param][-5:]
227
+ elif key == "authorizedNif":
228
+ value = "xxxx"
229
+ else:
230
+ value = data[anonym_param]
231
+ anonym_params = anonym_params + f"{key}={value}&"
230
232
 
231
233
  # check if query is already in cache
232
234
  if not ignore_recent_queries and self._is_recent_query(url + params):
233
235
  _cache = self._get_cache_for_query(url + params)
234
236
  if _cache is not None:
237
+ _LOGGER.info("Returning cached data")
235
238
  return _cache
236
239
  return []
237
240
 
238
241
  # run the query
239
242
  try:
240
- _LOGGER.debug("GET %s", url + params)
243
+ _LOGGER.info("GET %s", url + anonym_params)
241
244
  reply = self._session.get(
242
245
  url + params,
243
246
  headers={"Accept-Encoding": "identity"},
244
247
  timeout=TIMEOUT,
245
248
  )
246
249
  except requests.exceptions.Timeout:
247
- _LOGGER.warning("Timeout at %s", url + params)
250
+ _LOGGER.warning("Timeout at %s", url + anonym_params)
248
251
  return []
249
252
 
250
253
  # eval response
251
254
  if reply.status_code == 200:
252
255
  # we're here if reply seems valid
253
- _LOGGER.info("Got 200 OK at %s", url + params)
256
+ _LOGGER.info("Got 200 OK")
254
257
  if reply.json():
255
258
  response = reply.json()
256
- self._update_recent_queries(url + params, response)
259
+ if not ignore_recent_queries:
260
+ self._update_recent_queries(url + params, response)
257
261
  else:
258
262
  # this mostly happens when datadis provides an empty response
259
- _LOGGER.info(
260
- "Datadis returned an empty response at %s", url + params
261
- )
262
- self._update_recent_queries(url + params)
263
+ _LOGGER.info("Got an empty response")
264
+ if not ignore_recent_queries:
265
+ self._update_recent_queries(url + params)
263
266
  elif reply.status_code == 401 and not refresh_token:
264
267
  # we're here if we were unauthorized so we will refresh the token
265
268
  response = self._get(
@@ -271,24 +274,24 @@ class DatadisConnector:
271
274
  elif reply.status_code == 429:
272
275
  # we're here if we exceeded datadis API rates (24h)
273
276
  _LOGGER.warning(
274
- "%s %s at %s",
277
+ "Got status code '%s' with message '%s'",
275
278
  reply.status_code,
276
279
  reply.text,
277
- url + params,
278
280
  )
279
- self._update_recent_queries(url + params)
281
+ if not ignore_recent_queries:
282
+ self._update_recent_queries(url + params)
280
283
  elif is_retry:
281
284
  # otherwise, if this was a retried request... warn the user
282
285
  if (url + params) not in self._warned_queries:
283
286
  _LOGGER.warning(
284
- "%s %s at %s. %s. %s",
287
+ "Got status code '%s' with message '%s'. %s. %s",
285
288
  reply.status_code,
286
289
  reply.text,
287
- url + params,
288
290
  "Query temporary disabled",
289
291
  "Future 500 code errors for this query will be silenced until restart",
290
292
  )
291
- self._update_recent_queries(url + params)
293
+ if not ignore_recent_queries:
294
+ self._update_recent_queries(url + params)
292
295
  self._warned_queries.append(url + params)
293
296
  else:
294
297
  # finally, retry since an unexpected error took place (mostly 500 errors - server fault)
@@ -71,9 +71,7 @@ DEFAULT_BILLING_SURPLUS_FORMULA = (
71
71
 
72
72
  # Sum energy and power terms, and substract surplus until 0.
73
73
  # An alternative would be "[(energy_term + power_term - surplus_term), 0]|max + others_term"
74
- DEFAULT_BILLING_MAIN_FORMULA = (
75
- "[(energy_term - surplus_term), 0]|max + power_term + others_term"
76
- )
74
+ DEFAULT_BILLING_MAIN_FORMULA = "energy_term + power_term + others_term"
77
75
 
78
76
 
79
77
  class SupplyData(TypedDict):
@@ -22,6 +22,12 @@ from .storage import check_storage_integrity, dump_storage, load_storage
22
22
  _LOGGER = logging.getLogger(__name__)
23
23
 
24
24
 
25
+ def acups(cups):
26
+ """Print an abbreviated and anonymized CUPS."""
27
+
28
+ return cups[-5:]
29
+
30
+
25
31
  class EdataHelper:
26
32
  """Main EdataHelper class."""
27
33
 
@@ -53,7 +59,7 @@ class EdataHelper:
53
59
  self.attributes = {}
54
60
  self._storage_dir = storage_dir_path
55
61
  self._cups = cups
56
- self._scups = cups[-5:]
62
+ self._scups = acups(cups)
57
63
  self._authorized_nif = datadis_authorized_nif
58
64
  self.last_update = {x: datetime(1970, 1, 1) for x in self.data}
59
65
  self._date_from = datetime(1970, 1, 1)
@@ -113,7 +119,7 @@ class EdataHelper:
113
119
  date_to: datetime = datetime.today(),
114
120
  incremental_update: bool = True,
115
121
  ):
116
- """Synchronous update."""
122
+ """Update synchronously."""
117
123
 
118
124
  _LOGGER.info(
119
125
  "%s: update triggered",
@@ -138,9 +144,9 @@ class EdataHelper:
138
144
  dump_storage(self._cups, self.data, self._storage_dir)
139
145
 
140
146
  def update_supplies(self):
141
- """Synchronous data update of supplies."""
147
+ """Update supplies."""
142
148
 
143
- _LOGGER.info("%s: supplies update triggered", self._scups)
149
+ _LOGGER.debug("%s: supplies update triggered", self._scups)
144
150
  if datetime.today().date() != self.last_update["supplies"].date():
145
151
  # if supplies haven't been updated today
146
152
  supplies = self.datadis_api.get_supplies(
@@ -155,9 +161,9 @@ class EdataHelper:
155
161
  _LOGGER.info("%s: supplies are already updated (skipping)", self._scups)
156
162
 
157
163
  def update_contracts(self, cups: str, distributor_code: str):
158
- """Synchronous data update of contracts."""
164
+ """Update contracts."""
159
165
 
160
- _LOGGER.info("%s: contracts update triggered", self._scups)
166
+ _LOGGER.debug("%s: contracts update triggered", self._scups)
161
167
  if datetime.today().date() != self.last_update["contracts"].date():
162
168
  # if contracts haven't been updated today
163
169
  contracts = self.datadis_api.get_contract_detail(
@@ -182,9 +188,9 @@ class EdataHelper:
182
188
  measurement_type: str,
183
189
  point_type: int,
184
190
  ):
185
- """Synchronous data update of consumptions."""
191
+ """Update consumptions."""
186
192
 
187
- _LOGGER.info("%s: consumptions update triggered", self._scups)
193
+ _LOGGER.debug("%s: consumptions update triggered", self._scups)
188
194
  if (datetime.now() - self.last_update["consumptions"]) > self.UPDATE_INTERVAL:
189
195
  consumptions = self.datadis_api.get_consumption_data(
190
196
  cups,
@@ -196,22 +202,23 @@ class EdataHelper:
196
202
  authorized_nif=self._authorized_nif,
197
203
  )
198
204
  if len(consumptions) > 0:
205
+ _LOGGER.info(
206
+ "%s: got consumptions from %s to %s",
207
+ self._scups,
208
+ consumptions[0]["datetime"].isoformat(),
209
+ consumptions[-1]["datetime"].isoformat(),
210
+ )
199
211
  self.data["consumptions"] = utils.extend_by_key(
200
212
  self.data["consumptions"], consumptions, "datetime"
201
213
  )
202
214
  self.last_update["consumptions"] = datetime.now()
203
- _LOGGER.info(
204
- "%s: consumptions update succeeded (%s new records)",
205
- self._scups,
206
- len(consumptions),
207
- )
208
215
  else:
209
216
  _LOGGER.info("%s: consumptions are already updated (skipping)", self._scups)
210
217
 
211
218
  def update_maximeter(self, cups, distributor_code, start_date, end_date):
212
- """Synchronous data update of maximeter."""
219
+ """Update maximeter."""
213
220
 
214
- _LOGGER.info("%s: maximeter update triggered", self._scups)
221
+ _LOGGER.debug("%s: maximeter update triggered", self._scups)
215
222
  if (datetime.now() - self.last_update["maximeter"]) > self.UPDATE_INTERVAL:
216
223
  maximeter = self.datadis_api.get_max_power(
217
224
  cups,
@@ -221,15 +228,14 @@ class EdataHelper:
221
228
  authorized_nif=self._authorized_nif,
222
229
  )
223
230
  if len(maximeter) > 0:
231
+ _LOGGER.info(
232
+ "%s: maximeter update succeeded",
233
+ self._scups,
234
+ )
224
235
  self.data["maximeter"] = utils.extend_by_key(
225
236
  self.data["maximeter"], maximeter, "datetime"
226
237
  )
227
238
  self.last_update["maximeter"] = datetime.now()
228
- _LOGGER.info(
229
- "%s: maximeter update succeeded (%s new records)",
230
- self._scups,
231
- len(maximeter),
232
- )
233
239
  else:
234
240
  _LOGGER.info("%s: maximeter is already updated (skipping)", self._scups)
235
241
 
@@ -239,7 +245,7 @@ class EdataHelper:
239
245
  date_from: datetime = datetime(1970, 1, 1),
240
246
  date_to: datetime = datetime.today(),
241
247
  ):
242
- """Synchronous data update."""
248
+ """Update all data from Datadis."""
243
249
 
244
250
  _LOGGER.info(
245
251
  "%s: datadis update triggered (from %s to %s)",
@@ -264,15 +270,25 @@ class EdataHelper:
264
270
  if supply is None:
265
271
  # return if specified cups seems not valid
266
272
  _LOGGER.error(
267
- "%s: CUPS not found, please check configured CUPS", self._scups
273
+ "%s: CUPS not found. Got: %s",
274
+ self._scups,
275
+ [acups(x["cups"]) for x in self.data["supplies"]],
268
276
  )
269
277
  return False
278
+ _LOGGER.info("%s: CUPS found in account", self._scups)
270
279
 
271
280
  # get some supply-related data
272
281
  supply_date_start = supply["date_start"]
273
282
  distributor_code = supply["distributorCode"]
274
283
  point_type = supply["pointType"]
275
284
 
285
+ _LOGGER.info(
286
+ "%s: CUPS start date is %s", self._scups, supply_date_start.isoformat()
287
+ )
288
+ _LOGGER.info(
289
+ "%s: CUPS end date is %s", self._scups, supply["date_end"].isoformat()
290
+ )
291
+
276
292
  # update contracts to get valid periods
277
293
  self.update_contracts(cups, distributor_code)
278
294
  if len(self.data["contracts"]) == 0:
@@ -280,9 +296,9 @@ class EdataHelper:
280
296
  "%s: contracts update failed or no contracts found in the provided account",
281
297
  self._scups,
282
298
  )
283
- return False
299
+ # return False
284
300
 
285
- # filter consumptions and maximeter, and look for gaps
301
+ # filter consumptions and maximeter, and log gaps
286
302
  def sort_and_filter(dt_from, dt_to):
287
303
  self.data["consumptions"], miss_cons = utils.extract_dt_ranges(
288
304
  self.data["consumptions"],
@@ -300,82 +316,70 @@ class EdataHelper:
300
316
 
301
317
  miss_cons, miss_maxim = sort_and_filter(date_from, date_to)
302
318
 
319
+ # update consumptions
303
320
  _LOGGER.info(
304
321
  "%s: missing consumptions: %s",
305
322
  self._scups,
306
- ", ".join(
307
- [x["from"].isoformat() + " - " + x["to"].isoformat() for x in miss_cons]
308
- ),
309
- )
310
- _LOGGER.info(
311
- "%s: missing maximeter: %s",
312
- self._scups,
313
323
  ", ".join(
314
324
  [
315
- x["from"].isoformat() + " - " + x["to"].isoformat()
316
- for x in miss_maxim
325
+ "from "
326
+ + (x["from"] + timedelta(hours=1)).isoformat()
327
+ + " to "
328
+ + x["to"].isoformat()
329
+ for x in miss_cons
317
330
  ]
318
331
  ),
319
332
  )
320
-
321
- oldest_contract = datetime.today()
322
- for contract in self.data["contracts"]:
323
- # register oldest contract
324
- if contract["date_start"] < oldest_contract:
325
- oldest_contract = contract["date_start"]
326
-
327
- # update consumptions
328
- for gap in [
329
- x
330
- for x in miss_cons
331
- if not (
332
- x["to"] < contract["date_start"] or x["from"] > contract["date_end"]
333
- )
334
- ]:
333
+ for gap in miss_cons:
334
+ if not (
335
+ gap["to"] < supply["date_start"] or gap["from"] > supply["date_end"]
336
+ ):
335
337
  # fetch consumptions for each consumptions gap in valid periods
338
+ start = max([gap["from"] + timedelta(hours=1), supply["date_start"]])
339
+ end = min([gap["to"], supply["date_end"]])
340
+ _LOGGER.info(
341
+ "%s: requesting consumptions from %s to %s",
342
+ self._scups,
343
+ start.isoformat(),
344
+ end.isoformat(),
345
+ )
336
346
  self.update_consumptions(
337
347
  cups,
338
348
  distributor_code,
339
- max([gap["from"] + timedelta(hours=1), contract["date_start"]]),
340
- min([gap["to"], contract["date_end"]]),
349
+ start,
350
+ end,
341
351
  "0",
342
352
  point_type,
343
353
  )
344
354
 
345
- # update maximeter
346
- for gap in [
347
- x
348
- for x in miss_maxim
349
- if not (
350
- date_to < contract["date_start"] or date_from > contract["date_end"]
351
- )
352
- ]:
355
+ # update maximeter
356
+ _LOGGER.info(
357
+ "%s: missing maximeter: %s",
358
+ self._scups,
359
+ ", ".join(
360
+ [
361
+ "from " + x["from"].isoformat() + " to " + x["to"].isoformat()
362
+ for x in miss_maxim
363
+ ]
364
+ ),
365
+ )
366
+ for gap in miss_maxim:
367
+ if not (date_to < supply["date_start"] or date_from > supply["date_end"]):
353
368
  # fetch maximeter for each maximeter gap in valid periods
354
369
  start = max(
355
- [gap["from"], contract["date_start"] + relativedelta(months=1)]
370
+ [gap["from"], supply["date_start"] + relativedelta(months=1)]
356
371
  )
357
- end = min([gap["to"], contract["date_end"]])
372
+ end = min([gap["to"], supply["date_end"]])
358
373
  start = min([start, end])
374
+ _LOGGER.info(
375
+ "%s: requesting maximeter from %s to %s",
376
+ self._scups,
377
+ start.isoformat(),
378
+ end.isoformat(),
379
+ )
359
380
  self.update_maximeter(cups, distributor_code, start, end)
360
381
 
361
- # safe check periods in non-registered contracts
362
- if oldest_contract != supply_date_start and oldest_contract > max(
363
- [date_from, supply_date_start]
364
- ):
365
- _LOGGER.info(
366
- "%s: supplies and contract start date do not match, exploring non-registered contracts",
367
- self._scups,
368
- )
369
- start = max([supply_date_start, date_from])
370
- self.update_consumptions(
371
- cups, distributor_code, start, oldest_contract, "0", point_type
372
- )
373
- self.update_maximeter(cups, distributor_code, start, oldest_contract)
374
- miss_cons, miss_maxim = sort_and_filter(start, date_to)
375
- else:
376
- miss_cons, miss_maxim = sort_and_filter(
377
- max([date_from, oldest_contract]), date_to
378
- )
382
+ miss_cons, miss_maxim = sort_and_filter(date_from, date_to)
379
383
 
380
384
  return True
381
385
 
@@ -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.18"
23
+ VERSION = "1.2.20"
24
24
 
25
25
  # What packages are required for this module to be executed?
26
26
  REQUIRED = [
e-data-1.2.18/PKG-INFO DELETED
@@ -1,128 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: e-data
3
- Version: 1.2.18
4
- Summary: Python library for managing spanish energy data from various web providers
5
- Home-page: https://github.com/uvejota/python-edata
6
- Author: VMG
7
- Author-email: vmayorg@outlook.es
8
- License: GPLv3
9
- Description:
10
- [![Downloads](https://pepy.tech/badge/e-data)](https://pepy.tech/project/e-data)
11
- [![Downloads](https://pepy.tech/badge/e-data/month)](https://pepy.tech/project/e-data)
12
- [![Downloads](https://pepy.tech/badge/e-data/week)](https://pepy.tech/project/e-data)
13
-
14
- # python-edata
15
-
16
- Este paquete proporciona herramientas para la descarga de tus datos de consumo eléctrico (desde Datadis.es) y su posterior procesado. La motivación principal es que conocer el consumo puede ayudarnos a reducirlo, e incluso a elegir una tarifa que mejor se adapte a nuestras necesidades. A día de hoy sus capacidades de facturación (€) son limitadas, soporta PVPC (según disponibilidad de datos de REData) y tarificación fija por tramos. Es el corazón de la integración [homeassistant-edata](https://github.com/uvejota/homeassistant-edata).
17
-
18
- _**Esta herramienta no mantiene ningún tipo de vinculación con los proveedores de datos anteriormente mencionados, simplemente consulta la información disponible y facilita su posterior análisis.**_
19
-
20
- ## Instalación
21
-
22
- Puedes instalar la última versión estable mediante:
23
-
24
- ``` bash
25
- pip install e-data
26
- ```
27
-
28
- Si quieres probar la versión `dev` o contribuir a su desarrollo, clona este repositorio e instala manualmente las dependencias:
29
-
30
- ``` bash
31
- pip install -r requirements.txt
32
- ```
33
-
34
- ## Estructura
35
-
36
- El paquete consta de tres módulos diferenciados:
37
-
38
- * **Conectores** (módulo `connectors`), para definir los métodos de consulta a los diferentes proveedores: Datadis y REData.
39
- * **Procesadores** (módulo `processors`), para procesar datos de consumo, maxímetro, o coste (tarificación). Ahora mismo consta de tres procesadores: `billing`, `consumption` y `maximeter`, además de algunas utilidades ubicadas en `utils`. Los procesadores deben heredar de la clase Processor definida en `base.py`
40
- * **Ayudantes** (módulo `helpers`), para ayudar en el uso y gestión de los anteriores, presentando de momento un único ayudante llamado `EdataHelper` que te permite recopilar `X` días de datos (por defecto 365) y automáticamente procesarlos. Los datos son almacenados en la variable `data`, mientras que los atributos autocalculados son almacenados en la variable `attributes`. Por lo general, primero utilizan los conectores y luego procesan los datos, gestionando varias tareas de recuperación (principalmente para Datadis).
41
-
42
- Estos módulos corresponden a la siguiente estructura del paquete:
43
-
44
- ```
45
- edata/
46
- · __init__.py
47
- · connectors/
48
- · __init__.py
49
- · datadis.py
50
- · redata.py
51
- · processors/
52
- · __init__.py
53
- · base.py
54
- · billing.py
55
- · consumption.py
56
- · maximeter.py
57
- · utils.py
58
- · helpers.py
59
- ```
60
-
61
- ## Ejemplo de uso
62
-
63
- Partimos de que tenemos credenciales en Datadis.es. Algunas aclaraciones:
64
- * No es necesario solicitar API pública en el registro (se utilizará la API privada habilitada por defecto)
65
- * El username suele ser el NIF del titular
66
- * Copie el CUPS de la web de Datadis, algunas comercializadoras adhieren caracteres adicionales en el CUPS mostrado en su factura.
67
- * La herramienta acepta el uso de NIF autorizado para consultar el suministro de otro titular.
68
-
69
- ``` python
70
- from datetime import datetime
71
- import json
72
-
73
- # importamos definiciones de datos que nos interesen
74
- from edata.definitions import PricingRules
75
- # importamos el ayudante
76
- from edata.helpers import EdataHelper
77
- # importamos el procesador de utilidades
78
- from edata.processors import utils
79
-
80
- # Preparar reglas de tarificación (si se quiere)
81
- PRICING_RULES_PVPC = PricingRules(
82
- p1_kw_year_eur=30.67266,
83
- p2_kw_year_eur=1.4243591,
84
- meter_month_eur=0.81,
85
- market_kw_year_eur=3.113,
86
- electricity_tax=1.0511300560,
87
- iva_tax=1.05,
88
- # podemos rellenar los siguientes campos si quisiéramos precio fijo (y no pvpc)
89
- p1_kwh_eur=None,
90
- p2_kwh_eur=None,
91
- p3_kwh_eur=None,
92
- )
93
-
94
- # Instanciar el helper
95
- # 'authorized_nif' permite indicar el NIF de la persona que nos autoriza a consultar su CUPS.
96
- # 'data' permite "cargar" al helper datos anteriores (resultado edata.data de una ejecución anterior), para evitar volver a consultar los mismos.
97
- edata = EdataHelper(
98
- "datadis_user",
99
- "datadis_password",
100
- "cups",
101
- datadis_authorized_nif=None,
102
- pricing_rules=PRICING_RULES_PVPC, # si se le pasa None, no aplica tarificación
103
- data=None, # aquí podríamos cargar datos anteriores
104
- )
105
-
106
- # Solicitar actualización de todo el histórico (se almacena en edata.data)
107
- edata.update(date_from=datetime(1970, 1, 1), date_to=datetime.today())
108
-
109
- # volcamos todo lo obtenido a un fichero
110
- with open("backup.json", "w") as file:
111
- json.dump(utils.serialize_dict(edata.data), file) # se puede utilizar deserialize_dict para la posterior lectura del backup
112
-
113
- # Imprimir atributos
114
- print(edata.attributes)
115
- ```
116
-
117
- Platform: UNKNOWN
118
- Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
119
- Classifier: Programming Language :: Python
120
- Classifier: Programming Language :: Python :: 3
121
- Classifier: Programming Language :: Python :: 3.6
122
- Classifier: Programming Language :: Python :: 3.7
123
- Classifier: Programming Language :: Python :: 3.8
124
- Classifier: Programming Language :: Python :: 3.9
125
- Classifier: Programming Language :: Python :: Implementation :: CPython
126
- Classifier: Programming Language :: Python :: Implementation :: PyPy
127
- Requires-Python: >=3.6.0
128
- Description-Content-Type: text/markdown
@@ -1,128 +0,0 @@
1
- Metadata-Version: 2.1
2
- Name: e-data
3
- Version: 1.2.18
4
- Summary: Python library for managing spanish energy data from various web providers
5
- Home-page: https://github.com/uvejota/python-edata
6
- Author: VMG
7
- Author-email: vmayorg@outlook.es
8
- License: GPLv3
9
- Description:
10
- [![Downloads](https://pepy.tech/badge/e-data)](https://pepy.tech/project/e-data)
11
- [![Downloads](https://pepy.tech/badge/e-data/month)](https://pepy.tech/project/e-data)
12
- [![Downloads](https://pepy.tech/badge/e-data/week)](https://pepy.tech/project/e-data)
13
-
14
- # python-edata
15
-
16
- Este paquete proporciona herramientas para la descarga de tus datos de consumo eléctrico (desde Datadis.es) y su posterior procesado. La motivación principal es que conocer el consumo puede ayudarnos a reducirlo, e incluso a elegir una tarifa que mejor se adapte a nuestras necesidades. A día de hoy sus capacidades de facturación (€) son limitadas, soporta PVPC (según disponibilidad de datos de REData) y tarificación fija por tramos. Es el corazón de la integración [homeassistant-edata](https://github.com/uvejota/homeassistant-edata).
17
-
18
- _**Esta herramienta no mantiene ningún tipo de vinculación con los proveedores de datos anteriormente mencionados, simplemente consulta la información disponible y facilita su posterior análisis.**_
19
-
20
- ## Instalación
21
-
22
- Puedes instalar la última versión estable mediante:
23
-
24
- ``` bash
25
- pip install e-data
26
- ```
27
-
28
- Si quieres probar la versión `dev` o contribuir a su desarrollo, clona este repositorio e instala manualmente las dependencias:
29
-
30
- ``` bash
31
- pip install -r requirements.txt
32
- ```
33
-
34
- ## Estructura
35
-
36
- El paquete consta de tres módulos diferenciados:
37
-
38
- * **Conectores** (módulo `connectors`), para definir los métodos de consulta a los diferentes proveedores: Datadis y REData.
39
- * **Procesadores** (módulo `processors`), para procesar datos de consumo, maxímetro, o coste (tarificación). Ahora mismo consta de tres procesadores: `billing`, `consumption` y `maximeter`, además de algunas utilidades ubicadas en `utils`. Los procesadores deben heredar de la clase Processor definida en `base.py`
40
- * **Ayudantes** (módulo `helpers`), para ayudar en el uso y gestión de los anteriores, presentando de momento un único ayudante llamado `EdataHelper` que te permite recopilar `X` días de datos (por defecto 365) y automáticamente procesarlos. Los datos son almacenados en la variable `data`, mientras que los atributos autocalculados son almacenados en la variable `attributes`. Por lo general, primero utilizan los conectores y luego procesan los datos, gestionando varias tareas de recuperación (principalmente para Datadis).
41
-
42
- Estos módulos corresponden a la siguiente estructura del paquete:
43
-
44
- ```
45
- edata/
46
- · __init__.py
47
- · connectors/
48
- · __init__.py
49
- · datadis.py
50
- · redata.py
51
- · processors/
52
- · __init__.py
53
- · base.py
54
- · billing.py
55
- · consumption.py
56
- · maximeter.py
57
- · utils.py
58
- · helpers.py
59
- ```
60
-
61
- ## Ejemplo de uso
62
-
63
- Partimos de que tenemos credenciales en Datadis.es. Algunas aclaraciones:
64
- * No es necesario solicitar API pública en el registro (se utilizará la API privada habilitada por defecto)
65
- * El username suele ser el NIF del titular
66
- * Copie el CUPS de la web de Datadis, algunas comercializadoras adhieren caracteres adicionales en el CUPS mostrado en su factura.
67
- * La herramienta acepta el uso de NIF autorizado para consultar el suministro de otro titular.
68
-
69
- ``` python
70
- from datetime import datetime
71
- import json
72
-
73
- # importamos definiciones de datos que nos interesen
74
- from edata.definitions import PricingRules
75
- # importamos el ayudante
76
- from edata.helpers import EdataHelper
77
- # importamos el procesador de utilidades
78
- from edata.processors import utils
79
-
80
- # Preparar reglas de tarificación (si se quiere)
81
- PRICING_RULES_PVPC = PricingRules(
82
- p1_kw_year_eur=30.67266,
83
- p2_kw_year_eur=1.4243591,
84
- meter_month_eur=0.81,
85
- market_kw_year_eur=3.113,
86
- electricity_tax=1.0511300560,
87
- iva_tax=1.05,
88
- # podemos rellenar los siguientes campos si quisiéramos precio fijo (y no pvpc)
89
- p1_kwh_eur=None,
90
- p2_kwh_eur=None,
91
- p3_kwh_eur=None,
92
- )
93
-
94
- # Instanciar el helper
95
- # 'authorized_nif' permite indicar el NIF de la persona que nos autoriza a consultar su CUPS.
96
- # 'data' permite "cargar" al helper datos anteriores (resultado edata.data de una ejecución anterior), para evitar volver a consultar los mismos.
97
- edata = EdataHelper(
98
- "datadis_user",
99
- "datadis_password",
100
- "cups",
101
- datadis_authorized_nif=None,
102
- pricing_rules=PRICING_RULES_PVPC, # si se le pasa None, no aplica tarificación
103
- data=None, # aquí podríamos cargar datos anteriores
104
- )
105
-
106
- # Solicitar actualización de todo el histórico (se almacena en edata.data)
107
- edata.update(date_from=datetime(1970, 1, 1), date_to=datetime.today())
108
-
109
- # volcamos todo lo obtenido a un fichero
110
- with open("backup.json", "w") as file:
111
- json.dump(utils.serialize_dict(edata.data), file) # se puede utilizar deserialize_dict para la posterior lectura del backup
112
-
113
- # Imprimir atributos
114
- print(edata.attributes)
115
- ```
116
-
117
- Platform: UNKNOWN
118
- Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
119
- Classifier: Programming Language :: Python
120
- Classifier: Programming Language :: Python :: 3
121
- Classifier: Programming Language :: Python :: 3.6
122
- Classifier: Programming Language :: Python :: 3.7
123
- Classifier: Programming Language :: Python :: 3.8
124
- Classifier: Programming Language :: Python :: 3.9
125
- Classifier: Programming Language :: Python :: Implementation :: CPython
126
- Classifier: Programming Language :: Python :: Implementation :: PyPy
127
- Requires-Python: >=3.6.0
128
- Description-Content-Type: text/markdown
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes