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.
- package/CHANGELOG.md +16 -0
- package/README.md +16 -2
- package/custom_components/meteocat/__init__.py +23 -1
- package/custom_components/meteocat/config_flow.py +56 -35
- package/custom_components/meteocat/const.py +21 -2
- package/custom_components/meteocat/coordinator.py +455 -0
- package/custom_components/meteocat/manifest.json +2 -2
- package/custom_components/meteocat/options_flow.py +104 -25
- package/custom_components/meteocat/sensor.py +275 -3
- package/custom_components/meteocat/strings.json +447 -2
- package/custom_components/meteocat/translations/ca.json +448 -3
- package/custom_components/meteocat/translations/en.json +447 -3
- package/custom_components/meteocat/translations/es.json +447 -2
- package/custom_components/meteocat/version.py +1 -1
- package/filetree.txt +1 -0
- package/images/api_limits.png +0 -0
- package/images/dynamic_sensors.png +0 -0
- package/images/pick_station.png +0 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
|
@@ -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,184 @@ 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
|
+
_attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
|
|
1193
|
+
|
|
1194
|
+
def __init__(self, alerts_region_coordinator, description, entry_data):
|
|
1195
|
+
super().__init__(alerts_region_coordinator)
|
|
1196
|
+
self.entity_description = description
|
|
1197
|
+
self._town_name = entry_data["town_name"]
|
|
1198
|
+
self._town_id = entry_data["town_id"]
|
|
1199
|
+
self._station_id = entry_data["station_id"]
|
|
1200
|
+
self._region_id = entry_data["region_id"]
|
|
1201
|
+
|
|
1202
|
+
# Unique ID for the entity
|
|
1203
|
+
self._attr_unique_id = f"sensor.{DOMAIN}_{self._region_id}_alerts"
|
|
1204
|
+
|
|
1205
|
+
# Assign entity_category if defined in the description
|
|
1206
|
+
self._attr_entity_category = getattr(description, "entity_category", None)
|
|
1207
|
+
|
|
1208
|
+
@property
|
|
1209
|
+
def native_value(self):
|
|
1210
|
+
"""Devuelve el número de alertas activas."""
|
|
1211
|
+
return self.coordinator.data.get("activas", 0)
|
|
1212
|
+
|
|
1213
|
+
@property
|
|
1214
|
+
def extra_state_attributes(self):
|
|
1215
|
+
"""Devuelve los atributos extra del sensor."""
|
|
1216
|
+
meteor_details = self.coordinator.data.get("detalles", {}).get("meteor", {})
|
|
1217
|
+
attributes = {f"alert_{i+1}": meteor for i, meteor in enumerate(meteor_details.keys())}
|
|
1218
|
+
_LOGGER.info("Atributos simplificados del sensor: %s", attributes)
|
|
1219
|
+
return attributes
|
|
1220
|
+
|
|
1221
|
+
@property
|
|
1222
|
+
def device_info(self) -> DeviceInfo:
|
|
1223
|
+
"""Devuelve la información del dispositivo."""
|
|
1224
|
+
return DeviceInfo(
|
|
1225
|
+
identifiers={(DOMAIN, self._town_id)},
|
|
1226
|
+
name="Meteocat " + self._station_id + " " + self._town_name,
|
|
1227
|
+
manufacturer="Meteocat",
|
|
1228
|
+
model="Meteocat API",
|
|
1229
|
+
)
|
|
1230
|
+
|
|
1231
|
+
class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinator], SensorEntity):
|
|
1232
|
+
"""Sensor dinámico que muestra el estado de las alertas por cada meteor para una región."""
|
|
1233
|
+
METEOR_MAPPING = {
|
|
1234
|
+
ALERT_WIND: "Vent",
|
|
1235
|
+
ALERT_RAIN_INTENSITY: "Intensitat de pluja",
|
|
1236
|
+
ALERT_RAIN: "Acumulació de pluja",
|
|
1237
|
+
ALERT_SEA: "Estat de la mar",
|
|
1238
|
+
ALERT_COLD: "Fred",
|
|
1239
|
+
ALERT_WARM: "Calor",
|
|
1240
|
+
ALERT_WARM_NIGHT: "Calor nocturna",
|
|
1241
|
+
ALERT_SNOW: "Neu acumulada en 24 hores",
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
_attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
|
|
1245
|
+
|
|
1246
|
+
def __init__(self, alerts_region_coordinator, description, entry_data):
|
|
1247
|
+
super().__init__(alerts_region_coordinator)
|
|
1248
|
+
self.entity_description = description
|
|
1249
|
+
self._town_name = entry_data["town_name"]
|
|
1250
|
+
self._town_id = entry_data["town_id"]
|
|
1251
|
+
self._station_id = entry_data["station_id"]
|
|
1252
|
+
self._region_id = entry_data["region_id"]
|
|
1253
|
+
|
|
1254
|
+
# Unique ID for the entity
|
|
1255
|
+
self._attr_unique_id = f"sensor.{DOMAIN}_{self._region_id}_{self.entity_description.key}"
|
|
1256
|
+
|
|
1257
|
+
# Assign entity_category if defined in the description
|
|
1258
|
+
self._attr_entity_category = getattr(description, "entity_category", None)
|
|
1259
|
+
|
|
1260
|
+
# Log para depuración
|
|
1261
|
+
_LOGGER.debug(
|
|
1262
|
+
"Inicializando sensor: %s, Unique ID: %s",
|
|
1263
|
+
self.entity_description.name,
|
|
1264
|
+
self._attr_unique_id,
|
|
1265
|
+
)
|
|
1266
|
+
|
|
1267
|
+
@property
|
|
1268
|
+
def native_value(self):
|
|
1269
|
+
"""Devuelve el estado de la alerta específica."""
|
|
1270
|
+
meteor_type = self.METEOR_MAPPING.get(self.entity_description.key)
|
|
1271
|
+
if not meteor_type:
|
|
1272
|
+
return "Desconocido"
|
|
1273
|
+
|
|
1274
|
+
meteor_data = self.coordinator.data.get("detalles", {}).get("meteor", {}).get(meteor_type, {})
|
|
1275
|
+
return meteor_data.get("estado", "Tancat")
|
|
1276
|
+
|
|
1277
|
+
@property
|
|
1278
|
+
def extra_state_attributes(self):
|
|
1279
|
+
"""Devuelve los atributos específicos de la alerta."""
|
|
1280
|
+
meteor_type = self.METEOR_MAPPING.get(self.entity_description.key)
|
|
1281
|
+
if not meteor_type:
|
|
1282
|
+
return {}
|
|
1283
|
+
|
|
1284
|
+
meteor_data = self.coordinator.data.get("detalles", {}).get("meteor", {}).get(meteor_type, {})
|
|
1285
|
+
if not meteor_data:
|
|
1286
|
+
return {}
|
|
1287
|
+
|
|
1288
|
+
return {
|
|
1289
|
+
"inicio": meteor_data.get("inicio"),
|
|
1290
|
+
"fin": meteor_data.get("fin"),
|
|
1291
|
+
"fecha": meteor_data.get("fecha"),
|
|
1292
|
+
"periodo": meteor_data.get("periodo"),
|
|
1293
|
+
"umbral": meteor_data.get("umbral"),
|
|
1294
|
+
"nivel": meteor_data.get("nivel"),
|
|
1295
|
+
"peligro": meteor_data.get("peligro"),
|
|
1296
|
+
"comentario": meteor_data.get("comentario"),
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
@property
|
|
1300
|
+
def device_info(self) -> DeviceInfo:
|
|
1301
|
+
"""Devuelve la información del dispositivo."""
|
|
1302
|
+
return DeviceInfo(
|
|
1303
|
+
identifiers={(DOMAIN, self._town_id)},
|
|
1304
|
+
name="Meteocat " + self._station_id + " " + self._town_name,
|
|
1305
|
+
manufacturer="Meteocat",
|
|
1306
|
+
model="Meteocat API",
|
|
1307
|
+
)
|