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.
- package/CHANGELOG.md +18 -0
- package/README.md +1 -1
- package/custom_components/meteocat/__init__.py +29 -1
- package/custom_components/meteocat/config_flow.py +140 -1
- package/custom_components/meteocat/const.py +14 -0
- package/custom_components/meteocat/coordinator.py +524 -9
- package/custom_components/meteocat/manifest.json +2 -2
- package/custom_components/meteocat/options_flow.py +30 -4
- package/custom_components/meteocat/sensor.py +420 -4
- package/custom_components/meteocat/strings.json +197 -4
- package/custom_components/meteocat/translations/ca.json +197 -4
- package/custom_components/meteocat/translations/en.json +197 -4
- package/custom_components/meteocat/translations/es.json +197 -4
- package/custom_components/meteocat/version.py +1 -1
- package/images/api_limits.png +0 -0
- package/images/diagnostic_sensors.png +0 -0
- package/images/dynamic_sensors.png +0 -0
- package/package.json +1 -1
- package/poetry.lock +598 -586
- package/pyproject.toml +3 -3
|
@@ -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
|
-
|
|
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
|
-
|
|
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 >=
|
|
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
|
+
)
|