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.
- e_data-1.2.20/PKG-INFO +136 -0
- e_data-1.2.20/e_data.egg-info/PKG-INFO +136 -0
- {e-data-1.2.18 → e_data-1.2.20}/edata/connectors/datadis.py +88 -85
- {e-data-1.2.18 → e_data-1.2.20}/edata/definitions.py +1 -3
- {e-data-1.2.18 → e_data-1.2.20}/edata/helpers.py +82 -78
- {e-data-1.2.18 → e_data-1.2.20}/setup.py +1 -1
- e-data-1.2.18/PKG-INFO +0 -128
- e-data-1.2.18/e_data.egg-info/PKG-INFO +0 -128
- {e-data-1.2.18 → e_data-1.2.20}/LICENSE +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/MANIFEST.in +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/README.md +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/e_data.egg-info/SOURCES.txt +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/e_data.egg-info/dependency_links.txt +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/e_data.egg-info/requires.txt +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/e_data.egg-info/top_level.txt +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/edata/__init__.py +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/edata/connectors/__init__.py +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/edata/connectors/redata.py +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/edata/const.py +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/edata/processors/__init__.py +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/edata/processors/base.py +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/edata/processors/billing.py +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/edata/processors/consumption.py +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/edata/processors/maximeter.py +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/edata/processors/utils.py +0 -0
- {e-data-1.2.18 → e_data-1.2.20}/edata/storage.py +0 -0
- {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
|
+
[](https://pepy.tech/project/e-data)
|
|
32
|
+
[](https://pepy.tech/project/e-data)
|
|
33
|
+
[](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
|
+
[](https://pepy.tech/project/e-data)
|
|
32
|
+
[](https://pepy.tech/project/e-data)
|
|
33
|
+
[](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
|
-
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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.
|
|
99
|
-
|
|
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.
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
|
|
174
|
-
|
|
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.
|
|
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 +
|
|
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
|
|
256
|
+
_LOGGER.info("Got 200 OK")
|
|
254
257
|
if reply.json():
|
|
255
258
|
response = reply.json()
|
|
256
|
-
|
|
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
|
-
|
|
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
|
-
"
|
|
277
|
+
"Got status code '%s' with message '%s'",
|
|
275
278
|
reply.status_code,
|
|
276
279
|
reply.text,
|
|
277
|
-
url + params,
|
|
278
280
|
)
|
|
279
|
-
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
|
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
|
-
"""
|
|
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
|
-
"""
|
|
147
|
+
"""Update supplies."""
|
|
142
148
|
|
|
143
|
-
_LOGGER.
|
|
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
|
-
"""
|
|
164
|
+
"""Update contracts."""
|
|
159
165
|
|
|
160
|
-
_LOGGER.
|
|
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
|
-
"""
|
|
191
|
+
"""Update consumptions."""
|
|
186
192
|
|
|
187
|
-
_LOGGER.
|
|
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
|
-
"""
|
|
219
|
+
"""Update maximeter."""
|
|
213
220
|
|
|
214
|
-
_LOGGER.
|
|
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
|
-
"""
|
|
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
|
|
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
|
|
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
|
-
|
|
316
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
340
|
-
|
|
349
|
+
start,
|
|
350
|
+
end,
|
|
341
351
|
"0",
|
|
342
352
|
point_type,
|
|
343
353
|
)
|
|
344
354
|
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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"],
|
|
370
|
+
[gap["from"], supply["date_start"] + relativedelta(months=1)]
|
|
356
371
|
)
|
|
357
|
-
end = min([gap["to"],
|
|
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
|
-
|
|
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
|
|
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
|
-
[](https://pepy.tech/project/e-data)
|
|
11
|
-
[](https://pepy.tech/project/e-data)
|
|
12
|
-
[](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
|
-
[](https://pepy.tech/project/e-data)
|
|
11
|
-
[](https://pepy.tech/project/e-data)
|
|
12
|
-
[](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
|
|
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
|