meteocat 1.1.0 → 1.1.2

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.
@@ -17,6 +17,7 @@ from homeassistant.components.weather import Forecast
17
17
  from meteocatpy.data import MeteocatStationData
18
18
  from meteocatpy.uvi import MeteocatUviData
19
19
  from meteocatpy.forecast import MeteocatForecast
20
+ from meteocatpy.alerts import MeteocatAlerts
20
21
 
21
22
  from meteocatpy.exceptions import (
22
23
  BadRequestError,
@@ -33,6 +34,11 @@ from .const import (
33
34
  DEFAULT_VALIDITY_DAYS,
34
35
  DEFAULT_VALIDITY_HOURS,
35
36
  DEFAULT_VALIDITY_MINUTES,
37
+ DEFAULT_ALERT_VALIDITY_TIME,
38
+ ALERT_VALIDITY_MULTIPLIER_100,
39
+ ALERT_VALIDITY_MULTIPLIER_200,
40
+ ALERT_VALIDITY_MULTIPLIER_500,
41
+ ALERT_VALIDITY_MULTIPLIER_DEFAULT
36
42
  )
37
43
 
38
44
  _LOGGER = logging.getLogger(__name__)
@@ -47,6 +53,8 @@ DEFAULT_UVI_UPDATE_INTERVAL = timedelta(minutes=60)
47
53
  DEFAULT_UVI_SENSOR_UPDATE_INTERVAL = timedelta(minutes=5)
48
54
  DEFAULT_CONDITION_SENSOR_UPDATE_INTERVAL = timedelta(minutes=5)
49
55
  DEFAULT_TEMP_FORECAST_UPDATE_INTERVAL = timedelta(minutes=5)
56
+ DEFAULT_ALERTS_UPDATE_INTERVAL = timedelta(minutes=10)
57
+ DEFAULT_ALERTS_REGION_UPDATE_INTERVAL = timedelta(minutes=5)
50
58
 
51
59
  # Definir la zona horaria local
52
60
  TIMEZONE = ZoneInfo("Europe/Madrid")
@@ -1102,3 +1110,450 @@ class MeteocatTempForecastCoordinator(DataUpdateCoordinator):
1102
1110
  "min_temp_forecast": float(variables.get("tmin", {}).get("valor", 0.0)),
1103
1111
  }
1104
1112
  return temp_forecast_data
1113
+
1114
+ class MeteocatAlertsCoordinator(DataUpdateCoordinator):
1115
+ """Coordinator para manejar la actualización de alertas."""
1116
+
1117
+ def __init__(self, hass: HomeAssistant, entry_data: dict):
1118
+ """
1119
+ Inicializa el coordinador de alertas de Meteocat.
1120
+
1121
+ Args:
1122
+ hass (HomeAssistant): Instancia de Home Assistant.
1123
+ entry_data (dict): Datos de configuración obtenidos de core.config_entries.
1124
+ """
1125
+ self.api_key = entry_data["api_key"]
1126
+ self.region_id = entry_data["region_id"] # ID de la región o comarca
1127
+ self.limit_prediccio = entry_data["limit_prediccio"] # Límite de llamada a la API para PREDICCIONES
1128
+ self.alerts_data = MeteocatAlerts(self.api_key)
1129
+
1130
+ # Define la ruta del archivo JSON principal donde se guardarán las alertas
1131
+ self.alerts_file = os.path.join(
1132
+ hass.config.path(),
1133
+ "custom_components",
1134
+ "meteocat",
1135
+ "files",
1136
+ "alerts.json"
1137
+ )
1138
+
1139
+ # Define la ruta del archivo JSON filtrado por región
1140
+ self.alerts_region_file = os.path.join(
1141
+ hass.config.path(),
1142
+ "custom_components",
1143
+ "meteocat",
1144
+ "files",
1145
+ f"alerts_{self.region_id}.json"
1146
+ )
1147
+
1148
+ super().__init__(
1149
+ hass,
1150
+ _LOGGER,
1151
+ name=f"{DOMAIN} Alerts Coordinator",
1152
+ update_interval=DEFAULT_ALERTS_UPDATE_INTERVAL,
1153
+ )
1154
+
1155
+ async def _async_update_data(self) -> Dict:
1156
+ """Actualiza los datos de alertas desde la API de Meteocat o desde el archivo local según las condiciones especificadas."""
1157
+ # Comprobar si existe el archivo 'alerts.json'
1158
+ existing_data = await load_json_from_file(self.alerts_file)
1159
+
1160
+ # Calcular el tiempo de validez basado en el límite de predicción
1161
+ if self.limit_prediccio <= 100:
1162
+ multiplier = ALERT_VALIDITY_MULTIPLIER_100
1163
+ elif 100 < self.limit_prediccio <= 200:
1164
+ multiplier = ALERT_VALIDITY_MULTIPLIER_200
1165
+ elif 200 < self.limit_prediccio <= 500:
1166
+ multiplier = ALERT_VALIDITY_MULTIPLIER_500
1167
+ else:
1168
+ multiplier = ALERT_VALIDITY_MULTIPLIER_DEFAULT
1169
+
1170
+ validity_duration = timedelta(minutes=DEFAULT_ALERT_VALIDITY_TIME * multiplier)
1171
+
1172
+ # Si no existe el archivo
1173
+ if not existing_data:
1174
+ return await self._fetch_and_save_new_data()
1175
+ else:
1176
+ # Comprobar la antigüedad de los datos
1177
+ last_update = datetime.fromisoformat(existing_data['actualitzat']['dataUpdate'])
1178
+ now = datetime.now(timezone.utc).astimezone(TIMEZONE)
1179
+
1180
+ # Comparar la antigüedad de los datos
1181
+ if now - last_update > validity_duration:
1182
+ return await self._fetch_and_save_new_data()
1183
+ else:
1184
+ # Devolver los datos del archivo existente
1185
+ _LOGGER.debug("Usando datos existentes de alertas: %s", existing_data)
1186
+ return {
1187
+ "actualizado": existing_data['actualitzat']['dataUpdate']
1188
+ }
1189
+
1190
+ async def _fetch_and_save_new_data(self):
1191
+ """Obtiene nuevos datos de la API y los guarda en el archivo JSON."""
1192
+ try:
1193
+ # Obtener los datos de alertas desde la API
1194
+ data = await asyncio.wait_for(self.alerts_data.get_alerts(), timeout=30)
1195
+ _LOGGER.debug("Datos de alertas actualizados exitosamente: %s", data)
1196
+
1197
+ # Validar que los datos sean una lista de diccionarios o una lista vacía
1198
+ if not isinstance(data, list) or (data and not all(isinstance(item, dict) for item in data)):
1199
+ _LOGGER.error(
1200
+ "Formato inválido: Se esperaba una lista de diccionarios, pero se obtuvo %s. Datos: %s",
1201
+ type(data).__name__,
1202
+ data,
1203
+ )
1204
+ raise ValueError("Formato de datos inválido")
1205
+
1206
+ # Añadir la clave 'actualitzat' con la fecha y hora actual de la zona horaria local
1207
+ current_time = datetime.now(timezone.utc).astimezone(TIMEZONE).isoformat()
1208
+ data_with_timestamp = {
1209
+ "actualitzat": {
1210
+ "dataUpdate": current_time
1211
+ },
1212
+ "dades": data
1213
+ }
1214
+
1215
+ # Guardar los datos de alertas en un archivo JSON
1216
+ await save_json_to_file(data_with_timestamp, self.alerts_file)
1217
+
1218
+ # Filtrar los datos por región y guardar en un nuevo archivo JSON
1219
+ await self._filter_alerts_by_region()
1220
+
1221
+ # Devolver tanto los datos de alertas como la fecha de actualización
1222
+ return {
1223
+ "actualizado": data_with_timestamp['actualitzat']['dataUpdate']
1224
+ }
1225
+ except asyncio.TimeoutError as err:
1226
+ _LOGGER.warning("Tiempo de espera agotado al obtener datos de alertas.")
1227
+ raise ConfigEntryNotReady from err
1228
+ except ForbiddenError as err:
1229
+ _LOGGER.error("Acceso denegado al obtener datos de alertas: %s", err)
1230
+ raise ConfigEntryNotReady from err
1231
+ except TooManyRequestsError as err:
1232
+ _LOGGER.warning("Límite de solicitudes alcanzado al obtener datos de alertas: %s", err)
1233
+ raise ConfigEntryNotReady from err
1234
+ except (BadRequestError, InternalServerError, UnknownAPIError) as err:
1235
+ _LOGGER.error("Error al obtener datos de alertas: %s", err)
1236
+ raise
1237
+ except Exception as err:
1238
+ _LOGGER.exception("Error inesperado al obtener datos de alertas: %s", err)
1239
+
1240
+ # Intentar cargar datos en caché si hay un error
1241
+ cached_data = await load_json_from_file(self.alerts_file)
1242
+ if self._is_valid_alert_data(cached_data):
1243
+ _LOGGER.warning(
1244
+ "Usando datos en caché para las alertas. Última actualización: %s",
1245
+ cached_data["actualitzat"]["dataUpdate"],
1246
+ )
1247
+ return cached_data
1248
+
1249
+ # Si no se puede actualizar ni cargar datos en caché, retornar None
1250
+ _LOGGER.error("No se pudo obtener datos actualizados ni cargar datos en caché de alertas.")
1251
+ return None
1252
+
1253
+ @staticmethod
1254
+ def _is_valid_alert_data(data: dict) -> bool:
1255
+ """Valida que los datos de alertas tengan el formato esperado."""
1256
+ return (
1257
+ isinstance(data, dict)
1258
+ and "dades" in data
1259
+ and isinstance(data["dades"], list)
1260
+ and (not data["dades"] or all(isinstance(item, dict) for item in data["dades"]))
1261
+ and "actualitzat" in data
1262
+ and isinstance(data["actualitzat"], dict)
1263
+ and "dataUpdate" in data["actualitzat"]
1264
+ )
1265
+
1266
+ async def _filter_alerts_by_region(self):
1267
+ """Filtra las alertas por la región y guarda los resultados en un archivo JSON."""
1268
+ # Obtener el momento actual
1269
+ now = datetime.now(timezone.utc).astimezone(TIMEZONE).isoformat()
1270
+
1271
+ # Carga el archivo alerts.json
1272
+ data = await load_json_from_file(self.alerts_file)
1273
+ if not data:
1274
+ _LOGGER.error("El archivo de alertas %s no existe o está vacío.", self.alerts_file)
1275
+ return
1276
+
1277
+ filtered_alerts = []
1278
+
1279
+ for item in data.get("dades", []):
1280
+ avisos_filtrados = []
1281
+
1282
+ for aviso in item.get("avisos", []):
1283
+ evolucions = []
1284
+ data_inici = None
1285
+ data_fi = None
1286
+
1287
+ for evolucion in aviso.get("evolucions", []):
1288
+ periodes = []
1289
+ for periode in evolucion.get("periodes", []):
1290
+ afectacions = [
1291
+ afectacio for afectacio in (periode.get("afectacions") or [])
1292
+ if afectacio and str(afectacio.get("idComarca")) == self.region_id
1293
+ ]
1294
+ if afectacions:
1295
+ if not data_inici:
1296
+ dia = evolucion.get("dia")[:-6]
1297
+ data_inici = f"{dia}{periode.get('nom').split('-')[0]}:00Z"
1298
+
1299
+ # Calcular dataFi
1300
+ dia = evolucion.get('dia')[:-6] # Eliminar "T00:00Z"
1301
+ hora_fin = periode.get('nom').split('-')[1]
1302
+ # Ajustar dataFi correctamente si termina a medianoche
1303
+ if hora_fin == "00":
1304
+ dia = evolucion.get('dia')[:-6] # Eliminar "T00:00Z"
1305
+ data_fi = f"{dia}23:59Z"
1306
+ else:
1307
+ dia = evolucion.get('dia')[:-6] # Eliminar "T00:00Z"
1308
+ data_fi = f"{dia}{hora_fin}:00Z"
1309
+
1310
+ periodes.append({
1311
+ "nom": periode.get("nom"),
1312
+ "afectacions": afectacions if afectacions else None
1313
+ })
1314
+
1315
+ if any(p.get("afectacions") for p in periodes):
1316
+ evolucions.append({
1317
+ "dia": evolucion.get("dia"),
1318
+ "comentari": evolucion.get("comentari"),
1319
+ "representatiu": evolucion.get("representatiu"),
1320
+ "llindar1": evolucion.get("llindar1"),
1321
+ "llindar2": evolucion.get("llindar2"),
1322
+ "distribucioGeografica": evolucion.get("distribucioGeografica"),
1323
+ "periodes": periodes,
1324
+ "valorMaxim": evolucion.get("valorMaxim")
1325
+ })
1326
+
1327
+ # Comprobar si la fecha de fin ya ha pasado
1328
+ if evolucions and data_fi >= now:
1329
+ avisos_filtrados.append({
1330
+ "tipus": aviso.get("tipus"),
1331
+ "dataEmisio": aviso.get("dataEmisio"),
1332
+ "dataInici": data_inici,
1333
+ "dataFi": data_fi,
1334
+ "evolucions": evolucions,
1335
+ "estat": aviso.get("estat")
1336
+ })
1337
+
1338
+ if avisos_filtrados:
1339
+ filtered_alerts.append({
1340
+ "estat": item.get("estat"),
1341
+ "meteor": item.get("meteor"),
1342
+ "avisos": avisos_filtrados
1343
+ })
1344
+
1345
+ # Guardar los datos filtrados en un archivo JSON
1346
+ await save_json_to_file({
1347
+ "actualitzat": data.get("actualitzat"),
1348
+ "dades": filtered_alerts
1349
+ }, self.alerts_region_file)
1350
+
1351
+ class MeteocatAlertsRegionCoordinator(DataUpdateCoordinator):
1352
+ """Coordinator para manejar la actualización de alertas por región."""
1353
+
1354
+ def __init__(self, hass: HomeAssistant, entry_data: dict):
1355
+ """Inicializa el coordinador para alertas de una comarca."""
1356
+ self.town_name = entry_data["town_name"]
1357
+ self.town_id = entry_data["town_id"]
1358
+ self.station_name = entry_data["station_name"]
1359
+ self.station_id = entry_data["station_id"]
1360
+ self.region_name = entry_data["region_name"]
1361
+ self.region_id = entry_data["region_id"]
1362
+ super().__init__(
1363
+ hass,
1364
+ _LOGGER,
1365
+ name=f"{DOMAIN} Alerts Region Coordinator",
1366
+ update_interval=DEFAULT_ALERTS_REGION_UPDATE_INTERVAL,
1367
+ )
1368
+ self._file_path = os.path.join(
1369
+ hass.config.path(),
1370
+ "custom_components",
1371
+ "meteocat",
1372
+ "files",
1373
+ f"alerts_{self.region_id}.json",
1374
+ )
1375
+
1376
+ def _convert_to_local_time(self, time_str: str) -> datetime:
1377
+ """Convierte una cadena de tiempo UTC a la zona horaria de Madrid."""
1378
+ if not time_str:
1379
+ return None
1380
+ # Convertir el tiempo de ISO a datetime con zona horaria UTC
1381
+ utc_time = datetime.fromisoformat(time_str.replace("Z", "+00:00"))
1382
+ # Convertir la hora UTC a la hora local
1383
+ local_time = utc_time.astimezone(TIMEZONE)
1384
+ return local_time
1385
+
1386
+ def _count_active_alerts(self, data: dict) -> int:
1387
+ """Cuenta las alertas activas procesando todas las alertas, sin detenerse en la primera coincidencia."""
1388
+ if not isinstance(data, dict) or "dades" not in data:
1389
+ _LOGGER.warning("Formato inesperado: 'dades' no es un diccionario o no contiene 'dades'.")
1390
+ return 0
1391
+
1392
+ active_alerts = 0
1393
+ current_time = datetime.now(TIMEZONE) # Hora local de Madrid
1394
+
1395
+ for item in data["dades"]: # Directamente acceder a 'dades' ya que sabemos que existe
1396
+ # Obtener el estado global de la alerta desde "dades"
1397
+ estat = item.get("estat", {}).get("nom")
1398
+
1399
+ # Proceder solo si el estado es "Obert"
1400
+ if estat == "Obert":
1401
+ avisos = item.get("avisos", [])
1402
+ for aviso in avisos:
1403
+ # Convertir las fechas de inicio y fin a hora local
1404
+ start_time = self._convert_to_local_time(aviso.get("dataInici"))
1405
+ end_time = self._convert_to_local_time(aviso.get("dataFi"))
1406
+
1407
+ # Verificar las condiciones para contar como alerta activa
1408
+ if start_time and end_time and start_time <= current_time <= end_time + timedelta(seconds=1):
1409
+ _LOGGER.debug(
1410
+ f"Alerta activa encontrada: {item.get('meteor', {}).get('nom', 'Desconocido')}"
1411
+ )
1412
+ active_alerts += 1
1413
+
1414
+ return active_alerts
1415
+
1416
+ def _get_time_period(self, current_hour: int) -> str:
1417
+ """Devuelve la franja horaria actual en formato 'nom'."""
1418
+ periods = ["00-06", "06-12", "12-18", "18-00"]
1419
+ if 0 <= current_hour < 6:
1420
+ return periods[0]
1421
+ elif 6 <= current_hour < 12:
1422
+ return periods[1]
1423
+ elif 12 <= current_hour < 18:
1424
+ return periods[2]
1425
+ else:
1426
+ return periods[3]
1427
+
1428
+ def _convert_period_to_local_time(self, period: str, date: str) -> str:
1429
+ """Convierte un periodo UTC a la hora local para una fecha específica."""
1430
+ utc_times = period.split("-")
1431
+ # Manejar la transición de "18-00" como "18-24" para el cálculo
1432
+ start_utc = utc_times[0]
1433
+ end_utc = "24" if utc_times[1] == "00" else utc_times[1]
1434
+
1435
+ # Convertir los tiempos UTC a datetime
1436
+ date_utc = datetime.fromisoformat(f"{date}T00:00+00:00") # Fecha base en UTC
1437
+ start_local = (date_utc + timedelta(hours=int(start_utc))).astimezone(TIMEZONE)
1438
+ end_local = (date_utc + timedelta(hours=int(end_utc))).astimezone(TIMEZONE)
1439
+
1440
+ # Formatear los tiempos a "HH:MM"
1441
+ return f"{start_local.strftime('%H:%M')} - {end_local.strftime('%H:%M')}"
1442
+
1443
+ async def _async_update_data(self) -> Dict[str, Any]:
1444
+ """Carga y procesa los datos de alertas desde el archivo JSON."""
1445
+ try:
1446
+ async with aiofiles.open(self._file_path, "r", encoding="utf-8") as file:
1447
+ raw_data = await file.read()
1448
+ data = json.loads(raw_data)
1449
+ _LOGGER.info("Datos cargados desde %s: %s", self._file_path, data) # Log de la carga de datos
1450
+ except FileNotFoundError:
1451
+ _LOGGER.error("No se encontró el archivo JSON de alertas en %s.", self._file_path)
1452
+ return {}
1453
+ except json.JSONDecodeError:
1454
+ _LOGGER.error("Error al decodificar el archivo JSON de alertas en %s.", self._file_path)
1455
+ return {}
1456
+
1457
+ return self._process_alerts_data(data)
1458
+
1459
+ def _process_alerts_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
1460
+ """Procesa los datos de alertas y devuelve un diccionario filtrado por región."""
1461
+ if not data.get("dades"):
1462
+ _LOGGER.info("No hay alertas activas para la región %s.", self.region_id)
1463
+ return {
1464
+ "estado": "Tancat",
1465
+ "actualizado": data.get("actualitzat", {}).get("dataUpdate", ""),
1466
+ "activas": 0, # Sin alertas activas
1467
+ "detalles": {"meteor": {}}
1468
+ }
1469
+
1470
+ # Obtener la fecha de actualización y añadirla a detalles
1471
+ data_update = data.get("actualitzat", {}).get("dataUpdate", "")
1472
+ current_time = datetime.now(TIMEZONE)
1473
+ current_date = current_time.date()
1474
+ current_hour = current_time.hour
1475
+ current_period = self._get_time_period(current_hour)
1476
+
1477
+ periods = ["00-06", "06-12", "12-18", "18-00"]
1478
+ current_period_index = periods.index(current_period)
1479
+
1480
+ alert_data = {
1481
+ "estado": "Obert",
1482
+ "actualizado": data_update,
1483
+ "activas": 0, # Inicializamos a 0 y actualizamos al final
1484
+ "detalles": {"meteor": {}}
1485
+ }
1486
+
1487
+ for alert in data["dades"]:
1488
+ estat = alert.get("estat", {}).get("nom", "Desconocido")
1489
+ meteor = alert.get("meteor", {}).get("nom", "Desconocido")
1490
+ avisos = alert.get("avisos", [])
1491
+ alert_found = False # Bandera para saber si encontramos una alerta para este meteor
1492
+
1493
+ for aviso in avisos:
1494
+ data_inici = self._convert_to_local_time(aviso.get("dataInici"))
1495
+ data_fi = self._convert_to_local_time(aviso.get("dataFi"))
1496
+ _LOGGER.info("Procesando aviso: inicio=%s, fin=%s", data_inici, data_fi) # Log del aviso
1497
+
1498
+ if data_inici and data_fi and data_inici <= data_fi: # Asegurarse que data_inici no sea mayor que data_fi
1499
+ evoluciones = aviso.get("evolucions", [])
1500
+ for evolucion in evoluciones:
1501
+ evolucion_date = datetime.fromisoformat(evolucion["dia"].replace("Z", "+00:00")).date()
1502
+ comentario = evolucion.get("comentari", "")
1503
+
1504
+ if evolucion_date >= current_date: # Mirar desde el día actual hacia adelante
1505
+ if evolucion_date == current_date:
1506
+ # Mirar el periodo actual y el siguiente si no hay alerta
1507
+ periodos_a_revisar = evolucion.get("periodes", [])[current_period_index:]
1508
+ if len(periodos_a_revisar) == 0 or not any(periodo.get("afectacions") for periodo in periodos_a_revisar):
1509
+ # Si no hay alertas en los periodos actuales o siguientes, miramos el siguiente periodo
1510
+ if current_period_index + 1 < len(periods):
1511
+ next_period = next((p for p in evolucion.get("periodes", []) if p.get("nom") == periods[current_period_index + 1]), None)
1512
+ if next_period and next_period.get("afectacions"):
1513
+ periodos_a_revisar = [next_period]
1514
+ else:
1515
+ periodos_a_revisar = []
1516
+ else:
1517
+ # Si estamos en el último periodo, miramos a días futuros
1518
+ periodos_a_revisar = []
1519
+ else:
1520
+ periodos_a_revisar = evolucion.get("periodes", [])
1521
+
1522
+ for periodo in periodos_a_revisar:
1523
+ local_period = self._convert_period_to_local_time(periodo["nom"], evolucion["dia"][:10])
1524
+ afectaciones = periodo.get("afectacions", [])
1525
+ if not afectaciones:
1526
+ _LOGGER.debug("No se encontraron afectaciones en el período: %s", periodo["nom"])
1527
+ continue
1528
+
1529
+ for afectacion in afectaciones:
1530
+ if afectacion.get("idComarca") == int(self.region_id): # Filtrar por idComarca de la región
1531
+ if not alert_found: # Solo agregamos la primera alerta encontrada para este meteor
1532
+ alert_data["detalles"]["meteor"][meteor] = {
1533
+ "fecha": evolucion["dia"][:10],
1534
+ "periodo": local_period,
1535
+ "estado": estat,
1536
+ "motivo": meteor,
1537
+ "inicio": data_inici,
1538
+ "fin": data_fi,
1539
+ "comentario": comentario,
1540
+ "umbral": afectacion.get("llindar", "Desconocido"),
1541
+ "peligro": afectacion.get("perill", 0),
1542
+ "nivel": afectacion.get("nivell", 0),
1543
+ }
1544
+ alert_found = True
1545
+ _LOGGER.info(
1546
+ "Alerta encontrada y agregada para %s: %s",
1547
+ meteor,
1548
+ alert_data["detalles"]["meteor"][meteor],
1549
+ )
1550
+ break # Salimos del ciclo de afectaciones ya que encontramos una alerta
1551
+ if alert_found:
1552
+ break # Salimos del ciclo de periodos ya que encontramos una alerta
1553
+ if alert_found:
1554
+ break # Salimos del ciclo de evoluciones si ya encontramos una alerta
1555
+
1556
+ alert_data["activas"] = len(alert_data["detalles"]["meteor"]) # Actualizar el número de alertas activas
1557
+ _LOGGER.info("Detalles recibidos: %s", alert_data.get("detalles", []))
1558
+
1559
+ return alert_data
@@ -8,6 +8,6 @@
8
8
  "iot_class": "cloud_polling",
9
9
  "issue_tracker": "https://github.com/figorr/meteocat/issues",
10
10
  "loggers": ["meteocatpy"],
11
- "requirements": ["meteocatpy==0.0.17", "packaging>=20.3", "wrapt>=1.14.0"],
12
- "version": "1.1.0"
11
+ "requirements": ["meteocatpy==0.0.20", "packaging>=20.3", "wrapt>=1.14.0"],
12
+ "version": "2.0.0"
13
13
  }
@@ -4,10 +4,13 @@ import logging
4
4
  from homeassistant.config_entries import ConfigEntry, OptionsFlow
5
5
  from homeassistant.exceptions import HomeAssistantError
6
6
  from homeassistant.helpers import config_validation as cv
7
+ from homeassistant.helpers.selector import SelectSelector, SelectSelectorConfig
7
8
  import voluptuous as vol
8
9
 
9
10
  from .const import (
10
- CONF_API_KEY
11
+ CONF_API_KEY,
12
+ LIMIT_XEMA,
13
+ LIMIT_PREDICCIO
11
14
  )
12
15
  from meteocatpy.town import MeteocatTown
13
16
  from meteocatpy.exceptions import (
@@ -27,45 +30,121 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
27
30
  """Inicializa el flujo de opciones."""
28
31
  self._config_entry = config_entry
29
32
  self.api_key: str | None = None
33
+ self.limit_xema: int | None = None
34
+ self.limit_prediccio: int | None = None
30
35
 
31
36
  async def async_step_init(self, user_input: dict | None = None):
32
37
  """Paso inicial del flujo de opciones."""
33
- return await self.async_step_update_api_key()
38
+ if user_input is not None:
39
+ if user_input["option"] == "update_api_and_limits":
40
+ return await self.async_step_update_api_and_limits()
41
+ elif user_input["option"] == "update_limits_only":
42
+ return await self.async_step_update_limits_only()
43
+
44
+ return self.async_show_form(
45
+ step_id="init",
46
+ data_schema=vol.Schema({
47
+ vol.Required("option"): SelectSelector(
48
+ SelectSelectorConfig(
49
+ options=[
50
+ "update_api_and_limits",
51
+ "update_limits_only"
52
+ ],
53
+ translation_key="option"
54
+ )
55
+ )
56
+ })
57
+ )
34
58
 
35
- async def async_step_update_api_key(self, user_input: dict | None = None):
36
- """Permite al usuario actualizar la API Key."""
59
+ async def async_step_update_api_and_limits(self, user_input: dict | None = None):
60
+ """Permite al usuario actualizar la API Key y los límites."""
37
61
  errors = {}
38
62
 
39
63
  if user_input is not None:
40
- self.api_key = user_input[CONF_API_KEY]
64
+ self.api_key = user_input.get(CONF_API_KEY)
65
+ self.limit_xema = user_input.get(LIMIT_XEMA)
66
+ self.limit_prediccio = user_input.get(LIMIT_PREDICCIO)
41
67
 
42
68
  # Validar la nueva API Key utilizando MeteocatTown
43
- town_client = MeteocatTown(self.api_key)
44
-
45
- try:
46
- await town_client.get_municipis() # Verificar que la API Key sea válida
47
- except (
48
- BadRequestError,
49
- ForbiddenError,
50
- TooManyRequestsError,
51
- InternalServerError,
52
- UnknownAPIError,
53
- ) as ex:
54
- _LOGGER.error("Error al validar la nueva API Key: %s", ex)
55
- errors["base"] = "cannot_connect"
56
- except Exception as ex:
57
- _LOGGER.error("Error inesperado al validar la nueva API Key: %s", ex)
58
- errors["base"] = "unknown"
69
+ if self.api_key:
70
+ town_client = MeteocatTown(self.api_key)
71
+ try:
72
+ await town_client.get_municipis() # Verificar que la API Key sea válida
73
+ except (
74
+ BadRequestError,
75
+ ForbiddenError,
76
+ TooManyRequestsError,
77
+ InternalServerError,
78
+ UnknownAPIError,
79
+ ) as ex:
80
+ _LOGGER.error("Error al validar la nueva API Key: %s", ex)
81
+ errors["base"] = "cannot_connect"
82
+ except Exception as ex:
83
+ _LOGGER.error("Error inesperado al validar la nueva API Key: %s", ex)
84
+ errors["base"] = "unknown"
85
+
86
+ # Validar que los límites sean números positivos
87
+ if not cv.positive_int(self.limit_xema) or not cv.positive_int(self.limit_prediccio):
88
+ errors["base"] = "invalid_limit"
59
89
 
60
90
  if not errors:
61
- # Actualizar la configuración de la entrada con la nueva API Key
91
+ # Actualizar la configuración de la entrada con la nueva API Key y límites
92
+ data_update = {}
93
+ if self.api_key:
94
+ data_update[CONF_API_KEY] = self.api_key
95
+ if self.limit_xema:
96
+ data_update[LIMIT_XEMA] = self.limit_xema
97
+ if self.limit_prediccio:
98
+ data_update[LIMIT_PREDICCIO] = self.limit_prediccio
99
+
62
100
  self.hass.config_entries.async_update_entry(
63
101
  self._config_entry,
64
- data={**self._config_entry.data, CONF_API_KEY: self.api_key},
102
+ data={**self._config_entry.data, **data_update},
65
103
  )
104
+ # Recargar la integración para aplicar los cambios dinámicamente
105
+ await self.hass.config_entries.async_reload(self._config_entry.entry_id)
106
+
66
107
  return self.async_create_entry(title="", data={})
67
108
 
68
- schema = vol.Schema({vol.Required(CONF_API_KEY): str})
109
+ schema = vol.Schema({
110
+ vol.Required(CONF_API_KEY): str,
111
+ vol.Required(LIMIT_XEMA, default=self._config_entry.data.get(LIMIT_XEMA)): cv.positive_int,
112
+ vol.Required(LIMIT_PREDICCIO, default=self._config_entry.data.get(LIMIT_PREDICCIO)): cv.positive_int,
113
+ })
69
114
  return self.async_show_form(
70
- step_id="update_api_key", data_schema=schema, errors=errors
115
+ step_id="update_api_and_limits", data_schema=schema, errors=errors
71
116
  )
117
+
118
+ async def async_step_update_limits_only(self, user_input: dict | None = None):
119
+ """Permite al usuario actualizar solo los límites de la API."""
120
+ errors = {}
121
+
122
+ if user_input is not None:
123
+ self.limit_xema = user_input.get(LIMIT_XEMA)
124
+ self.limit_prediccio = user_input.get(LIMIT_PREDICCIO)
125
+
126
+ # Validar que los límites sean números positivos
127
+ if not cv.positive_int(self.limit_xema) or not cv.positive_int(self.limit_prediccio):
128
+ errors["base"] = "invalid_limit"
129
+
130
+ if not errors:
131
+ self.hass.config_entries.async_update_entry(
132
+ self._config_entry,
133
+ data={
134
+ **self._config_entry.data,
135
+ LIMIT_XEMA: self.limit_xema,
136
+ LIMIT_PREDICCIO: self.limit_prediccio
137
+ },
138
+ )
139
+ # Recargar la integración para aplicar los cambios dinámicamente
140
+ await self.hass.config_entries.async_reload(self._config_entry.entry_id)
141
+
142
+ return self.async_create_entry(title="", data={})
143
+
144
+ schema = vol.Schema({
145
+ vol.Required(LIMIT_XEMA, default=self._config_entry.data.get(LIMIT_XEMA)): cv.positive_int,
146
+ vol.Required(LIMIT_PREDICCIO, default=self._config_entry.data.get(LIMIT_PREDICCIO)): cv.positive_int,
147
+ })
148
+ return self.async_show_form(
149
+ step_id="update_limits_only", data_schema=schema, errors=errors
150
+ )