meteocat 1.1.1 → 2.0.1

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.
@@ -1,8 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from dataclasses import dataclass
4
- from datetime import datetime, timezone, time
5
- from zoneinfo import ZoneInfo
4
+ from datetime import datetime, timezone, time, timedelta
5
+ from zoneinfo import ZoneInfo
6
+ import os
7
+ import json
8
+ import aiofiles
9
+ import asyncio
6
10
  import logging
7
11
  from homeassistant.helpers.entity import (
8
12
  DeviceInfo,
@@ -15,6 +19,7 @@ from homeassistant.components.sensor import (
15
19
  SensorStateClass,
16
20
  )
17
21
  from homeassistant.core import callback
22
+ from homeassistant.helpers.aiohttp_client import async_get_clientsession
18
23
  from homeassistant.helpers.entity_platform import AddEntitiesCallback
19
24
  from homeassistant.helpers.update_coordinator import CoordinatorEntity
20
25
  from homeassistant.const import (
@@ -55,6 +60,16 @@ from .const import (
55
60
  HOURLY_FORECAST_FILE_STATUS,
56
61
  DAILY_FORECAST_FILE_STATUS,
57
62
  UVI_FILE_STATUS,
63
+ ALERTS,
64
+ ALERT_FILE_STATUS,
65
+ ALERT_WIND,
66
+ ALERT_RAIN_INTENSITY,
67
+ ALERT_RAIN,
68
+ ALERT_SEA,
69
+ ALERT_COLD,
70
+ ALERT_WARM,
71
+ ALERT_WARM_NIGHT,
72
+ ALERT_SNOW,
58
73
  WIND_SPEED_CODE,
59
74
  WIND_DIRECTION_CODE,
60
75
  TEMPERATURE_CODE,
@@ -70,6 +85,7 @@ from .const import (
70
85
  DEFAULT_VALIDITY_DAYS,
71
86
  DEFAULT_VALIDITY_HOURS,
72
87
  DEFAULT_VALIDITY_MINUTES,
88
+ DEFAULT_ALERT_VALIDITY_TIME,
73
89
  )
74
90
 
75
91
  from .coordinator import (
@@ -81,6 +97,8 @@ from .coordinator import (
81
97
  MeteocatEntityCoordinator,
82
98
  DailyForecastCoordinator,
83
99
  MeteocatUviCoordinator,
100
+ MeteocatAlertsCoordinator,
101
+ MeteocatAlertsRegionCoordinator,
84
102
  )
85
103
 
86
104
  # Definir la zona horaria local
@@ -275,6 +293,57 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
275
293
  translation_key="uvi_file_status",
276
294
  icon="mdi:update",
277
295
  entity_category=EntityCategory.DIAGNOSTIC,
296
+ ),
297
+ MeteocatSensorEntityDescription(
298
+ key=ALERTS,
299
+ translation_key="alerts",
300
+ icon="mdi:alert-outline",
301
+ ),
302
+ MeteocatSensorEntityDescription(
303
+ key=ALERT_FILE_STATUS,
304
+ translation_key="alert_file_status",
305
+ icon="mdi:update",
306
+ entity_category=EntityCategory.DIAGNOSTIC,
307
+ ),
308
+ MeteocatSensorEntityDescription(
309
+ key=ALERT_WIND,
310
+ translation_key="alert_wind",
311
+ icon="mdi:alert-outline",
312
+ ),
313
+ MeteocatSensorEntityDescription(
314
+ key=ALERT_RAIN_INTENSITY,
315
+ translation_key="alert_rain_intensity",
316
+ icon="mdi:alert-outline",
317
+ ),
318
+ MeteocatSensorEntityDescription(
319
+ key=ALERT_RAIN,
320
+ translation_key="alert_rain",
321
+ icon="mdi:alert-outline",
322
+ ),
323
+ MeteocatSensorEntityDescription(
324
+ key=ALERT_SEA,
325
+ translation_key="alert_sea",
326
+ icon="mdi:alert-outline",
327
+ ),
328
+ MeteocatSensorEntityDescription(
329
+ key=ALERT_COLD,
330
+ translation_key="alert_cold",
331
+ icon="mdi:alert-outline",
332
+ ),
333
+ MeteocatSensorEntityDescription(
334
+ key=ALERT_WARM,
335
+ translation_key="alert_warm",
336
+ icon="mdi:alert-outline",
337
+ ),
338
+ MeteocatSensorEntityDescription(
339
+ key=ALERT_WARM_NIGHT,
340
+ translation_key="alert_warm_night",
341
+ icon="mdi:alert-outline",
342
+ ),
343
+ MeteocatSensorEntityDescription(
344
+ key=ALERT_SNOW,
345
+ translation_key="alert_snow",
346
+ icon="mdi:alert-outline",
278
347
  )
279
348
  )
280
349
 
@@ -292,6 +361,8 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
292
361
  temp_forecast_coordinator = entry_data.get("temp_forecast_coordinator")
293
362
  entity_coordinator = entry_data.get("entity_coordinator")
294
363
  uvi_coordinator = entry_data.get("uvi_coordinator")
364
+ alerts_coordinator = entry_data.get("alerts_coordinator")
365
+ alerts_region_coordinator = entry_data.get("alerts_region_coordinator")
295
366
 
296
367
  # Sensores generales
297
368
  async_add_entities(
@@ -356,6 +427,27 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
356
427
  if description.key == UVI_FILE_STATUS
357
428
  )
358
429
 
430
+ # Sensores de alertas
431
+ async_add_entities(
432
+ MeteocatAlertStatusSensor(alerts_coordinator, description, entry_data)
433
+ for description in SENSOR_TYPES
434
+ if description.key == ALERT_FILE_STATUS
435
+ )
436
+
437
+ # Sensores de alertas para la comarca
438
+ async_add_entities(
439
+ MeteocatAlertRegionSensor(alerts_region_coordinator, description, entry_data)
440
+ for description in SENSOR_TYPES
441
+ if description.key == ALERTS
442
+ )
443
+
444
+ # Sensores de alertas para cada meteor
445
+ async_add_entities(
446
+ MeteocatAlertMeteorSensor(alerts_region_coordinator, description, entry_data)
447
+ for description in SENSOR_TYPES
448
+ if description.key in {ALERT_WIND, ALERT_RAIN_INTENSITY, ALERT_RAIN, ALERT_SEA, ALERT_COLD, ALERT_WARM, ALERT_WARM_NIGHT, ALERT_SNOW}
449
+ )
450
+
359
451
  # Cambiar UTC a la zona horaria local
360
452
  def convert_to_local_time(utc_time: str, local_tz: str = "Europe/Madrid") -> datetime | None:
361
453
  """
@@ -1032,4 +1124,249 @@ class MeteocatUviStatusSensor(CoordinatorEntity[MeteocatUviCoordinator], SensorE
1032
1124
  name="Meteocat " + self._station_id + " " + self._town_name,
1033
1125
  manufacturer="Meteocat",
1034
1126
  model="Meteocat API",
1035
- )
1127
+ )
1128
+
1129
+ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], SensorEntity):
1130
+ _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1131
+
1132
+ def __init__(self, alerts_coordinator, description, entry_data):
1133
+ super().__init__(alerts_coordinator)
1134
+ self.entity_description = description
1135
+ self._town_name = entry_data["town_name"]
1136
+ self._town_id = entry_data["town_id"]
1137
+ self._station_id = entry_data["station_id"]
1138
+ self._region_id = entry_data["region_id"]
1139
+
1140
+ # Unique ID for the entity
1141
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._region_id}_alert_status"
1142
+
1143
+ # Assign entity_category if defined in the description
1144
+ self._attr_entity_category = getattr(description, "entity_category", None)
1145
+
1146
+ def _get_data_update(self):
1147
+ """Obtiene la fecha de actualización directamente desde el coordinador."""
1148
+ data_update = self.coordinator.data.get("actualizado")
1149
+ if data_update:
1150
+ try:
1151
+ return datetime.fromisoformat(data_update.rstrip("Z"))
1152
+ except ValueError:
1153
+ _LOGGER.error("Formato de fecha de actualización inválido: %s", data_update)
1154
+ return None
1155
+
1156
+ @property
1157
+ def native_value(self):
1158
+ """Devuelve el estado actual de las alertas basado en la fecha de actualización."""
1159
+ data_update = self._get_data_update()
1160
+ if not data_update:
1161
+ return "unknown"
1162
+
1163
+ current_time = datetime.now(ZoneInfo("UTC"))
1164
+
1165
+ # Comprobar si el archivo de alertas está obsoleto
1166
+ if current_time - data_update >= timedelta(hours=DEFAULT_ALERT_VALIDITY_TIME):
1167
+ return "obsolete"
1168
+
1169
+ return "updated"
1170
+
1171
+ @property
1172
+ def extra_state_attributes(self):
1173
+ """Devuelve los atributos adicionales del estado."""
1174
+ attributes = super().extra_state_attributes or {}
1175
+ data_update = self._get_data_update()
1176
+ if data_update:
1177
+ attributes["update_date"] = data_update.isoformat()
1178
+ return attributes
1179
+
1180
+ @property
1181
+ def device_info(self) -> DeviceInfo:
1182
+ """Devuelve la información del dispositivo."""
1183
+ return DeviceInfo(
1184
+ identifiers={(DOMAIN, self._town_id)},
1185
+ name=f"Meteocat {self._station_id} {self._town_name}",
1186
+ manufacturer="Meteocat",
1187
+ model="Meteocat API",
1188
+ )
1189
+
1190
+ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinator], SensorEntity):
1191
+ """Sensor dinámico que muestra el estado de las alertas por región."""
1192
+
1193
+ METEOR_MAPPING = {
1194
+ "Temps violent": "violent_weather",
1195
+ "Intensitat de pluja": "rain_intensity",
1196
+ "Acumulació de pluja": "rain_amount",
1197
+ "Neu acumulada en 24 hores": "snow_amount_24",
1198
+ "Vent": "wind",
1199
+ "Estat de la mar": "sea_state",
1200
+ "Fred": "cold",
1201
+ "Calor": "heat",
1202
+ "Calor nocturna": "night_heat",
1203
+ }
1204
+
1205
+ _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1206
+
1207
+ def __init__(self, alerts_region_coordinator, description, entry_data):
1208
+ super().__init__(alerts_region_coordinator)
1209
+ self.entity_description = description
1210
+ self._town_name = entry_data["town_name"]
1211
+ self._town_id = entry_data["town_id"]
1212
+ self._station_id = entry_data["station_id"]
1213
+ self._region_id = entry_data["region_id"]
1214
+
1215
+ # Unique ID for the entity
1216
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._region_id}_alerts"
1217
+
1218
+ # Assign entity_category if defined in the description
1219
+ self._attr_entity_category = getattr(description, "entity_category", None)
1220
+
1221
+ @property
1222
+ def native_value(self):
1223
+ """Devuelve el número de alertas activas."""
1224
+ return self.coordinator.data.get("activas", 0)
1225
+
1226
+ @property
1227
+ def extra_state_attributes(self):
1228
+ """Devuelve los atributos extra del sensor con los nombres traducidos."""
1229
+ meteor_details = self.coordinator.data.get("detalles", {}).get("meteor", {})
1230
+
1231
+ # Convertimos las claves al formato deseado usando el mapping
1232
+ attributes = {
1233
+ f"alert_{i+1}": self.METEOR_MAPPING.get(meteor, "unknown")
1234
+ for i, meteor in enumerate(meteor_details.keys())
1235
+ }
1236
+
1237
+ _LOGGER.info("Atributos traducidos del sensor: %s", attributes)
1238
+ return attributes
1239
+
1240
+ @property
1241
+ def device_info(self) -> DeviceInfo:
1242
+ """Devuelve la información del dispositivo."""
1243
+ return DeviceInfo(
1244
+ identifiers={(DOMAIN, self._town_id)},
1245
+ name="Meteocat " + self._station_id + " " + self._town_name,
1246
+ manufacturer="Meteocat",
1247
+ model="Meteocat API",
1248
+ )
1249
+
1250
+ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinator], SensorEntity):
1251
+ """Sensor dinámico que muestra el estado de las alertas por cada meteor para una región."""
1252
+ METEOR_MAPPING = {
1253
+ ALERT_WIND: "Vent",
1254
+ ALERT_RAIN_INTENSITY: "Intensitat de pluja",
1255
+ ALERT_RAIN: "Acumulació de pluja",
1256
+ ALERT_SEA: "Estat de la mar",
1257
+ ALERT_COLD: "Fred",
1258
+ ALERT_WARM: "Calor",
1259
+ ALERT_WARM_NIGHT: "Calor nocturna",
1260
+ ALERT_SNOW: "Neu acumulada en 24 hores",
1261
+ }
1262
+
1263
+ STATE_MAPPING = {
1264
+ "Obert": "opened",
1265
+ "Tancat": "closed",
1266
+ }
1267
+
1268
+ UMBRAL_MAPPING = {
1269
+ "Ratxes de vent > 25 m/s": "wind_gusts_25",
1270
+ "Esclafits": "microburst",
1271
+ "Tornados o mànegues": "tornadoes",
1272
+ "Ratxa màxima > 40m/s": "wind_40",
1273
+ "Ratxa màxima > 35m/s": "wind_35",
1274
+ "Ratxa màxima > 30m/s": "wind_30",
1275
+ "Ratxa màxima > 25m/s": "wind_25",
1276
+ "Ratxa màxima > 20m/s": "wind_20",
1277
+ "Pedra de diàmetre > 2 cm": "hail_2_cm",
1278
+ "Intensitat > 40 mm / 30 minuts": "intensity_40_30",
1279
+ "Intensitat > 20 mm / 30 minuts": "intensity_20_30",
1280
+ "Acumulada > 200 mm /24 hores": "rain_200_24",
1281
+ "Acumulada > 100 mm /24 hores": "rain_100_24",
1282
+ "Onades > 4.00 metres (mar brava)": "waves_4",
1283
+ "Onades > 2.50 metres (maregassa)": "waves_2_50",
1284
+ "Fred molt intens": "cold_very_intense",
1285
+ "Fred intens": "cold_intense",
1286
+ "Calor molt intensa": "heat_very_intense",
1287
+ "Calor intensa": "heat_intense",
1288
+ "Calor nocturna molt intensa": "heat_night_very_intense",
1289
+ "Calor nocturna intensa": "heat_night_intense",
1290
+ "gruix > 50 cm a cotes superiors a 1000 metres fins a 1500 metres": "thickness_50_at_1000",
1291
+ "gruix > 30 cm a cotes superiors a 800 metres fins a 1000 metres": "thickness_30_at_800",
1292
+ "gruix > 20 cm a cotes superiors a 600 metres fins a 800 metres": "thickness_20_at_600",
1293
+ "gruix > 20 cm a cotes superiors a 1000 metres fins a 1500 metres": "thickness_20_at_1000",
1294
+ "gruix > 15 cm a cotes superiors a 300 metres fins a 600 metres": "thickness_15_at_300",
1295
+ "gruix > 10 cm a cotes superiors a 800 metres fins a 1000 metres": "thickness_10_at_800",
1296
+ "gruix > 5 cm a cotes inferiors a 300 metres": "thickness_5_at_300",
1297
+ "gruix > 5 cm a cotes superiors a 600 metres fins a 800 metres": "thickness_5_at_600",
1298
+ "gruix > 2 cm a cotes superiors a 300 metres fins a 600 metres": "thickness_2_at_300",
1299
+ "gruix ≥ 0 cm a cotes inferiors a 300 metres": "thickness_0_at_300",
1300
+ }
1301
+
1302
+ _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1303
+
1304
+ def __init__(self, alerts_region_coordinator, description, entry_data):
1305
+ super().__init__(alerts_region_coordinator)
1306
+ self.entity_description = description
1307
+ self._town_name = entry_data["town_name"]
1308
+ self._town_id = entry_data["town_id"]
1309
+ self._station_id = entry_data["station_id"]
1310
+ self._region_id = entry_data["region_id"]
1311
+
1312
+ # Unique ID for the entity
1313
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._region_id}_{self.entity_description.key}"
1314
+
1315
+ # Assign entity_category if defined in the description
1316
+ self._attr_entity_category = getattr(description, "entity_category", None)
1317
+
1318
+ # Log para depuración
1319
+ _LOGGER.debug(
1320
+ "Inicializando sensor: %s, Unique ID: %s",
1321
+ self.entity_description.name,
1322
+ self._attr_unique_id,
1323
+ )
1324
+
1325
+ @property
1326
+ def native_value(self):
1327
+ """Devuelve el estado de la alerta específica."""
1328
+ meteor_type = self.METEOR_MAPPING.get(self.entity_description.key)
1329
+ if not meteor_type:
1330
+ return "Desconocido"
1331
+
1332
+ meteor_data = self.coordinator.data.get("detalles", {}).get("meteor", {}).get(meteor_type, {})
1333
+
1334
+ # Convertir estado para translation_key
1335
+ estado_original = meteor_data.get("estado", "Tancat")
1336
+ return self.STATE_MAPPING.get(estado_original, "unknown")
1337
+
1338
+ @property
1339
+ def extra_state_attributes(self):
1340
+ """Devuelve los atributos específicos de la alerta."""
1341
+ meteor_type = self.METEOR_MAPPING.get(self.entity_description.key)
1342
+ if not meteor_type:
1343
+ return {}
1344
+
1345
+ meteor_data = self.coordinator.data.get("detalles", {}).get("meteor", {}).get(meteor_type, {})
1346
+ if not meteor_data:
1347
+ return {}
1348
+
1349
+ # Convertir umbral para translation_key
1350
+ umbral_original = meteor_data.get("umbral")
1351
+ umbral_convertido = self.UMBRAL_MAPPING.get(umbral_original, "unknown")
1352
+
1353
+ return {
1354
+ "inicio": meteor_data.get("inicio"),
1355
+ "fin": meteor_data.get("fin"),
1356
+ "fecha": meteor_data.get("fecha"),
1357
+ "periodo": meteor_data.get("periodo"),
1358
+ "umbral": umbral_convertido,
1359
+ "nivel": meteor_data.get("nivel"),
1360
+ "peligro": meteor_data.get("peligro"),
1361
+ "comentario": meteor_data.get("comentario"),
1362
+ }
1363
+
1364
+ @property
1365
+ def device_info(self) -> DeviceInfo:
1366
+ """Devuelve la información del dispositivo."""
1367
+ return DeviceInfo(
1368
+ identifiers={(DOMAIN, self._town_id)},
1369
+ name="Meteocat " + self._station_id + " " + self._town_name,
1370
+ manufacturer="Meteocat",
1371
+ model="Meteocat API",
1372
+ )