meteocat 0.1.34 → 0.1.36
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.
- package/CHANGELOG.md +25 -0
- package/custom_components/meteocat/__init__.py +39 -4
- package/custom_components/meteocat/coordinator.py +234 -8
- package/custom_components/meteocat/manifest.json +1 -1
- package/custom_components/meteocat/options_flow.py +3 -3
- package/custom_components/meteocat/sensor.py +71 -4
- package/custom_components/meteocat/strings.json +4 -1
- package/custom_components/meteocat/translations/ca.json +4 -1
- package/custom_components/meteocat/translations/en.json +4 -1
- package/custom_components/meteocat/translations/es.json +4 -1
- package/custom_components/meteocat/version.py +1 -1
- package/package.json +1 -1
- package/poetry.lock +15 -15
- package/pyproject.toml +5 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
## [0.1.36](https://github.com/figorr/meteocat/compare/v0.1.35...v0.1.36) (2024-12-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* 0.1.36 ([73da4ed](https://github.com/figorr/meteocat/commit/73da4ed7c598a150528294e89c93407e33abfd24))
|
|
7
|
+
* add hour attribute to UVI sensor ([a365626](https://github.com/figorr/meteocat/commit/a365626aa2bb585ca9a835afcec6cea99e85a4bd))
|
|
8
|
+
* add hour attribute translation ([d50f8bb](https://github.com/figorr/meteocat/commit/d50f8bbbcb3c8867dedcbe51f065951f5dfab817))
|
|
9
|
+
* fix self. _config.entry from deprecated self. config.entry ([ed4bfeb](https://github.com/figorr/meteocat/commit/ed4bfebf0ea45bc63de000a21f6063a0cb9e499a))
|
|
10
|
+
* ignore uvi test ([cf35867](https://github.com/figorr/meteocat/commit/cf358675b1afce3e1ea1650414a22c2372af4b49))
|
|
11
|
+
* set coordinators to uvi sensor ([38288cc](https://github.com/figorr/meteocat/commit/38288cc75a30a1ad77a492dcb2b15592379e774f))
|
|
12
|
+
* set uvi coordinators ([1ea0432](https://github.com/figorr/meteocat/commit/1ea0432d6749cac96d45e52d3cf18e7a83e59739))
|
|
13
|
+
* update devs ([9274984](https://github.com/figorr/meteocat/commit/9274984154facd81bdeaf0e0050c870a08996b10))
|
|
14
|
+
* update devs ([44d7699](https://github.com/figorr/meteocat/commit/44d7699c4d3bde13f6145a5380eafc5c77818c45))
|
|
15
|
+
* use cached data when API failed ([f31bd10](https://github.com/figorr/meteocat/commit/f31bd10539e77f7d3e12bc27b3bf88cc9ab317d2))
|
|
16
|
+
* uvi sensor ([ee0b194](https://github.com/figorr/meteocat/commit/ee0b19461bdfacc26d493f9af0e27729e45ae545))
|
|
17
|
+
|
|
18
|
+
## [0.1.35](https://github.com/figorr/meteocat/compare/v0.1.34...v0.1.35) (2024-12-14)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Bug Fixes
|
|
22
|
+
|
|
23
|
+
* 0.1.35 ([87adbc5](https://github.com/figorr/meteocat/commit/87adbc5c76941af1ff837f63920ca65164df38c2))
|
|
24
|
+
* fix station data json name ([2c411bd](https://github.com/figorr/meteocat/commit/2c411bd5d040d8c50bd00dae3c4b7858c049e522))
|
|
25
|
+
|
|
1
26
|
## [0.1.34](https://github.com/figorr/meteocat/compare/v0.1.33...v0.1.34) (2024-12-14)
|
|
2
27
|
|
|
3
28
|
|
|
@@ -8,13 +8,19 @@ from homeassistant.core import HomeAssistant
|
|
|
8
8
|
from homeassistant.exceptions import HomeAssistantError
|
|
9
9
|
from homeassistant.helpers.entity_platform import async_get_platforms
|
|
10
10
|
|
|
11
|
-
from .coordinator import
|
|
11
|
+
from .coordinator import (
|
|
12
|
+
MeteocatSensorCoordinator,
|
|
13
|
+
# MeteocatEntityCoordinator,
|
|
14
|
+
MeteocatUviCoordinator,
|
|
15
|
+
MeteocatUviFileCoordinator,
|
|
16
|
+
)
|
|
17
|
+
|
|
12
18
|
from .const import DOMAIN, PLATFORMS
|
|
13
19
|
|
|
14
20
|
_LOGGER = logging.getLogger(__name__)
|
|
15
21
|
|
|
16
22
|
# Versión
|
|
17
|
-
__version__ = "0.1.
|
|
23
|
+
__version__ = "0.1.36"
|
|
18
24
|
|
|
19
25
|
def safe_remove(path: Path, is_folder: bool = False):
|
|
20
26
|
"""Elimina de forma segura un archivo o carpeta si existe."""
|
|
@@ -62,6 +68,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
|
62
68
|
|
|
63
69
|
# entity_coordinator = MeteocatEntityCoordinator(hass=hass, entry_data=entry_data)
|
|
64
70
|
# await entity_coordinator.async_config_entry_first_refresh()
|
|
71
|
+
|
|
72
|
+
uvi_coordinator = MeteocatUviCoordinator(hass=hass, entry_data=entry_data)
|
|
73
|
+
await uvi_coordinator.async_config_entry_first_refresh()
|
|
74
|
+
|
|
75
|
+
uvi_file_coordinator = MeteocatUviFileCoordinator(hass=hass, entry_data=entry_data)
|
|
76
|
+
await uvi_file_coordinator.async_config_entry_first_refresh()
|
|
77
|
+
|
|
65
78
|
except Exception as err: # Capturar todos los errores
|
|
66
79
|
_LOGGER.exception(f"Error al inicializar los coordinadores: {err}")
|
|
67
80
|
return False
|
|
@@ -71,6 +84,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
|
71
84
|
hass.data[DOMAIN][entry.entry_id] = {
|
|
72
85
|
"sensor_coordinator": sensor_coordinator,
|
|
73
86
|
# "entity_coordinator": entity_coordinator,
|
|
87
|
+
"uvi_coordinator": uvi_coordinator,
|
|
88
|
+
"uvi_file_coordinator": uvi_file_coordinator,
|
|
74
89
|
**entry_data,
|
|
75
90
|
}
|
|
76
91
|
|
|
@@ -96,13 +111,32 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|
|
96
111
|
"""Limpia cualquier dato adicional al desinstalar la integración."""
|
|
97
112
|
_LOGGER.info(f"Eliminando datos residuales de la integración: {entry.entry_id}")
|
|
98
113
|
|
|
99
|
-
# Definir las rutas
|
|
114
|
+
# Definir las rutas base a eliminar
|
|
100
115
|
custom_components_path = Path(hass.config.path("custom_components")) / DOMAIN
|
|
101
116
|
assets_folder = custom_components_path / "assets"
|
|
102
117
|
files_folder = custom_components_path / "files"
|
|
118
|
+
|
|
119
|
+
# Definir archivos relacionados a eliminar
|
|
103
120
|
symbols_file = assets_folder / "symbols.json"
|
|
104
121
|
variables_file = assets_folder / "variables.json"
|
|
105
|
-
|
|
122
|
+
|
|
123
|
+
# Obtener el `station_id` para identificar el archivo a eliminar
|
|
124
|
+
station_id = entry.data.get("station_id")
|
|
125
|
+
if not station_id:
|
|
126
|
+
_LOGGER.warning("No se encontró 'station_id' en la configuración. No se puede eliminar el archivo de datos de la estación.")
|
|
127
|
+
return
|
|
128
|
+
|
|
129
|
+
# Archivo JSON de la estación
|
|
130
|
+
station_data_file = files_folder / f"station_{station_id.lower()}_data.json"
|
|
131
|
+
|
|
132
|
+
# Obtener el `town_id` para identificar el archivo a eliminar
|
|
133
|
+
town_id = entry.data.get("town_id")
|
|
134
|
+
if not town_id:
|
|
135
|
+
_LOGGER.warning("No se encontró 'town_id' en la configuración. No se puede eliminar el archivo de datos de la estación.")
|
|
136
|
+
return
|
|
137
|
+
|
|
138
|
+
# Archivo JSON UVI del municipio
|
|
139
|
+
town_data_file = files_folder / f"uvi_{town_id.lower()}_data.json"
|
|
106
140
|
|
|
107
141
|
# Validar la ruta base
|
|
108
142
|
if not custom_components_path.exists():
|
|
@@ -113,5 +147,6 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|
|
113
147
|
safe_remove(symbols_file)
|
|
114
148
|
safe_remove(variables_file)
|
|
115
149
|
safe_remove(station_data_file)
|
|
150
|
+
safe_remove(town_data_file)
|
|
116
151
|
safe_remove(assets_folder, is_folder=True)
|
|
117
152
|
safe_remove(files_folder, is_folder=True)
|
|
@@ -5,7 +5,7 @@ import json
|
|
|
5
5
|
import aiofiles
|
|
6
6
|
import logging
|
|
7
7
|
import asyncio
|
|
8
|
-
from datetime import timedelta
|
|
8
|
+
from datetime import datetime, timedelta
|
|
9
9
|
from typing import Dict, Any
|
|
10
10
|
|
|
11
11
|
from homeassistant.core import HomeAssistant
|
|
@@ -13,7 +13,9 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
|
13
13
|
from homeassistant.exceptions import ConfigEntryNotReady
|
|
14
14
|
|
|
15
15
|
from meteocatpy.data import MeteocatStationData
|
|
16
|
+
from meteocatpy.uvi import MeteocatUviData
|
|
16
17
|
# from meteocatpy.forecast import MeteocatForecast
|
|
18
|
+
|
|
17
19
|
from meteocatpy.exceptions import (
|
|
18
20
|
BadRequestError,
|
|
19
21
|
ForbiddenError,
|
|
@@ -28,10 +30,14 @@ _LOGGER = logging.getLogger(__name__)
|
|
|
28
30
|
|
|
29
31
|
# Valores predeterminados para los intervalos de actualización
|
|
30
32
|
DEFAULT_SENSOR_UPDATE_INTERVAL = timedelta(minutes=90)
|
|
31
|
-
DEFAULT_ENTITY_UPDATE_INTERVAL = timedelta(hours=
|
|
33
|
+
DEFAULT_ENTITY_UPDATE_INTERVAL = timedelta(hours=48)
|
|
34
|
+
DEFAULT_HOURLY_FORECAST_UPDATE_INTERVAL = timedelta(hours=24)
|
|
35
|
+
DEFAULT_DAYLY_FORECAST_UPDATE_INTERVAL = timedelta(hours=48)
|
|
36
|
+
DEFAULT_UVI_UPDATE_INTERVAL = timedelta(hours=48)
|
|
37
|
+
DEFAULT_UVI_SENSOR_UPDATE_INTERVAL = timedelta(minutes=5)
|
|
32
38
|
|
|
33
39
|
async def save_json_to_file(data: dict, output_file: str) -> None:
|
|
34
|
-
"""
|
|
40
|
+
"""Guarda datos JSON en un archivo de forma asíncrona."""
|
|
35
41
|
try:
|
|
36
42
|
# Crea el directorio si no existe
|
|
37
43
|
os.makedirs(os.path.dirname(output_file), exist_ok=True)
|
|
@@ -40,7 +46,17 @@ async def save_json_to_file(data: dict, output_file: str) -> None:
|
|
|
40
46
|
async with aiofiles.open(output_file, mode="w", encoding="utf-8") as f:
|
|
41
47
|
await f.write(json.dumps(data, indent=4, ensure_ascii=False))
|
|
42
48
|
except Exception as e:
|
|
43
|
-
raise RuntimeError(f"Error
|
|
49
|
+
raise RuntimeError(f"Error guardando JSON to {output_file}: {e}")
|
|
50
|
+
|
|
51
|
+
def load_json_from_file(input_file: str) -> dict | list | None:
|
|
52
|
+
"""Carga datos JSON desde un archivo de forma síncrona."""
|
|
53
|
+
try:
|
|
54
|
+
if os.path.exists(input_file):
|
|
55
|
+
with open(input_file, "r", encoding="utf-8") as f:
|
|
56
|
+
return json.load(f)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
_LOGGER.error(f"Error cargando JSON desde {input_file}: {e}")
|
|
59
|
+
return None
|
|
44
60
|
|
|
45
61
|
class MeteocatSensorCoordinator(DataUpdateCoordinator):
|
|
46
62
|
"""Coordinator para manejar la actualización de datos de los sensores."""
|
|
@@ -96,7 +112,11 @@ class MeteocatSensorCoordinator(DataUpdateCoordinator):
|
|
|
96
112
|
|
|
97
113
|
# Determinar la ruta al archivo en la carpeta raíz del repositorio
|
|
98
114
|
output_file = os.path.join(
|
|
99
|
-
self.hass.config.path(),
|
|
115
|
+
self.hass.config.path(),
|
|
116
|
+
"custom_components",
|
|
117
|
+
"meteocat",
|
|
118
|
+
"files",
|
|
119
|
+
f"station_{self.station_id.lower()}_data.json"
|
|
100
120
|
)
|
|
101
121
|
|
|
102
122
|
# Guardar los datos en un archivo JSON
|
|
@@ -128,12 +148,218 @@ class MeteocatSensorCoordinator(DataUpdateCoordinator):
|
|
|
128
148
|
)
|
|
129
149
|
raise
|
|
130
150
|
except Exception as err:
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
151
|
+
if isinstance(err, ConfigEntryNotReady):
|
|
152
|
+
# El dispositivo no pudo inicializarse por primera vez
|
|
153
|
+
_LOGGER.exception(
|
|
154
|
+
"No se pudo inicializar el dispositivo (Station ID: %s) debido a un error: %s",
|
|
155
|
+
self.station_id,
|
|
156
|
+
err,
|
|
157
|
+
)
|
|
158
|
+
raise # Re-raise the exception to indicate a fundamental failure in initialization
|
|
159
|
+
else:
|
|
160
|
+
# Manejar error durante la actualización de datos
|
|
161
|
+
_LOGGER.exception(
|
|
162
|
+
"Error inesperado al obtener datos de los sensores para (Station ID: %s): %s",
|
|
163
|
+
self.station_id,
|
|
164
|
+
err,
|
|
165
|
+
)
|
|
166
|
+
# Intentar cargar datos en caché si hay un error
|
|
167
|
+
cached_data = load_json_from_file(output_file)
|
|
168
|
+
if cached_data:
|
|
169
|
+
_LOGGER.info("Usando datos en caché para la estación %s.", self.station_id)
|
|
170
|
+
return cached_data
|
|
171
|
+
# No se puede actualizar el estado, retornar None o un estado fallido
|
|
172
|
+
return None # o cualquier otro valor que indique un estado de error
|
|
173
|
+
|
|
174
|
+
class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
175
|
+
"""Coordinator para manejar la actualización de datos de los sensores."""
|
|
176
|
+
|
|
177
|
+
def __init__(
|
|
178
|
+
self,
|
|
179
|
+
hass: HomeAssistant,
|
|
180
|
+
entry_data: dict,
|
|
181
|
+
update_interval: timedelta = DEFAULT_UVI_UPDATE_INTERVAL,
|
|
182
|
+
):
|
|
183
|
+
"""
|
|
184
|
+
Inicializa el coordinador del sensor del Índice UV de Meteocat.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
hass (HomeAssistant): Instancia de Home Assistant.
|
|
188
|
+
entry_data (dict): Datos de configuración obtenidos de core.config_entries.
|
|
189
|
+
update_interval (timedelta): Intervalo de actualización.
|
|
190
|
+
"""
|
|
191
|
+
self.api_key = entry_data["api_key"] # Usamos la API key de la configuración
|
|
192
|
+
self.town_id = entry_data["town_id"] # Usamos el ID del municipio
|
|
193
|
+
self.meteocat_uvi_data = MeteocatUviData(self.api_key)
|
|
194
|
+
|
|
195
|
+
super().__init__(
|
|
196
|
+
hass,
|
|
197
|
+
_LOGGER,
|
|
198
|
+
name=f"{DOMAIN} Uvi Coordinator",
|
|
199
|
+
update_interval=update_interval,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
async def _async_update_data(self) -> Dict:
|
|
203
|
+
"""Actualiza los datos de los sensores desde la API de Meteocat."""
|
|
204
|
+
try:
|
|
205
|
+
# Obtener datos desde la API con manejo de tiempo límite
|
|
206
|
+
data = await asyncio.wait_for(
|
|
207
|
+
self.meteocat_uvi_data.get_uvi_index(self.town_id),
|
|
208
|
+
timeout=30 # Tiempo límite de 30 segundos
|
|
209
|
+
)
|
|
210
|
+
_LOGGER.debug("Datos de sensores actualizados exitosamente: %s", data)
|
|
211
|
+
|
|
212
|
+
# Validar que los datos sean un dict con una clave 'uvi'
|
|
213
|
+
if not isinstance(data, dict) or 'uvi' not in data:
|
|
214
|
+
_LOGGER.error("Formato inválido: Se esperaba un dict con la clave 'uvi'. Datos: %s", data)
|
|
215
|
+
raise ValueError("Formato de datos inválido")
|
|
216
|
+
|
|
217
|
+
# Extraer la lista de datos bajo la clave 'uvi'
|
|
218
|
+
uvi_data = data.get('uvi', [])
|
|
219
|
+
|
|
220
|
+
# Validar que 'uvi' sea una lista de diccionarios
|
|
221
|
+
if not isinstance(uvi_data, list) or not all(isinstance(item, dict) for item in uvi_data):
|
|
222
|
+
_LOGGER.error("Formato inválido: 'uvi' debe ser una lista de dicts. Datos: %s", uvi_data)
|
|
223
|
+
raise ValueError("Formato de datos inválido")
|
|
224
|
+
|
|
225
|
+
# Determinar la ruta al archivo en la carpeta raíz del repositorio
|
|
226
|
+
output_file = os.path.join(
|
|
227
|
+
self.hass.config.path(),
|
|
228
|
+
"custom_components",
|
|
229
|
+
"meteocat",
|
|
230
|
+
"files",
|
|
231
|
+
f"uvi_{self.town_id.lower()}_data.json"
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# Guardar los datos en un archivo JSON
|
|
235
|
+
await save_json_to_file(data, output_file)
|
|
236
|
+
|
|
237
|
+
return uvi_data
|
|
238
|
+
except asyncio.TimeoutError as err:
|
|
239
|
+
_LOGGER.warning("Tiempo de espera agotado al obtener datos de la API de Meteocat.")
|
|
240
|
+
raise ConfigEntryNotReady from err
|
|
241
|
+
except ForbiddenError as err:
|
|
242
|
+
_LOGGER.error(
|
|
243
|
+
"Acceso denegado al obtener datos del índice UV para (Town ID: %s): %s",
|
|
244
|
+
self.town_id,
|
|
245
|
+
err,
|
|
246
|
+
)
|
|
247
|
+
raise ConfigEntryNotReady from err
|
|
248
|
+
except TooManyRequestsError as err:
|
|
249
|
+
_LOGGER.warning(
|
|
250
|
+
"Límite de solicitudes alcanzado al obtener datos del índice UV para (Town ID: %s): %s",
|
|
251
|
+
self.town_id,
|
|
252
|
+
err,
|
|
253
|
+
)
|
|
254
|
+
raise ConfigEntryNotReady from err
|
|
255
|
+
except (BadRequestError, InternalServerError, UnknownAPIError) as err:
|
|
256
|
+
_LOGGER.error(
|
|
257
|
+
"Error al obtener datos del índice UV para (Town ID: %s): %s",
|
|
258
|
+
self.town_id,
|
|
134
259
|
err,
|
|
135
260
|
)
|
|
136
261
|
raise
|
|
262
|
+
except Exception as err:
|
|
263
|
+
if isinstance(err, ConfigEntryNotReady):
|
|
264
|
+
# El dispositivo no pudo inicializarse por primera vez
|
|
265
|
+
_LOGGER.exception(
|
|
266
|
+
"No se pudo inicializar el dispositivo (Town ID: %s) debido a un error: %s",
|
|
267
|
+
self.town_id,
|
|
268
|
+
err,
|
|
269
|
+
)
|
|
270
|
+
raise # Re-raise the exception to indicate a fundamental failure in initialization
|
|
271
|
+
else:
|
|
272
|
+
# Manejar error durante la actualización de datos
|
|
273
|
+
_LOGGER.exception(
|
|
274
|
+
"Error inesperado al obtener datos del índice UV para (Town ID: %s): %s",
|
|
275
|
+
self.town_id,
|
|
276
|
+
err,
|
|
277
|
+
)
|
|
278
|
+
# Intentar cargar datos en caché si hay un error
|
|
279
|
+
cached_data = load_json_from_file(output_file)
|
|
280
|
+
if cached_data:
|
|
281
|
+
_LOGGER.info("Usando datos en caché para la ciudad %s.", self.town_id)
|
|
282
|
+
return cached_data
|
|
283
|
+
# No se puede actualizar el estado, retornar None o un estado fallido
|
|
284
|
+
return None # o cualquier otro valor que indique un estado de error
|
|
285
|
+
|
|
286
|
+
class MeteocatUviFileCoordinator(DataUpdateCoordinator):
|
|
287
|
+
"""Coordinator to read and process UV data from a file."""
|
|
288
|
+
|
|
289
|
+
def __init__(
|
|
290
|
+
self,
|
|
291
|
+
hass: HomeAssistant,
|
|
292
|
+
entry_data: dict,
|
|
293
|
+
update_interval: timedelta = DEFAULT_UVI_SENSOR_UPDATE_INTERVAL,
|
|
294
|
+
):
|
|
295
|
+
"""
|
|
296
|
+
Inicializa el coordinador del sensor del Índice UV de Meteocat.
|
|
297
|
+
|
|
298
|
+
Args:
|
|
299
|
+
hass (HomeAssistant): Instancia de Home Assistant.
|
|
300
|
+
entry_data (dict): Datos de configuración obtenidos de core.config_entries.
|
|
301
|
+
update_interval (timedelta): Intervalo de actualización.
|
|
302
|
+
"""
|
|
303
|
+
self.town_id = entry_data["town_id"] # Usamos el ID del municipio
|
|
304
|
+
|
|
305
|
+
super().__init__(
|
|
306
|
+
hass,
|
|
307
|
+
_LOGGER,
|
|
308
|
+
name=f"{DOMAIN} Uvi File Coordinator",
|
|
309
|
+
update_interval=update_interval,
|
|
310
|
+
)
|
|
311
|
+
self._file_path = os.path.join(
|
|
312
|
+
hass.config.path("custom_components/meteocat/files"),
|
|
313
|
+
f"uvi_{self.town_id.lower()}_data.json",
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
async def _async_update_data(self):
|
|
317
|
+
"""Read and process UV data for the current hour from the file asynchronously."""
|
|
318
|
+
try:
|
|
319
|
+
async with aiofiles.open(self._file_path, "r", encoding="utf-8") as file:
|
|
320
|
+
raw_data = await file.read()
|
|
321
|
+
raw_data = json.loads(raw_data) # Parse JSON data
|
|
322
|
+
except FileNotFoundError:
|
|
323
|
+
_LOGGER.error(
|
|
324
|
+
"No se ha encontrado el archivo JSON con datos del índice UV en %s.",
|
|
325
|
+
self._file_path,
|
|
326
|
+
)
|
|
327
|
+
return {}
|
|
328
|
+
except json.JSONDecodeError:
|
|
329
|
+
_LOGGER.error(
|
|
330
|
+
"Error al decodificar el archivo JSON del índice UV en %s.",
|
|
331
|
+
self._file_path,
|
|
332
|
+
)
|
|
333
|
+
return {}
|
|
334
|
+
|
|
335
|
+
return self._get_uv_for_current_hour(raw_data)
|
|
336
|
+
|
|
337
|
+
def _get_uv_for_current_hour(self, raw_data):
|
|
338
|
+
"""Get UV data for the current hour."""
|
|
339
|
+
# Fecha y hora actual
|
|
340
|
+
current_datetime = datetime.now()
|
|
341
|
+
current_date = current_datetime.strftime("%Y-%m-%d")
|
|
342
|
+
current_hour = current_datetime.hour
|
|
343
|
+
|
|
344
|
+
# Busca los datos para la fecha actual
|
|
345
|
+
for day_data in raw_data.get("uvi", []):
|
|
346
|
+
if day_data["date"] == current_date:
|
|
347
|
+
# Encuentra los datos de la hora actual
|
|
348
|
+
for hour_data in day_data.get("hours", []):
|
|
349
|
+
if hour_data["hour"] == current_hour:
|
|
350
|
+
return {
|
|
351
|
+
"hour": hour_data.get("hour", 0),
|
|
352
|
+
"uvi": hour_data.get("uvi", 0),
|
|
353
|
+
"uvi_clouds": hour_data.get("uvi_clouds", 0),
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
# Si no se encuentran datos, devuelve un diccionario vacío con valores predeterminados
|
|
357
|
+
_LOGGER.warning(
|
|
358
|
+
"No se encontraron datos del índice UV para hoy (%s) y la hora actual (%s).",
|
|
359
|
+
current_date,
|
|
360
|
+
current_hour,
|
|
361
|
+
)
|
|
362
|
+
return {"hour": 0, "uvi": 0, "uvi_clouds": 0}
|
|
137
363
|
|
|
138
364
|
# class MeteocatEntityCoordinator(DataUpdateCoordinator):
|
|
139
365
|
# """Coordinator para manejar la actualización de datos de las entidades de predicción."""
|
|
@@ -25,7 +25,7 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
|
|
|
25
25
|
|
|
26
26
|
def __init__(self, config_entry: ConfigEntry):
|
|
27
27
|
"""Inicializa el flujo de opciones."""
|
|
28
|
-
self.
|
|
28
|
+
self._config_entry = config_entry
|
|
29
29
|
self.api_key: str | None = None
|
|
30
30
|
|
|
31
31
|
async def async_step_init(self, user_input: dict | None = None):
|
|
@@ -60,8 +60,8 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
|
|
|
60
60
|
if not errors:
|
|
61
61
|
# Actualizar la configuración de la entrada con la nueva API Key
|
|
62
62
|
self.hass.config_entries.async_update_entry(
|
|
63
|
-
self.
|
|
64
|
-
data={**self.
|
|
63
|
+
self._config_entry,
|
|
64
|
+
data={**self._config_entry.data, CONF_API_KEY: self.api_key},
|
|
65
65
|
)
|
|
66
66
|
return self.async_create_entry(title="", data={})
|
|
67
67
|
|
|
@@ -59,7 +59,10 @@ from .const import (
|
|
|
59
59
|
WIND_GUST_CODE,
|
|
60
60
|
)
|
|
61
61
|
|
|
62
|
-
from .coordinator import
|
|
62
|
+
from .coordinator import (
|
|
63
|
+
MeteocatSensorCoordinator,
|
|
64
|
+
MeteocatUviFileCoordinator,
|
|
65
|
+
)
|
|
63
66
|
|
|
64
67
|
_LOGGER = logging.getLogger(__name__)
|
|
65
68
|
|
|
@@ -201,18 +204,82 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
|
|
|
201
204
|
)
|
|
202
205
|
)
|
|
203
206
|
|
|
204
|
-
|
|
205
207
|
@callback
|
|
206
208
|
async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback) -> None:
|
|
207
209
|
"""Set up Meteocat sensors from a config entry."""
|
|
208
210
|
entry_data = hass.data[DOMAIN][entry.entry_id]
|
|
209
|
-
coordinator = entry_data["sensor_coordinator"]
|
|
210
211
|
|
|
212
|
+
# Coordinadores para sensores
|
|
213
|
+
coordinator = entry_data.get("sensor_coordinator")
|
|
214
|
+
uvi_file_coordinator = entry_data.get("uvi_file_coordinator")
|
|
215
|
+
|
|
216
|
+
# Sensores generales
|
|
211
217
|
async_add_entities(
|
|
212
218
|
MeteocatSensor(coordinator, description, entry_data)
|
|
213
219
|
for description in SENSOR_TYPES
|
|
220
|
+
if description.key != UV_INDEX # Excluir UVI del coordinador general
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
# Sensor UVI
|
|
224
|
+
async_add_entities(
|
|
225
|
+
MeteocatUviSensor(uvi_file_coordinator, description, entry_data)
|
|
226
|
+
for description in SENSOR_TYPES
|
|
227
|
+
if description.key == UV_INDEX # Incluir UVI en el coordinador UVI FILE COORDINATOR
|
|
214
228
|
)
|
|
215
229
|
|
|
230
|
+
class MeteocatUviSensor(CoordinatorEntity[MeteocatUviFileCoordinator], SensorEntity):
|
|
231
|
+
"""Representation of a Meteocat UV Index sensor."""
|
|
232
|
+
|
|
233
|
+
_attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
|
|
234
|
+
|
|
235
|
+
def __init__(self, uvi_file_coordinator, description, entry_data):
|
|
236
|
+
"""Initialize the UV Index sensor."""
|
|
237
|
+
super().__init__(uvi_file_coordinator)
|
|
238
|
+
self.entity_description = description
|
|
239
|
+
self._town_name = entry_data["town_name"]
|
|
240
|
+
self._town_id = entry_data["town_id"]
|
|
241
|
+
self._station_id = entry_data["station_id"]
|
|
242
|
+
|
|
243
|
+
# Unique ID for the entity
|
|
244
|
+
self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
|
|
245
|
+
|
|
246
|
+
# Asigna entity_category desde description (si está definido)
|
|
247
|
+
self._attr_entity_category = getattr(description, "entity_category", None)
|
|
248
|
+
|
|
249
|
+
# Log para depuración
|
|
250
|
+
_LOGGER.debug(
|
|
251
|
+
"Inicializando sensor: %s, Unique ID: %s",
|
|
252
|
+
self.entity_description.name,
|
|
253
|
+
self._attr_unique_id,
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
@property
|
|
257
|
+
def native_value(self):
|
|
258
|
+
"""Return the current UV index value."""
|
|
259
|
+
if self.entity_description.key == UV_INDEX:
|
|
260
|
+
uvi_data = self.coordinator.data or {}
|
|
261
|
+
return uvi_data.get("uvi", None)
|
|
262
|
+
|
|
263
|
+
@property
|
|
264
|
+
def extra_state_attributes(self):
|
|
265
|
+
"""Return additional attributes for the sensor."""
|
|
266
|
+
attributes = super().extra_state_attributes or {}
|
|
267
|
+
if self.entity_description.key == UV_INDEX:
|
|
268
|
+
uvi_data = self.coordinator.data or {}
|
|
269
|
+
# Add the "hour" attribute if it exists
|
|
270
|
+
attributes["hour"] = uvi_data.get("hour")
|
|
271
|
+
return attributes
|
|
272
|
+
|
|
273
|
+
@property
|
|
274
|
+
def device_info(self) -> DeviceInfo:
|
|
275
|
+
"""Return the device info."""
|
|
276
|
+
return DeviceInfo(
|
|
277
|
+
identifiers={(DOMAIN, self._town_id)},
|
|
278
|
+
name="Meteocat " + self._station_id + " " + self._town_name,
|
|
279
|
+
manufacturer="Meteocat",
|
|
280
|
+
model="Meteocat API",
|
|
281
|
+
)
|
|
282
|
+
|
|
216
283
|
class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity):
|
|
217
284
|
"""Representation of a Meteocat sensor."""
|
|
218
285
|
STATIC_KEYS = {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID}
|
|
@@ -225,7 +292,7 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
|
|
|
225
292
|
PRESSURE: PRESSURE_CODE,
|
|
226
293
|
PRECIPITATION: PRECIPITATION_CODE,
|
|
227
294
|
SOLAR_GLOBAL_IRRADIANCE: SOLAR_GLOBAL_IRRADIANCE_CODE,
|
|
228
|
-
UV_INDEX: UV_INDEX_CODE,
|
|
295
|
+
# UV_INDEX: UV_INDEX_CODE,
|
|
229
296
|
MAX_TEMPERATURE: MAX_TEMPERATURE_CODE,
|
|
230
297
|
MIN_TEMPERATURE: MIN_TEMPERATURE_CODE,
|
|
231
298
|
WIND_GUST: WIND_GUST_CODE,
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# version.py
|
|
2
|
-
__version__ = "0.1.
|
|
2
|
+
__version__ = "0.1.36"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meteocat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.36",
|
|
4
4
|
"description": "[](https://opensource.org/licenses/Apache-2.0)\r [](https://pypi.org/project/meteocat)\r [](https://gitlab.com/figorr/meteocat/commits/master)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|
package/poetry.lock
CHANGED
|
@@ -121,13 +121,13 @@ speedups = ["Brotli", "aiodns (>=3.2.0)", "brotlicffi"]
|
|
|
121
121
|
|
|
122
122
|
[[package]]
|
|
123
123
|
name = "aiosignal"
|
|
124
|
-
version = "1.3.
|
|
124
|
+
version = "1.3.2"
|
|
125
125
|
description = "aiosignal: a list of registered asynchronous callbacks"
|
|
126
126
|
optional = false
|
|
127
|
-
python-versions = ">=3.
|
|
127
|
+
python-versions = ">=3.9"
|
|
128
128
|
files = [
|
|
129
|
-
{file = "aiosignal-1.3.
|
|
130
|
-
{file = "aiosignal-1.3.
|
|
129
|
+
{file = "aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5"},
|
|
130
|
+
{file = "aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54"},
|
|
131
131
|
]
|
|
132
132
|
|
|
133
133
|
[package.dependencies]
|
|
@@ -298,13 +298,13 @@ files = [
|
|
|
298
298
|
|
|
299
299
|
[[package]]
|
|
300
300
|
name = "certifi"
|
|
301
|
-
version = "2024.
|
|
301
|
+
version = "2024.12.14"
|
|
302
302
|
description = "Python package for providing Mozilla's CA Bundle."
|
|
303
303
|
optional = false
|
|
304
304
|
python-versions = ">=3.6"
|
|
305
305
|
files = [
|
|
306
|
-
{file = "certifi-2024.
|
|
307
|
-
{file = "certifi-2024.
|
|
306
|
+
{file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"},
|
|
307
|
+
{file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"},
|
|
308
308
|
]
|
|
309
309
|
|
|
310
310
|
[[package]]
|
|
@@ -796,13 +796,13 @@ poetry = ["poetry"]
|
|
|
796
796
|
|
|
797
797
|
[[package]]
|
|
798
798
|
name = "eventlet"
|
|
799
|
-
version = "0.38.
|
|
799
|
+
version = "0.38.2"
|
|
800
800
|
description = "Highly concurrent networking library"
|
|
801
801
|
optional = false
|
|
802
802
|
python-versions = ">=3.7"
|
|
803
803
|
files = [
|
|
804
|
-
{file = "eventlet-0.38.
|
|
805
|
-
{file = "eventlet-0.38.
|
|
804
|
+
{file = "eventlet-0.38.2-py3-none-any.whl", hash = "sha256:4a2e3cbc53917c8f39074ccf689501168563d3a4df59e9cddd5e9d3b7f85c599"},
|
|
805
|
+
{file = "eventlet-0.38.2.tar.gz", hash = "sha256:6a46823af1dca7d29cf04c0d680365805435473c3acbffc176765c7f8787edac"},
|
|
806
806
|
]
|
|
807
807
|
|
|
808
808
|
[package.dependencies]
|
|
@@ -2160,20 +2160,20 @@ testing = ["coverage (==6.2)", "mypy (==0.931)"]
|
|
|
2160
2160
|
|
|
2161
2161
|
[[package]]
|
|
2162
2162
|
name = "pytest-asyncio"
|
|
2163
|
-
version = "0.
|
|
2163
|
+
version = "0.25.0"
|
|
2164
2164
|
description = "Pytest support for asyncio"
|
|
2165
2165
|
optional = false
|
|
2166
|
-
python-versions = ">=3.
|
|
2166
|
+
python-versions = ">=3.9"
|
|
2167
2167
|
files = [
|
|
2168
|
-
{file = "pytest_asyncio-0.
|
|
2169
|
-
{file = "pytest_asyncio-0.
|
|
2168
|
+
{file = "pytest_asyncio-0.25.0-py3-none-any.whl", hash = "sha256:db5432d18eac6b7e28b46dcd9b69921b55c3b1086e85febfe04e70b18d9e81b3"},
|
|
2169
|
+
{file = "pytest_asyncio-0.25.0.tar.gz", hash = "sha256:8c0610303c9e0442a5db8604505fc0f545456ba1528824842b37b4a626cbf609"},
|
|
2170
2170
|
]
|
|
2171
2171
|
|
|
2172
2172
|
[package.dependencies]
|
|
2173
2173
|
pytest = ">=8.2,<9"
|
|
2174
2174
|
|
|
2175
2175
|
[package.extras]
|
|
2176
|
-
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1
|
|
2176
|
+
docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"]
|
|
2177
2177
|
testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"]
|
|
2178
2178
|
|
|
2179
2179
|
[[package]]
|
package/pyproject.toml
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "meteocat"
|
|
3
|
-
version = "0.1.
|
|
3
|
+
version = "0.1.36"
|
|
4
4
|
description = "Script para obtener datos meteorológicos de la API de Meteocat"
|
|
5
5
|
authors = ["figorr <jdcuartero@yahoo.es>"]
|
|
6
6
|
license = "Apache-2.0"
|
|
7
7
|
readme = "README.md"
|
|
8
8
|
repository = "https://gitlab.com/figorr/meteocat"
|
|
9
9
|
keywords = ['meteocat']
|
|
10
|
+
packages = [
|
|
11
|
+
{ include = "meteocat", from = "custom_components" }
|
|
12
|
+
]
|
|
10
13
|
|
|
11
14
|
|
|
12
15
|
[tool.poetry.dependencies]
|
|
@@ -62,4 +65,4 @@ pipdeptree = "^2.2.1"
|
|
|
62
65
|
|
|
63
66
|
[tool.poetry.group.dev.dependencies]
|
|
64
67
|
pyupgrade = "^3.4.0"
|
|
65
|
-
pre-commit = "^3.3.1"
|
|
68
|
+
pre-commit = "^3.3.1"
|