meteocat 2.1.0 → 2.2.3

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.
@@ -10,7 +10,10 @@ import voluptuous as vol
10
10
  from .const import (
11
11
  CONF_API_KEY,
12
12
  LIMIT_XEMA,
13
- LIMIT_PREDICCIO
13
+ LIMIT_PREDICCIO,
14
+ LIMIT_XDDE,
15
+ LIMIT_QUOTA,
16
+ LIMIT_BASIC
14
17
  )
15
18
  from meteocatpy.town import MeteocatTown
16
19
  from meteocatpy.exceptions import (
@@ -64,6 +67,9 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
64
67
  self.api_key = user_input.get(CONF_API_KEY)
65
68
  self.limit_xema = user_input.get(LIMIT_XEMA)
66
69
  self.limit_prediccio = user_input.get(LIMIT_PREDICCIO)
70
+ self.limit_xdde = user_input.get(LIMIT_XDDE)
71
+ self.limit_quota = user_input.get(LIMIT_QUOTA)
72
+ self.limit_basic = user_input.get(LIMIT_BASIC)
67
73
 
68
74
  # Validar la nueva API Key utilizando MeteocatTown
69
75
  if self.api_key:
@@ -84,7 +90,8 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
84
90
  errors["base"] = "unknown"
85
91
 
86
92
  # 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):
93
+ limits_to_validate = [self.limit_xema, self.limit_prediccio, self.limit_xdde, self.limit_quota, self.limit_basic]
94
+ if not all(cv.positive_int(limit) for limit in limits_to_validate if limit is not None):
88
95
  errors["base"] = "invalid_limit"
89
96
 
90
97
  if not errors:
@@ -96,6 +103,12 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
96
103
  data_update[LIMIT_XEMA] = self.limit_xema
97
104
  if self.limit_prediccio:
98
105
  data_update[LIMIT_PREDICCIO] = self.limit_prediccio
106
+ if self.limit_xdde:
107
+ data_update[LIMIT_XDDE] = self.limit_xdde
108
+ if self.limit_quota:
109
+ data_update[LIMIT_QUOTA] = self.limit_quota
110
+ if self.limit_basic:
111
+ data_update[LIMIT_BASIC] = self.limit_basic
99
112
 
100
113
  self.hass.config_entries.async_update_entry(
101
114
  self._config_entry,
@@ -110,6 +123,9 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
110
123
  vol.Required(CONF_API_KEY): str,
111
124
  vol.Required(LIMIT_XEMA, default=self._config_entry.data.get(LIMIT_XEMA)): cv.positive_int,
112
125
  vol.Required(LIMIT_PREDICCIO, default=self._config_entry.data.get(LIMIT_PREDICCIO)): cv.positive_int,
126
+ vol.Required(LIMIT_XDDE, default=self._config_entry.data.get(LIMIT_XDDE)): cv.positive_int,
127
+ vol.Required(LIMIT_QUOTA, default=self._config_entry.data.get(LIMIT_QUOTA)): cv.positive_int,
128
+ vol.Required(LIMIT_BASIC, default=self._config_entry.data.get(LIMIT_BASIC)): cv.positive_int,
113
129
  })
114
130
  return self.async_show_form(
115
131
  step_id="update_api_and_limits", data_schema=schema, errors=errors
@@ -122,9 +138,13 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
122
138
  if user_input is not None:
123
139
  self.limit_xema = user_input.get(LIMIT_XEMA)
124
140
  self.limit_prediccio = user_input.get(LIMIT_PREDICCIO)
141
+ self.limit_xdde = user_input.get(LIMIT_XDDE)
142
+ self.limit_quota = user_input.get(LIMIT_QUOTA)
143
+ self.limit_basic = user_input.get(LIMIT_BASIC)
125
144
 
126
145
  # 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):
146
+ limits_to_validate = [self.limit_xema, self.limit_prediccio, self.limit_xdde, self.limit_quota, self.limit_basic]
147
+ if not all(cv.positive_int(limit) for limit in limits_to_validate if limit is not None):
128
148
  errors["base"] = "invalid_limit"
129
149
 
130
150
  if not errors:
@@ -133,7 +153,10 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
133
153
  data={
134
154
  **self._config_entry.data,
135
155
  LIMIT_XEMA: self.limit_xema,
136
- LIMIT_PREDICCIO: self.limit_prediccio
156
+ LIMIT_PREDICCIO: self.limit_prediccio,
157
+ LIMIT_XDDE: self.limit_xdde,
158
+ LIMIT_QUOTA: self.limit_quota,
159
+ LIMIT_BASIC: self.limit_basic
137
160
  },
138
161
  )
139
162
  # Recargar la integración para aplicar los cambios dinámicamente
@@ -144,6 +167,9 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
144
167
  schema = vol.Schema({
145
168
  vol.Required(LIMIT_XEMA, default=self._config_entry.data.get(LIMIT_XEMA)): cv.positive_int,
146
169
  vol.Required(LIMIT_PREDICCIO, default=self._config_entry.data.get(LIMIT_PREDICCIO)): cv.positive_int,
170
+ vol.Required(LIMIT_XDDE, default=self._config_entry.data.get(LIMIT_XDDE)): cv.positive_int,
171
+ vol.Required(LIMIT_QUOTA, default=self._config_entry.data.get(LIMIT_QUOTA)): cv.positive_int,
172
+ vol.Required(LIMIT_BASIC, default=self._config_entry.data.get(LIMIT_BASIC)): cv.positive_int,
147
173
  })
148
174
  return self.async_show_form(
149
175
  step_id="update_limits_only", data_schema=schema, errors=errors
@@ -39,6 +39,8 @@ from .const import (
39
39
  TOWN_ID,
40
40
  STATION_NAME,
41
41
  STATION_ID,
42
+ REGION_NAME,
43
+ REGION_ID,
42
44
  WIND_SPEED,
43
45
  WIND_DIRECTION,
44
46
  WIND_DIRECTION_CARDINAL,
@@ -60,6 +62,12 @@ from .const import (
60
62
  HOURLY_FORECAST_FILE_STATUS,
61
63
  DAILY_FORECAST_FILE_STATUS,
62
64
  UVI_FILE_STATUS,
65
+ QUOTA_FILE_STATUS,
66
+ QUOTA_XDDE,
67
+ QUOTA_PREDICCIO,
68
+ QUOTA_BASIC,
69
+ QUOTA_XEMA,
70
+ QUOTA_QUERIES,
63
71
  ALERTS,
64
72
  ALERT_FILE_STATUS,
65
73
  ALERT_WIND,
@@ -70,6 +78,9 @@ from .const import (
70
78
  ALERT_WARM,
71
79
  ALERT_WARM_NIGHT,
72
80
  ALERT_SNOW,
81
+ LIGHTNING_FILE_STATUS,
82
+ LIGHTNING_REGION,
83
+ LIGHTNING_TOWN,
73
84
  WIND_SPEED_CODE,
74
85
  WIND_DIRECTION_CODE,
75
86
  TEMPERATURE_CODE,
@@ -86,6 +97,12 @@ from .const import (
86
97
  DEFAULT_VALIDITY_HOURS,
87
98
  DEFAULT_VALIDITY_MINUTES,
88
99
  DEFAULT_ALERT_VALIDITY_TIME,
100
+ DEFAULT_QUOTES_VALIDITY_TIME,
101
+ ALERT_VALIDITY_MULTIPLIER_100,
102
+ ALERT_VALIDITY_MULTIPLIER_200,
103
+ ALERT_VALIDITY_MULTIPLIER_500,
104
+ ALERT_VALIDITY_MULTIPLIER_DEFAULT,
105
+ DEFAULT_LIGHTNING_VALIDITY_TIME,
89
106
  )
90
107
 
91
108
  from .coordinator import (
@@ -99,6 +116,10 @@ from .coordinator import (
99
116
  MeteocatUviCoordinator,
100
117
  MeteocatAlertsCoordinator,
101
118
  MeteocatAlertsRegionCoordinator,
119
+ MeteocatQuotesCoordinator,
120
+ MeteocatQuotesFileCoordinator,
121
+ MeteocatLightningCoordinator,
122
+ MeteocatLightningFileCoordinator,
102
123
  )
103
124
 
104
125
  # Definir la zona horaria local
@@ -224,6 +245,16 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
224
245
  state_class=SensorStateClass.MEASUREMENT,
225
246
  native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
226
247
  ),
248
+ MeteocatSensorEntityDescription(
249
+ key=LIGHTNING_REGION,
250
+ translation_key="lightning_region",
251
+ icon="mdi:weather-lightning",
252
+ ),
253
+ MeteocatSensorEntityDescription(
254
+ key=LIGHTNING_TOWN,
255
+ translation_key="lightning_town",
256
+ icon="mdi:weather-lightning",
257
+ ),
227
258
  # Sensores estáticos
228
259
  MeteocatSensorEntityDescription(
229
260
  key=TOWN_NAME,
@@ -249,6 +280,18 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
249
280
  icon="mdi:identifier",
250
281
  entity_category=EntityCategory.DIAGNOSTIC,
251
282
  ),
283
+ MeteocatSensorEntityDescription(
284
+ key=REGION_NAME,
285
+ translation_key="region_name",
286
+ icon="mdi:broadcast",
287
+ entity_category=EntityCategory.DIAGNOSTIC,
288
+ ),
289
+ MeteocatSensorEntityDescription(
290
+ key=REGION_ID,
291
+ translation_key="region_id",
292
+ icon="mdi:identifier",
293
+ entity_category=EntityCategory.DIAGNOSTIC,
294
+ ),
252
295
  MeteocatSensorEntityDescription(
253
296
  key=STATION_TIMESTAMP,
254
297
  translation_key="station_timestamp",
@@ -294,6 +337,18 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
294
337
  icon="mdi:update",
295
338
  entity_category=EntityCategory.DIAGNOSTIC,
296
339
  ),
340
+ MeteocatSensorEntityDescription(
341
+ key=QUOTA_FILE_STATUS,
342
+ translation_key="quota_file_status",
343
+ icon="mdi:update",
344
+ entity_category=EntityCategory.DIAGNOSTIC,
345
+ ),
346
+ MeteocatSensorEntityDescription(
347
+ key=LIGHTNING_FILE_STATUS,
348
+ translation_key="lightning_file_status",
349
+ icon="mdi:update",
350
+ entity_category=EntityCategory.DIAGNOSTIC,
351
+ ),
297
352
  MeteocatSensorEntityDescription(
298
353
  key=ALERTS,
299
354
  translation_key="alerts",
@@ -344,6 +399,36 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
344
399
  key=ALERT_SNOW,
345
400
  translation_key="alert_snow",
346
401
  icon="mdi:alert-outline",
402
+ ),
403
+ MeteocatSensorEntityDescription(
404
+ key=QUOTA_XDDE,
405
+ translation_key="quota_xdde",
406
+ icon="mdi:counter",
407
+ entity_category=EntityCategory.DIAGNOSTIC,
408
+ ),
409
+ MeteocatSensorEntityDescription(
410
+ key=QUOTA_PREDICCIO,
411
+ translation_key="quota_prediccio",
412
+ icon="mdi:counter",
413
+ entity_category=EntityCategory.DIAGNOSTIC,
414
+ ),
415
+ MeteocatSensorEntityDescription(
416
+ key=QUOTA_BASIC,
417
+ translation_key="quota_basic",
418
+ icon="mdi:counter",
419
+ entity_category=EntityCategory.DIAGNOSTIC,
420
+ ),
421
+ MeteocatSensorEntityDescription(
422
+ key=QUOTA_XEMA,
423
+ translation_key="quota_xema",
424
+ icon="mdi:counter",
425
+ entity_category=EntityCategory.DIAGNOSTIC,
426
+ ),
427
+ MeteocatSensorEntityDescription(
428
+ key=QUOTA_QUERIES,
429
+ translation_key="quota_queries",
430
+ icon="mdi:counter",
431
+ entity_category=EntityCategory.DIAGNOSTIC,
347
432
  )
348
433
  )
349
434
 
@@ -363,6 +448,10 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
363
448
  uvi_coordinator = entry_data.get("uvi_coordinator")
364
449
  alerts_coordinator = entry_data.get("alerts_coordinator")
365
450
  alerts_region_coordinator = entry_data.get("alerts_region_coordinator")
451
+ quotes_coordinator = entry_data.get("quotes_coordinator")
452
+ quotes_file_coordinator = entry_data.get("quotes_file_coordinator")
453
+ lightning_coordinator = entry_data.get("lightning_coordinator")
454
+ lightning_file_coordinator = entry_data.get("lightning_file_coordinator")
366
455
 
367
456
  # Sensores generales
368
457
  async_add_entities(
@@ -375,7 +464,7 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
375
464
  async_add_entities(
376
465
  MeteocatStaticSensor(static_sensor_coordinator, description, entry_data)
377
466
  for description in SENSOR_TYPES
378
- if description.key in {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID}
467
+ if description.key in {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID, REGION_NAME, REGION_ID}
379
468
  )
380
469
 
381
470
  # Sensor UVI
@@ -448,6 +537,34 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
448
537
  if description.key in {ALERT_WIND, ALERT_RAIN_INTENSITY, ALERT_RAIN, ALERT_SEA, ALERT_COLD, ALERT_WARM, ALERT_WARM_NIGHT, ALERT_SNOW}
449
538
  )
450
539
 
540
+ # Sensores de estado de cuotas
541
+ async_add_entities(
542
+ MeteocatQuotaStatusSensor(quotes_coordinator, description, entry_data)
543
+ for description in SENSOR_TYPES
544
+ if description.key == QUOTA_FILE_STATUS
545
+ )
546
+
547
+ # Sensores cuotas
548
+ async_add_entities(
549
+ MeteocatQuotaSensor(quotes_file_coordinator, description, entry_data)
550
+ for description in SENSOR_TYPES
551
+ if description.key in {QUOTA_XDDE, QUOTA_PREDICCIO, QUOTA_BASIC, QUOTA_XEMA, QUOTA_QUERIES}
552
+ )
553
+
554
+ # Sensores de estado de rayos
555
+ async_add_entities(
556
+ MeteocatLightningStatusSensor(lightning_coordinator, description, entry_data)
557
+ for description in SENSOR_TYPES
558
+ if description.key == LIGHTNING_FILE_STATUS
559
+ )
560
+
561
+ # Sensores de rayos en comarca y municipio
562
+ async_add_entities(
563
+ MeteocatLightningSensor(lightning_file_coordinator, description, entry_data)
564
+ for description in SENSOR_TYPES
565
+ if description.key in {LIGHTNING_REGION, LIGHTNING_TOWN}
566
+ )
567
+
451
568
  # Cambiar UTC a la zona horaria local
452
569
  def convert_to_local_time(utc_time: str, local_tz: str = "Europe/Madrid") -> datetime | None:
453
570
  """
@@ -473,7 +590,7 @@ def convert_to_local_time(utc_time: str, local_tz: str = "Europe/Madrid") -> dat
473
590
 
474
591
  class MeteocatStaticSensor(CoordinatorEntity[MeteocatStaticSensorCoordinator], SensorEntity):
475
592
  """Representation of a static Meteocat sensor."""
476
- STATIC_KEYS = {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID}
593
+ STATIC_KEYS = {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID, REGION_NAME, REGION_ID}
477
594
 
478
595
  _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
479
596
 
@@ -485,6 +602,8 @@ class MeteocatStaticSensor(CoordinatorEntity[MeteocatStaticSensorCoordinator], S
485
602
  self._town_id = entry_data["town_id"]
486
603
  self._station_name = entry_data["station_name"]
487
604
  self._station_id = entry_data["station_id"]
605
+ self._region_name = entry_data["region_name"]
606
+ self._region_id = entry_data["region_id"]
488
607
 
489
608
  # Unique ID for the entity
490
609
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._station_id}_{self.entity_description.key}"
@@ -513,6 +632,10 @@ class MeteocatStaticSensor(CoordinatorEntity[MeteocatStaticSensorCoordinator], S
513
632
  return self._station_name
514
633
  if self.entity_description.key == STATION_ID:
515
634
  return self._station_id
635
+ if self.entity_description.key == REGION_NAME:
636
+ return self._region_name
637
+ if self.entity_description.key == REGION_ID:
638
+ return self._region_id
516
639
 
517
640
  @property
518
641
  def device_info(self) -> DeviceInfo:
@@ -1136,6 +1259,7 @@ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], Se
1136
1259
  self._town_id = entry_data["town_id"]
1137
1260
  self._station_id = entry_data["station_id"]
1138
1261
  self._region_id = entry_data["region_id"]
1262
+ self._limit_prediccio = entry_data["limit_prediccio"]
1139
1263
 
1140
1264
  # Unique ID for the entity
1141
1265
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._region_id}_alert_status"
@@ -1152,6 +1276,19 @@ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], Se
1152
1276
  except ValueError:
1153
1277
  _LOGGER.error("Formato de fecha de actualización inválido: %s", data_update)
1154
1278
  return None
1279
+
1280
+ def _get_validity_duration(self):
1281
+ """Calcula la duración de validez basada en el límite de predicción."""
1282
+ if self._limit_prediccio <= 100:
1283
+ multiplier = ALERT_VALIDITY_MULTIPLIER_100
1284
+ elif 100 < self._limit_prediccio <= 200:
1285
+ multiplier = ALERT_VALIDITY_MULTIPLIER_200
1286
+ elif 200 < self._limit_prediccio <= 500:
1287
+ multiplier = ALERT_VALIDITY_MULTIPLIER_500
1288
+ else:
1289
+ multiplier = ALERT_VALIDITY_MULTIPLIER_DEFAULT
1290
+
1291
+ return timedelta(minutes=DEFAULT_ALERT_VALIDITY_TIME * multiplier)
1155
1292
 
1156
1293
  @property
1157
1294
  def native_value(self):
@@ -1161,11 +1298,11 @@ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], Se
1161
1298
  return "unknown"
1162
1299
 
1163
1300
  current_time = datetime.now(ZoneInfo("UTC"))
1301
+ validity_duration = self._get_validity_duration()
1164
1302
 
1165
1303
  # Comprobar si el archivo de alertas está obsoleto
1166
- if current_time - data_update >= timedelta(hours=DEFAULT_ALERT_VALIDITY_TIME):
1304
+ if current_time - data_update >= validity_duration:
1167
1305
  return "obsolete"
1168
-
1169
1306
  return "updated"
1170
1307
 
1171
1308
  @property
@@ -1370,3 +1507,282 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1370
1507
  manufacturer="Meteocat",
1371
1508
  model="Meteocat API",
1372
1509
  )
1510
+
1511
+ class MeteocatQuotaStatusSensor(CoordinatorEntity[MeteocatQuotesCoordinator], SensorEntity):
1512
+ _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1513
+
1514
+ def __init__(self, quotes_coordinator, description, entry_data):
1515
+ super().__init__(quotes_coordinator)
1516
+ self.entity_description = description
1517
+ self._town_name = entry_data["town_name"]
1518
+ self._town_id = entry_data["town_id"]
1519
+ self._station_id = entry_data["station_id"]
1520
+
1521
+ # Unique ID for the entity
1522
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_quota_status"
1523
+
1524
+ # Assign entity_category if defined in the description
1525
+ self._attr_entity_category = getattr(description, "entity_category", None)
1526
+
1527
+ def _get_data_update(self):
1528
+ """Obtiene la fecha de actualización directamente desde el coordinador."""
1529
+ data_update = self.coordinator.data.get("actualizado")
1530
+ if data_update:
1531
+ try:
1532
+ return datetime.fromisoformat(data_update.rstrip("Z"))
1533
+ except ValueError:
1534
+ _LOGGER.error("Formato de fecha de actualización inválido: %s", data_update)
1535
+ return None
1536
+
1537
+ @property
1538
+ def native_value(self):
1539
+ """Devuelve el estado actual de las alertas basado en la fecha de actualización."""
1540
+ data_update = self._get_data_update()
1541
+ if not data_update:
1542
+ return "unknown"
1543
+
1544
+ current_time = datetime.now(ZoneInfo("UTC"))
1545
+
1546
+ # Comprobar si el archivo de alertas está obsoleto
1547
+ if current_time - data_update >= timedelta(minutes=DEFAULT_QUOTES_VALIDITY_TIME):
1548
+ return "obsolete"
1549
+
1550
+ return "updated"
1551
+
1552
+ @property
1553
+ def extra_state_attributes(self):
1554
+ """Devuelve los atributos adicionales del estado."""
1555
+ attributes = super().extra_state_attributes or {}
1556
+ data_update = self._get_data_update()
1557
+ if data_update:
1558
+ attributes["update_date"] = data_update.isoformat()
1559
+ return attributes
1560
+
1561
+ @property
1562
+ def device_info(self) -> DeviceInfo:
1563
+ """Devuelve la información del dispositivo."""
1564
+ return DeviceInfo(
1565
+ identifiers={(DOMAIN, self._town_id)},
1566
+ name=f"Meteocat {self._station_id} {self._town_name}",
1567
+ manufacturer="Meteocat",
1568
+ model="Meteocat API",
1569
+ )
1570
+
1571
+ class MeteocatQuotaSensor(CoordinatorEntity[MeteocatQuotesFileCoordinator], SensorEntity):
1572
+ """Representation of Meteocat Quota sensors."""
1573
+
1574
+ # Mapeo de claves en sensor.py a nombres en quotes.json
1575
+ QUOTA_MAPPING = {
1576
+ "quota_xdde": "XDDE",
1577
+ "quota_prediccio": "Prediccio",
1578
+ "quota_basic": "Basic",
1579
+ "quota_xema": "XEMA",
1580
+ "quota_queries": "Quota",
1581
+ }
1582
+
1583
+ # Mapeo de periodos para facilitar la traducción del estado
1584
+ PERIOD_STATE_MAPPING = {
1585
+ "Setmanal": "weekly",
1586
+ "Mensual": "monthly",
1587
+ "Anual": "annual",
1588
+ }
1589
+
1590
+ _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1591
+
1592
+ def __init__(self, quotes_file_coordinator, description, entry_data):
1593
+ super().__init__(quotes_file_coordinator)
1594
+ self.entity_description = description
1595
+ self._town_name = entry_data["town_name"]
1596
+ self._town_id = entry_data["town_id"]
1597
+ self._station_id = entry_data["station_id"]
1598
+
1599
+ # Unique ID for the entity
1600
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
1601
+
1602
+ # Assign entity_category if defined in the description
1603
+ self._attr_entity_category = getattr(description, "entity_category", None)
1604
+
1605
+ def _get_plan_data(self):
1606
+ """Encuentra los datos del plan correspondiente al sensor actual."""
1607
+ if not self.coordinator.data:
1608
+ return None
1609
+
1610
+ plan_name = self.QUOTA_MAPPING.get(self.entity_description.key)
1611
+
1612
+ if not plan_name:
1613
+ _LOGGER.error(f"No se encontró un mapeo para la clave: {self.entity_description.key}")
1614
+ return None
1615
+
1616
+ for plan in self.coordinator.data.get("plans", []):
1617
+ if plan.get("nom") == plan_name:
1618
+ return plan # Retorna el plan encontrado
1619
+
1620
+ _LOGGER.warning(f"No se encontró el plan '{plan_name}' en los datos del coordinador.")
1621
+ return None
1622
+
1623
+ @property
1624
+ def native_value(self):
1625
+ """Devuelve el estado de la cuota: 'ok' si no se ha excedido, 'exceeded' si se ha superado."""
1626
+ plan = self._get_plan_data()
1627
+
1628
+ if not plan:
1629
+ return None
1630
+
1631
+ max_consultes = plan.get("maxConsultes")
1632
+ consultes_realitzades = plan.get("consultesRealitzades")
1633
+
1634
+ if max_consultes is None or consultes_realitzades is None or \
1635
+ not isinstance(max_consultes, (int, float)) or not isinstance(consultes_realitzades, (int, float)):
1636
+ _LOGGER.warning(f"Datos inválidos para el plan '{plan.get('nom', 'unknown')}': {plan}")
1637
+ return None
1638
+
1639
+ return "ok" if consultes_realitzades <= max_consultes else "exceeded"
1640
+
1641
+ @property
1642
+ def extra_state_attributes(self):
1643
+ """Devuelve atributos adicionales del estado del sensor."""
1644
+ attributes = super().extra_state_attributes or {}
1645
+ plan = self._get_plan_data()
1646
+
1647
+ if not plan:
1648
+ return {}
1649
+
1650
+ # Aplicar el mapeo de periodos
1651
+ period = plan.get("periode", "desconocido")
1652
+ translated_period = self.PERIOD_STATE_MAPPING.get(period, period) # Si no está en el mapping, dejar el original
1653
+
1654
+ attributes.update({
1655
+ "period": translated_period,
1656
+ "max_queries": plan.get("maxConsultes"),
1657
+ "made_queries": plan.get("consultesRealitzades"),
1658
+ "remain_queries": plan.get("consultesRestants"),
1659
+ })
1660
+
1661
+ return attributes
1662
+
1663
+ @property
1664
+ def device_info(self) -> DeviceInfo:
1665
+ """Devuelve la información del dispositivo."""
1666
+ return DeviceInfo(
1667
+ identifiers={(DOMAIN, self._town_id)},
1668
+ name=f"Meteocat {self._station_id} {self._town_name}",
1669
+ manufacturer="Meteocat",
1670
+ model="Meteocat API",
1671
+ )
1672
+
1673
+ class MeteocatLightningStatusSensor(CoordinatorEntity[MeteocatLightningCoordinator], SensorEntity):
1674
+ _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1675
+
1676
+ def __init__(self, lightning_coordinator, description, entry_data):
1677
+ super().__init__(lightning_coordinator)
1678
+ self.entity_description = description
1679
+ self._town_name = entry_data["town_name"]
1680
+ self._town_id = entry_data["town_id"]
1681
+ self._station_id = entry_data["station_id"]
1682
+
1683
+ # Unique ID for the entity
1684
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_lightning_status"
1685
+
1686
+ # Assign entity_category if defined in the description
1687
+ self._attr_entity_category = getattr(description, "entity_category", None)
1688
+
1689
+ def _get_data_update(self):
1690
+ """Obtiene la fecha de actualización directamente desde el coordinador."""
1691
+ data_update = self.coordinator.data.get("actualizado")
1692
+ if data_update:
1693
+ try:
1694
+ return datetime.fromisoformat(data_update.rstrip("Z"))
1695
+ except ValueError:
1696
+ _LOGGER.error("Formato de fecha de actualización inválido: %s", data_update)
1697
+ return None
1698
+
1699
+ @property
1700
+ def native_value(self):
1701
+ """Devuelve el estado actual de las alertas basado en la fecha de actualización."""
1702
+ data_update = self._get_data_update()
1703
+ if not data_update:
1704
+ return "unknown"
1705
+
1706
+ current_time = datetime.now(ZoneInfo("UTC"))
1707
+
1708
+ # Comprobar si el archivo de alertas está obsoleto
1709
+ if current_time - data_update >= timedelta(minutes=DEFAULT_LIGHTNING_VALIDITY_TIME):
1710
+ return "obsolete"
1711
+
1712
+ return "updated"
1713
+
1714
+ @property
1715
+ def extra_state_attributes(self):
1716
+ """Devuelve los atributos adicionales del estado."""
1717
+ attributes = super().extra_state_attributes or {}
1718
+ data_update = self._get_data_update()
1719
+ if data_update:
1720
+ attributes["update_date"] = data_update.isoformat()
1721
+ return attributes
1722
+
1723
+ @property
1724
+ def device_info(self) -> DeviceInfo:
1725
+ """Devuelve la información del dispositivo."""
1726
+ return DeviceInfo(
1727
+ identifiers={(DOMAIN, self._town_id)},
1728
+ name=f"Meteocat {self._station_id} {self._town_name}",
1729
+ manufacturer="Meteocat",
1730
+ model="Meteocat API",
1731
+ )
1732
+
1733
+ class MeteocatLightningSensor(CoordinatorEntity[MeteocatLightningFileCoordinator], SensorEntity):
1734
+ """Representation of Meteocat Lightning sensors."""
1735
+
1736
+ _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1737
+
1738
+ def __init__(self, lightning_file_coordinator, description, entry_data):
1739
+ super().__init__(lightning_file_coordinator)
1740
+ self.entity_description = description
1741
+ self._town_name = entry_data["town_name"]
1742
+ self._town_id = entry_data["town_id"]
1743
+ self._station_id = entry_data["station_id"]
1744
+ self._region_id = entry_data["region_id"]
1745
+
1746
+ # Unique ID for the entity
1747
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
1748
+
1749
+ # Assign entity_category if defined in the description
1750
+ self._attr_entity_category = getattr(description, "entity_category", None)
1751
+
1752
+ @property
1753
+ def native_value(self):
1754
+ """Return the total number of lightning strikes."""
1755
+ if self.entity_description.key == LIGHTNING_REGION:
1756
+ return self.coordinator.data.get("region", {}).get("total", 0)
1757
+ elif self.entity_description.key == LIGHTNING_TOWN:
1758
+ return self.coordinator.data.get("town", {}).get("total", 0)
1759
+ return None
1760
+
1761
+ @property
1762
+ def extra_state_attributes(self):
1763
+ """Return additional attributes for the sensor."""
1764
+ attributes = super().extra_state_attributes or {}
1765
+ if self.entity_description.key == LIGHTNING_REGION:
1766
+ data = self.coordinator.data.get("region", {})
1767
+ elif self.entity_description.key == LIGHTNING_TOWN:
1768
+ data = self.coordinator.data.get("town", {})
1769
+ else:
1770
+ return attributes
1771
+
1772
+ # Agregar atributos específicos
1773
+ attributes.update({
1774
+ "cloud_cloud": data.get("cc", 0),
1775
+ "cloud_ground_neg": data.get("cg-", 0),
1776
+ "cloud_ground_pos": data.get("cg+", 0),
1777
+ })
1778
+ return attributes
1779
+
1780
+ @property
1781
+ def device_info(self) -> DeviceInfo:
1782
+ """Return the device info."""
1783
+ return DeviceInfo(
1784
+ identifiers={(DOMAIN, self._town_id)},
1785
+ name=f"Meteocat {self._station_id} {self._town_name}",
1786
+ manufacturer="Meteocat",
1787
+ model="Meteocat API",
1788
+ )