meteocat 4.0.0 → 4.0.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/.github/ISSUE_TEMPLATE/bug_report.md +45 -45
- package/.github/ISSUE_TEMPLATE/config.yml +8 -8
- package/.github/ISSUE_TEMPLATE/improvement.md +39 -39
- package/.github/ISSUE_TEMPLATE/new_function.md +41 -41
- package/.github/labels.yml +63 -63
- package/.github/workflows/autocloser.yaml +27 -27
- package/.github/workflows/close-on-label.yml +48 -48
- package/.github/workflows/force-sync-labels.yml +18 -18
- package/.github/workflows/hassfest.yaml +13 -13
- package/.github/workflows/publish-zip.yml +67 -67
- package/.github/workflows/release.yml +41 -41
- package/.github/workflows/stale.yml +63 -63
- package/.github/workflows/sync-gitlab.yml +107 -107
- package/.github/workflows/sync-labels.yml +21 -21
- package/.github/workflows/validate.yaml +16 -16
- package/.pre-commit-config.yaml +37 -37
- package/.releaserc +37 -37
- package/AUTHORS.md +13 -13
- package/CHANGELOG.md +971 -954
- package/README.md +207 -207
- package/conftest.py +11 -11
- package/custom_components/meteocat/__init__.py +298 -298
- package/custom_components/meteocat/condition.py +63 -63
- package/custom_components/meteocat/config_flow.py +613 -613
- package/custom_components/meteocat/const.py +132 -132
- package/custom_components/meteocat/coordinator.py +248 -68
- package/custom_components/meteocat/helpers.py +58 -58
- package/custom_components/meteocat/manifest.json +25 -25
- package/custom_components/meteocat/options_flow.py +287 -287
- package/custom_components/meteocat/sensor.py +4 -2
- package/custom_components/meteocat/strings.json +1060 -1058
- package/custom_components/meteocat/translations/ca.json +1060 -1058
- package/custom_components/meteocat/translations/en.json +1060 -1058
- package/custom_components/meteocat/translations/es.json +1060 -1058
- package/custom_components/meteocat/version.py +1 -1
- package/custom_components/meteocat/weather.py +218 -218
- package/filetree.py +48 -48
- package/filetree.txt +80 -79
- package/hacs.json +8 -8
- package/info.md +11 -11
- package/package.json +22 -22
- package/poetry.lock +3222 -3222
- package/pyproject.toml +68 -68
- package/requirements.test.txt +3 -3
- package/setup.cfg +64 -64
- package/setup.py +10 -10
- package/tests/bandit.yaml +17 -17
- package/tests/conftest.py +19 -19
- package/tests/test_init.py +9 -9
|
@@ -1,132 +1,132 @@
|
|
|
1
|
-
# Constantes generales
|
|
2
|
-
DOMAIN = "meteocat"
|
|
3
|
-
BASE_URL = "https://api.meteo.cat"
|
|
4
|
-
CONF_API_KEY = "api_key"
|
|
5
|
-
TOWN_NAME = "town_name"
|
|
6
|
-
TOWN_ID = "town_id"
|
|
7
|
-
VARIABLE_NAME = "variable_name"
|
|
8
|
-
VARIABLE_ID = "variable_id"
|
|
9
|
-
STATION_NAME = "station_name"
|
|
10
|
-
STATION_ID = "station_id"
|
|
11
|
-
STATION_TYPE = "station_type"
|
|
12
|
-
LATITUDE = "latitude"
|
|
13
|
-
LONGITUDE = "longitude"
|
|
14
|
-
ALTITUDE = "altitude"
|
|
15
|
-
REGION_ID = "region_id"
|
|
16
|
-
REGION_NAME = "region_name"
|
|
17
|
-
PROVINCE_ID = "province_id"
|
|
18
|
-
PROVINCE_NAME = "province_name"
|
|
19
|
-
LIMIT_XEMA = "limit_xema"
|
|
20
|
-
LIMIT_PREDICCIO = "limit_prediccio"
|
|
21
|
-
LIMIT_XDDE = "limit_xdde"
|
|
22
|
-
LIMIT_BASIC = "limit_basic"
|
|
23
|
-
LIMIT_QUOTA = "limit_quota"
|
|
24
|
-
STATION_STATUS = "station_status"
|
|
25
|
-
HOURLY_FORECAST_FILE_STATUS = "hourly_forecast_file_status"
|
|
26
|
-
DAILY_FORECAST_FILE_STATUS = "daily_forecast_file_status"
|
|
27
|
-
UVI_FILE_STATUS = "uvi_file_status"
|
|
28
|
-
ALERTS = "alerts"
|
|
29
|
-
ALERT_FILE_STATUS = "alert_file_status"
|
|
30
|
-
ALERT_WIND = "alert_wind"
|
|
31
|
-
ALERT_RAIN_INTENSITY = "alert_rain_intensity"
|
|
32
|
-
ALERT_RAIN = "alert_rain"
|
|
33
|
-
ALERT_SEA = "alert_sea"
|
|
34
|
-
ALERT_COLD = "alert_cold"
|
|
35
|
-
ALERT_WARM = "alert_warm"
|
|
36
|
-
ALERT_WARM_NIGHT = "alert_warm_night"
|
|
37
|
-
ALERT_SNOW = "alert_snow"
|
|
38
|
-
QUOTA_FILE_STATUS = "quota_file_status"
|
|
39
|
-
QUOTA_XDDE = "quota_xdde"
|
|
40
|
-
QUOTA_PREDICCIO = "quota_prediccio"
|
|
41
|
-
QUOTA_BASIC = "quota_basic"
|
|
42
|
-
QUOTA_XEMA = "quota_xema"
|
|
43
|
-
QUOTA_QUERIES = "quota_queries"
|
|
44
|
-
LIGHTNING_FILE_STATUS = "lightning_file_status"
|
|
45
|
-
SUN = "sun"
|
|
46
|
-
SUNRISE = "sunrise"
|
|
47
|
-
SUNSET = "sunset"
|
|
48
|
-
SUN_FILE_STATUS = "sun_file_status"
|
|
49
|
-
MOON_PHASE = "moon_phase"
|
|
50
|
-
MOON_FILE_STATUS = "moon_file_status"
|
|
51
|
-
MOONRISE = "moonrise"
|
|
52
|
-
MOONSET = "moonset"
|
|
53
|
-
|
|
54
|
-
from homeassistant.const import Platform
|
|
55
|
-
|
|
56
|
-
ATTRIBUTION = "Powered by Meteocatpy & Solarmoonpy"
|
|
57
|
-
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
|
58
|
-
DEFAULT_NAME = "METEOCAT"
|
|
59
|
-
|
|
60
|
-
# Tiempos para validación de API
|
|
61
|
-
DEFAULT_VALIDITY_DAYS = 1 # Número de días a partir de los cuales se considera que el archivo de información está obsoleto
|
|
62
|
-
DEFAULT_VALIDITY_HOURS = 6 # Hora a partir de la cual la API tiene la información actualizada de predicciones disponible para descarga
|
|
63
|
-
DEFAULT_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la información actualizada de predicciones disponible para descarga
|
|
64
|
-
DEFAULT_UVI_LOW_VALIDITY_HOURS = 5 # Hora a partir de la cual la API tiene la información actualizada de datos UVI disponible para descarga con límite bajo de cuota
|
|
65
|
-
DEFAULT_UVI_LOW_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la información actualizada de datos UVI disponible para descarga con límite bajo de cuota
|
|
66
|
-
DEFAULT_UVI_HIGH_VALIDITY_HOURS = 9 # Hora a partir de la cual la API tiene la información actualizada de datos UVI disponible para descarga con límite alto de cuota
|
|
67
|
-
DEFAULT_UVI_HIGH_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la información actualizada de datos UVI disponible para descarga con límite alto de cuota
|
|
68
|
-
DEFAULT_ALERT_VALIDITY_TIME = 120 # Minutos a partir de los cuales las alertas están obsoletas y se se debe proceder a una nueva llamada a la API
|
|
69
|
-
DEFAULT_QUOTES_VALIDITY_TIME = 240 # Minutos a partir de los cuales los datos de cuotas están obsoletos y se se debe proceder a una nueva llamada a la API
|
|
70
|
-
DEFAULT_LIGHTNING_VALIDITY_TIME = 240 # Minutos a partir de los cuales los datos de rayos están obsoletos y se se debe proceder a una nueva llamada a la API
|
|
71
|
-
DEFAULT_LIGHTNING_VALIDITY_HOURS = 1 # Hora a partir de la cual la API tiene la información actualizada de rayos disponible para descarga
|
|
72
|
-
DEFAULT_LIGHTNING_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la información actualizada de rayos disponible para descarga
|
|
73
|
-
|
|
74
|
-
# Multiplicadores para la duración de validez basada en limit_prediccio
|
|
75
|
-
ALERT_VALIDITY_MULTIPLIER_100 = 12 # para limit_prediccio <= 100
|
|
76
|
-
ALERT_VALIDITY_MULTIPLIER_200 = 6 # para 100 < limit_prediccio <= 200
|
|
77
|
-
ALERT_VALIDITY_MULTIPLIER_500 = 3 # para 200 < limit_prediccio <= 500
|
|
78
|
-
ALERT_VALIDITY_MULTIPLIER_DEFAULT = 1 # para limit_prediccio > 500
|
|
79
|
-
|
|
80
|
-
# CUOTA ALTA PARA FAVORECER ACTUALIZACIONES DIARIAS DE LAS PREDICCIONES
|
|
81
|
-
PREDICCIO_HIGH_QUOTA_LIMIT = 550
|
|
82
|
-
|
|
83
|
-
# Códigos de sensores de la API
|
|
84
|
-
WIND_SPEED = "wind_speed" # Velocidad del viento
|
|
85
|
-
WIND_DIRECTION = "wind_direction" # Dirección del viento
|
|
86
|
-
WIND_DIRECTION_CARDINAL = "wind_direction_cardinal" # Dirección del viento en cardinal
|
|
87
|
-
TEMPERATURE = "temperature" # Temperatura
|
|
88
|
-
HUMIDITY = "humidity" # Humedad relativa
|
|
89
|
-
PRESSURE = "pressure" # Presión atmosférica
|
|
90
|
-
PRECIPITATION = "precipitation" # Precipitación
|
|
91
|
-
PRECIPITATION_ACCUMULATED = "precipitation_accumulated" #Precipitación acumulada
|
|
92
|
-
PRECIPITATION_PROBABILITY = "precipitation_probability" #Precipitación probabilidad
|
|
93
|
-
SOLAR_GLOBAL_IRRADIANCE = "solar_global_irradiance" # Irradiación solar global
|
|
94
|
-
UV_INDEX = "uv_index" # UV
|
|
95
|
-
MAX_TEMPERATURE = "max_temperature" # Temperatura máxima
|
|
96
|
-
MIN_TEMPERATURE = "min_temperature" # Temperatura mínima
|
|
97
|
-
FEELS_LIKE = "feels_like" # Sensación térmica
|
|
98
|
-
WIND_GUST = "wind_gust" # Racha de viento
|
|
99
|
-
STATION_TIMESTAMP = "station_timestamp" # Código de tiempo de la estación
|
|
100
|
-
CONDITION = "condition" # Estado del cielo
|
|
101
|
-
MAX_TEMPERATURE_FORECAST = "max_temperature_forecast" # Temperatura máxima prevista
|
|
102
|
-
MIN_TEMPERATURE_FORECAST = "min_temperature_forecast" # Temperatura mínima prevista
|
|
103
|
-
LIGHTNING_REGION = "lightning_region" # Rayos de la comarca
|
|
104
|
-
LIGHTNING_TOWN = "lightning_town" # Rayos de la población
|
|
105
|
-
|
|
106
|
-
# Definición de códigos para variables
|
|
107
|
-
WIND_SPEED_CODE = 30
|
|
108
|
-
WIND_DIRECTION_CODE = 31
|
|
109
|
-
TEMPERATURE_CODE = 32
|
|
110
|
-
HUMIDITY_CODE = 33
|
|
111
|
-
PRESSURE_CODE = 34
|
|
112
|
-
PRECIPITATION_CODE = 35
|
|
113
|
-
SOLAR_GLOBAL_IRRADIANCE_CODE = 36
|
|
114
|
-
UV_INDEX_CODE = 39
|
|
115
|
-
MAX_TEMPERATURE_CODE = 40
|
|
116
|
-
MIN_TEMPERATURE_CODE = 42
|
|
117
|
-
WIND_GUST_CODE = 50
|
|
118
|
-
|
|
119
|
-
# Mapeo de códigos 'estatCel' a condiciones de Home Assistant
|
|
120
|
-
CONDITION_MAPPING = {
|
|
121
|
-
"sunny": [1],
|
|
122
|
-
# "clear-night": [1],
|
|
123
|
-
"partlycloudy": [2, 3],
|
|
124
|
-
"cloudy": [4, 20, 21, 22],
|
|
125
|
-
"rainy": [5, 6, 23],
|
|
126
|
-
"pouring": [7, 8, 25],
|
|
127
|
-
"lightning-rainy": [8, 24],
|
|
128
|
-
"hail": [9],
|
|
129
|
-
"snowy": [10, 26, 27, 28],
|
|
130
|
-
"fog": [11, 12],
|
|
131
|
-
"snow-rainy": [27, 29, 30],
|
|
132
|
-
}
|
|
1
|
+
# Constantes generales
|
|
2
|
+
DOMAIN = "meteocat"
|
|
3
|
+
BASE_URL = "https://api.meteo.cat"
|
|
4
|
+
CONF_API_KEY = "api_key"
|
|
5
|
+
TOWN_NAME = "town_name"
|
|
6
|
+
TOWN_ID = "town_id"
|
|
7
|
+
VARIABLE_NAME = "variable_name"
|
|
8
|
+
VARIABLE_ID = "variable_id"
|
|
9
|
+
STATION_NAME = "station_name"
|
|
10
|
+
STATION_ID = "station_id"
|
|
11
|
+
STATION_TYPE = "station_type"
|
|
12
|
+
LATITUDE = "latitude"
|
|
13
|
+
LONGITUDE = "longitude"
|
|
14
|
+
ALTITUDE = "altitude"
|
|
15
|
+
REGION_ID = "region_id"
|
|
16
|
+
REGION_NAME = "region_name"
|
|
17
|
+
PROVINCE_ID = "province_id"
|
|
18
|
+
PROVINCE_NAME = "province_name"
|
|
19
|
+
LIMIT_XEMA = "limit_xema"
|
|
20
|
+
LIMIT_PREDICCIO = "limit_prediccio"
|
|
21
|
+
LIMIT_XDDE = "limit_xdde"
|
|
22
|
+
LIMIT_BASIC = "limit_basic"
|
|
23
|
+
LIMIT_QUOTA = "limit_quota"
|
|
24
|
+
STATION_STATUS = "station_status"
|
|
25
|
+
HOURLY_FORECAST_FILE_STATUS = "hourly_forecast_file_status"
|
|
26
|
+
DAILY_FORECAST_FILE_STATUS = "daily_forecast_file_status"
|
|
27
|
+
UVI_FILE_STATUS = "uvi_file_status"
|
|
28
|
+
ALERTS = "alerts"
|
|
29
|
+
ALERT_FILE_STATUS = "alert_file_status"
|
|
30
|
+
ALERT_WIND = "alert_wind"
|
|
31
|
+
ALERT_RAIN_INTENSITY = "alert_rain_intensity"
|
|
32
|
+
ALERT_RAIN = "alert_rain"
|
|
33
|
+
ALERT_SEA = "alert_sea"
|
|
34
|
+
ALERT_COLD = "alert_cold"
|
|
35
|
+
ALERT_WARM = "alert_warm"
|
|
36
|
+
ALERT_WARM_NIGHT = "alert_warm_night"
|
|
37
|
+
ALERT_SNOW = "alert_snow"
|
|
38
|
+
QUOTA_FILE_STATUS = "quota_file_status"
|
|
39
|
+
QUOTA_XDDE = "quota_xdde"
|
|
40
|
+
QUOTA_PREDICCIO = "quota_prediccio"
|
|
41
|
+
QUOTA_BASIC = "quota_basic"
|
|
42
|
+
QUOTA_XEMA = "quota_xema"
|
|
43
|
+
QUOTA_QUERIES = "quota_queries"
|
|
44
|
+
LIGHTNING_FILE_STATUS = "lightning_file_status"
|
|
45
|
+
SUN = "sun"
|
|
46
|
+
SUNRISE = "sunrise"
|
|
47
|
+
SUNSET = "sunset"
|
|
48
|
+
SUN_FILE_STATUS = "sun_file_status"
|
|
49
|
+
MOON_PHASE = "moon_phase"
|
|
50
|
+
MOON_FILE_STATUS = "moon_file_status"
|
|
51
|
+
MOONRISE = "moonrise"
|
|
52
|
+
MOONSET = "moonset"
|
|
53
|
+
|
|
54
|
+
from homeassistant.const import Platform
|
|
55
|
+
|
|
56
|
+
ATTRIBUTION = "Powered by Meteocatpy & Solarmoonpy"
|
|
57
|
+
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
|
|
58
|
+
DEFAULT_NAME = "METEOCAT"
|
|
59
|
+
|
|
60
|
+
# Tiempos para validación de API
|
|
61
|
+
DEFAULT_VALIDITY_DAYS = 1 # Número de días a partir de los cuales se considera que el archivo de información está obsoleto
|
|
62
|
+
DEFAULT_VALIDITY_HOURS = 6 # Hora a partir de la cual la API tiene la información actualizada de predicciones disponible para descarga
|
|
63
|
+
DEFAULT_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la información actualizada de predicciones disponible para descarga
|
|
64
|
+
DEFAULT_UVI_LOW_VALIDITY_HOURS = 5 # Hora a partir de la cual la API tiene la información actualizada de datos UVI disponible para descarga con límite bajo de cuota
|
|
65
|
+
DEFAULT_UVI_LOW_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la información actualizada de datos UVI disponible para descarga con límite bajo de cuota
|
|
66
|
+
DEFAULT_UVI_HIGH_VALIDITY_HOURS = 9 # Hora a partir de la cual la API tiene la información actualizada de datos UVI disponible para descarga con límite alto de cuota
|
|
67
|
+
DEFAULT_UVI_HIGH_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la información actualizada de datos UVI disponible para descarga con límite alto de cuota
|
|
68
|
+
DEFAULT_ALERT_VALIDITY_TIME = 120 # Minutos a partir de los cuales las alertas están obsoletas y se se debe proceder a una nueva llamada a la API
|
|
69
|
+
DEFAULT_QUOTES_VALIDITY_TIME = 240 # Minutos a partir de los cuales los datos de cuotas están obsoletos y se se debe proceder a una nueva llamada a la API
|
|
70
|
+
DEFAULT_LIGHTNING_VALIDITY_TIME = 240 # Minutos a partir de los cuales los datos de rayos están obsoletos y se se debe proceder a una nueva llamada a la API
|
|
71
|
+
DEFAULT_LIGHTNING_VALIDITY_HOURS = 1 # Hora a partir de la cual la API tiene la información actualizada de rayos disponible para descarga
|
|
72
|
+
DEFAULT_LIGHTNING_VALIDITY_MINUTES = 0 # Minutos a partir de los cuales la API tiene la información actualizada de rayos disponible para descarga
|
|
73
|
+
|
|
74
|
+
# Multiplicadores para la duración de validez basada en limit_prediccio
|
|
75
|
+
ALERT_VALIDITY_MULTIPLIER_100 = 12 # para limit_prediccio <= 100
|
|
76
|
+
ALERT_VALIDITY_MULTIPLIER_200 = 6 # para 100 < limit_prediccio <= 200
|
|
77
|
+
ALERT_VALIDITY_MULTIPLIER_500 = 3 # para 200 < limit_prediccio <= 500
|
|
78
|
+
ALERT_VALIDITY_MULTIPLIER_DEFAULT = 1 # para limit_prediccio > 500
|
|
79
|
+
|
|
80
|
+
# CUOTA ALTA PARA FAVORECER ACTUALIZACIONES DIARIAS DE LAS PREDICCIONES
|
|
81
|
+
PREDICCIO_HIGH_QUOTA_LIMIT = 550
|
|
82
|
+
|
|
83
|
+
# Códigos de sensores de la API
|
|
84
|
+
WIND_SPEED = "wind_speed" # Velocidad del viento
|
|
85
|
+
WIND_DIRECTION = "wind_direction" # Dirección del viento
|
|
86
|
+
WIND_DIRECTION_CARDINAL = "wind_direction_cardinal" # Dirección del viento en cardinal
|
|
87
|
+
TEMPERATURE = "temperature" # Temperatura
|
|
88
|
+
HUMIDITY = "humidity" # Humedad relativa
|
|
89
|
+
PRESSURE = "pressure" # Presión atmosférica
|
|
90
|
+
PRECIPITATION = "precipitation" # Precipitación
|
|
91
|
+
PRECIPITATION_ACCUMULATED = "precipitation_accumulated" #Precipitación acumulada
|
|
92
|
+
PRECIPITATION_PROBABILITY = "precipitation_probability" #Precipitación probabilidad
|
|
93
|
+
SOLAR_GLOBAL_IRRADIANCE = "solar_global_irradiance" # Irradiación solar global
|
|
94
|
+
UV_INDEX = "uv_index" # UV
|
|
95
|
+
MAX_TEMPERATURE = "max_temperature" # Temperatura máxima
|
|
96
|
+
MIN_TEMPERATURE = "min_temperature" # Temperatura mínima
|
|
97
|
+
FEELS_LIKE = "feels_like" # Sensación térmica
|
|
98
|
+
WIND_GUST = "wind_gust" # Racha de viento
|
|
99
|
+
STATION_TIMESTAMP = "station_timestamp" # Código de tiempo de la estación
|
|
100
|
+
CONDITION = "condition" # Estado del cielo
|
|
101
|
+
MAX_TEMPERATURE_FORECAST = "max_temperature_forecast" # Temperatura máxima prevista
|
|
102
|
+
MIN_TEMPERATURE_FORECAST = "min_temperature_forecast" # Temperatura mínima prevista
|
|
103
|
+
LIGHTNING_REGION = "lightning_region" # Rayos de la comarca
|
|
104
|
+
LIGHTNING_TOWN = "lightning_town" # Rayos de la población
|
|
105
|
+
|
|
106
|
+
# Definición de códigos para variables
|
|
107
|
+
WIND_SPEED_CODE = 30
|
|
108
|
+
WIND_DIRECTION_CODE = 31
|
|
109
|
+
TEMPERATURE_CODE = 32
|
|
110
|
+
HUMIDITY_CODE = 33
|
|
111
|
+
PRESSURE_CODE = 34
|
|
112
|
+
PRECIPITATION_CODE = 35
|
|
113
|
+
SOLAR_GLOBAL_IRRADIANCE_CODE = 36
|
|
114
|
+
UV_INDEX_CODE = 39
|
|
115
|
+
MAX_TEMPERATURE_CODE = 40
|
|
116
|
+
MIN_TEMPERATURE_CODE = 42
|
|
117
|
+
WIND_GUST_CODE = 50
|
|
118
|
+
|
|
119
|
+
# Mapeo de códigos 'estatCel' a condiciones de Home Assistant
|
|
120
|
+
CONDITION_MAPPING = {
|
|
121
|
+
"sunny": [1],
|
|
122
|
+
# "clear-night": [1],
|
|
123
|
+
"partlycloudy": [2, 3],
|
|
124
|
+
"cloudy": [4, 20, 21, 22],
|
|
125
|
+
"rainy": [5, 6, 23],
|
|
126
|
+
"pouring": [7, 8, 25],
|
|
127
|
+
"lightning-rainy": [8, 24],
|
|
128
|
+
"hail": [9],
|
|
129
|
+
"snowy": [10, 26, 27, 28],
|
|
130
|
+
"fog": [11, 12],
|
|
131
|
+
"snow-rainy": [27, 29, 30],
|
|
132
|
+
}
|
|
@@ -290,27 +290,54 @@ class MeteocatSensorCoordinator(DataUpdateCoordinator):
|
|
|
290
290
|
raise
|
|
291
291
|
except Exception as err:
|
|
292
292
|
if isinstance(err, ConfigEntryNotReady):
|
|
293
|
-
_LOGGER.exception(
|
|
294
|
-
"No se pudo inicializar el dispositivo (Station ID: %s): %s",
|
|
295
|
-
self.station_id,
|
|
296
|
-
err,
|
|
297
|
-
)
|
|
293
|
+
_LOGGER.exception("No se pudo inicializar el dispositivo (Station ID: %s): %s", self.station_id, err)
|
|
298
294
|
raise
|
|
299
295
|
else:
|
|
300
|
-
_LOGGER.exception(
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
err,
|
|
304
|
-
)
|
|
305
|
-
|
|
306
|
-
# Cargar datos en caché si la API falla
|
|
296
|
+
_LOGGER.exception("Error inesperado al obtener datos de sensores (Station ID: %s): %s", self.station_id, err)
|
|
297
|
+
|
|
298
|
+
# === FALLBACK SEGURO ===
|
|
307
299
|
cached_data = await load_json_from_file(self.station_file)
|
|
308
|
-
if cached_data:
|
|
309
|
-
|
|
300
|
+
if cached_data and isinstance(cached_data, list) and cached_data:
|
|
301
|
+
# Buscar la última lectura (cualquier variable)
|
|
302
|
+
last_reading = None
|
|
303
|
+
last_time_str = "unknown"
|
|
304
|
+
for var_block in cached_data:
|
|
305
|
+
for variable in var_block.get("variables", []):
|
|
306
|
+
lectures = variable.get("lectures", [])
|
|
307
|
+
if lectures:
|
|
308
|
+
candidate = lectures[-1].get("data")
|
|
309
|
+
if candidate and (last_reading is None or candidate > last_time_str):
|
|
310
|
+
last_reading = candidate
|
|
311
|
+
last_time_str = candidate
|
|
312
|
+
|
|
313
|
+
# Formatear hora legible
|
|
314
|
+
try:
|
|
315
|
+
if last_time_str != "unknown":
|
|
316
|
+
dt = datetime.fromisoformat(last_time_str.replace("Z", "+00:00"))
|
|
317
|
+
local_dt = dt.astimezone(TIMEZONE)
|
|
318
|
+
display_time = local_dt.strftime("%d/%m/%Y %H:%M")
|
|
319
|
+
else:
|
|
320
|
+
display_time = "unknown"
|
|
321
|
+
except (ValueError, TypeError, AttributeError):
|
|
322
|
+
display_time = last_time_str.split("T")[0] if "T" in last_time_str else last_time_str
|
|
323
|
+
|
|
324
|
+
_LOGGER.warning(
|
|
325
|
+
"SENSOR: API falló → usando caché local:\n"
|
|
326
|
+
" • Estación: %s (%s)\n"
|
|
327
|
+
" • Archivo: %s\n"
|
|
328
|
+
" • Última lectura: %s",
|
|
329
|
+
self.station_name,
|
|
330
|
+
self.station_id,
|
|
331
|
+
self.station_file.name,
|
|
332
|
+
display_time
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
self.async_set_updated_data(cached_data)
|
|
310
336
|
return cached_data
|
|
311
|
-
|
|
312
|
-
_LOGGER.error("No
|
|
313
|
-
|
|
337
|
+
|
|
338
|
+
_LOGGER.error("SENSOR: No hay caché disponible para los datos de la estación %s.", self.station_id)
|
|
339
|
+
self.async_set_updated_data([])
|
|
340
|
+
return []
|
|
314
341
|
|
|
315
342
|
class MeteocatStaticSensorCoordinator(DataUpdateCoordinator):
|
|
316
343
|
"""Coordinator to manage and update static sensor data."""
|
|
@@ -490,13 +517,30 @@ class MeteocatUviCoordinator(DataUpdateCoordinator):
|
|
|
490
517
|
raise
|
|
491
518
|
except Exception as err:
|
|
492
519
|
_LOGGER.exception("Error inesperado al obtener datos del índice UV para %s: %s", self.town_id, err)
|
|
493
|
-
|
|
494
|
-
#
|
|
520
|
+
|
|
521
|
+
# === FALLBACK SEGURO ===
|
|
495
522
|
cached_data = await load_json_from_file(self.uvi_file)
|
|
496
|
-
if cached_data:
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
523
|
+
if cached_data and "uvi" in cached_data and cached_data["uvi"]:
|
|
524
|
+
raw_date = cached_data["uvi"][0].get("date", "unknown")
|
|
525
|
+
# Formatear fecha para log
|
|
526
|
+
try:
|
|
527
|
+
first_date = datetime.strptime(raw_date, "%Y-%m-%d").strftime("%d/%m/%Y")
|
|
528
|
+
except (ValueError, TypeError):
|
|
529
|
+
first_date = raw_date
|
|
530
|
+
|
|
531
|
+
_LOGGER.warning(
|
|
532
|
+
"API UVI falló → usando caché local:\n"
|
|
533
|
+
" • Archivo: %s\n"
|
|
534
|
+
" • Datos desde: %s",
|
|
535
|
+
self.uvi_file.name,
|
|
536
|
+
first_date
|
|
537
|
+
)
|
|
538
|
+
|
|
539
|
+
self.async_set_updated_data(cached_data["uvi"])
|
|
540
|
+
return cached_data["uvi"]
|
|
541
|
+
|
|
542
|
+
_LOGGER.error("No hay datos UVI ni en caché para %s", self.town_id)
|
|
543
|
+
self.async_set_updated_data([])
|
|
500
544
|
return []
|
|
501
545
|
|
|
502
546
|
class MeteocatUviFileCoordinator(BaseFileCoordinator):
|
|
@@ -753,20 +797,36 @@ class MeteocatEntityCoordinator(DataUpdateCoordinator):
|
|
|
753
797
|
raise
|
|
754
798
|
except Exception as err:
|
|
755
799
|
_LOGGER.exception("Error inesperado al obtener datos de predicción: %s", err)
|
|
756
|
-
|
|
757
|
-
#
|
|
758
|
-
# Fallback: usar caché local si todo falla
|
|
759
|
-
# -----------------------------------------------------------------
|
|
800
|
+
|
|
801
|
+
# === FALLBACK SEGURO ===
|
|
760
802
|
hourly_cache = await load_json_from_file(self.hourly_file) or {}
|
|
761
803
|
daily_cache = await load_json_from_file(self.daily_file) or {}
|
|
762
|
-
|
|
804
|
+
|
|
805
|
+
# --- Fecha horaria ---
|
|
806
|
+
h_raw = hourly_cache.get("dies", [{}])[0].get("data", "")
|
|
807
|
+
try:
|
|
808
|
+
h_date = h_raw.replace("Z", "").split("T")[0]
|
|
809
|
+
h_display = datetime.strptime(h_date, "%Y-%m-%d").strftime("%d/%m/%Y")
|
|
810
|
+
except (ValueError, AttributeError, IndexError):
|
|
811
|
+
h_display = "unknown"
|
|
812
|
+
|
|
813
|
+
# --- Fecha diaria ---
|
|
814
|
+
d_raw = daily_cache.get("dies", [{}])[0].get("data", "")
|
|
815
|
+
try:
|
|
816
|
+
d_date = d_raw.replace("Z", "").split("T")[0]
|
|
817
|
+
d_display = datetime.strptime(d_date, "%Y-%m-%d").strftime("%d/%m/%Y")
|
|
818
|
+
except (ValueError, AttributeError, IndexError):
|
|
819
|
+
d_display = "unknown"
|
|
820
|
+
|
|
763
821
|
_LOGGER.warning(
|
|
764
|
-
"
|
|
765
|
-
|
|
766
|
-
"
|
|
767
|
-
|
|
822
|
+
"API falló → usando caché local:\n"
|
|
823
|
+
" • %s → %s\n"
|
|
824
|
+
" • %s → %s",
|
|
825
|
+
self.hourly_file.name, h_display,
|
|
826
|
+
self.daily_file.name, d_display
|
|
768
827
|
)
|
|
769
|
-
|
|
828
|
+
|
|
829
|
+
self.async_set_updated_data({"hourly": hourly_cache, "daily": daily_cache})
|
|
770
830
|
return {"hourly": hourly_cache, "daily": daily_cache}
|
|
771
831
|
|
|
772
832
|
def get_condition_from_code(code: int) -> str:
|
|
@@ -1381,20 +1441,36 @@ class MeteocatAlertsCoordinator(DataUpdateCoordinator):
|
|
|
1381
1441
|
_LOGGER.error("Error al obtener datos de alertas: %s", err)
|
|
1382
1442
|
raise
|
|
1383
1443
|
except Exception as err:
|
|
1384
|
-
_LOGGER.exception("Error
|
|
1385
|
-
|
|
1386
|
-
#
|
|
1444
|
+
_LOGGER.exception("Error al obtener alertas: %s", err)
|
|
1445
|
+
|
|
1446
|
+
# === FALLBACK SEGURO ===
|
|
1387
1447
|
cached_data = await load_json_from_file(self.alerts_file)
|
|
1388
1448
|
if self._is_valid_alert_data(cached_data):
|
|
1449
|
+
update_str = cached_data["actualitzat"]["dataUpdate"]
|
|
1450
|
+
try:
|
|
1451
|
+
update_dt = datetime.fromisoformat(update_str)
|
|
1452
|
+
local_dt = update_dt.astimezone(TIMEZONE)
|
|
1453
|
+
display_time = local_dt.strftime("%d/%m/%Y %H:%M")
|
|
1454
|
+
except (ValueError, TypeError):
|
|
1455
|
+
display_time = update_str
|
|
1456
|
+
|
|
1389
1457
|
_LOGGER.warning(
|
|
1390
|
-
"
|
|
1391
|
-
|
|
1458
|
+
"ALERTAS: API falló → usando caché local:\n"
|
|
1459
|
+
" • Archivo: %s\n"
|
|
1460
|
+
" • Última actualización: %s\n"
|
|
1461
|
+
" • Alertas activas: %d",
|
|
1462
|
+
self.alerts_file.name,
|
|
1463
|
+
display_time
|
|
1392
1464
|
)
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1465
|
+
|
|
1466
|
+
self.async_set_updated_data({
|
|
1467
|
+
"actualizado": cached_data["actualitzat"]["dataUpdate"]
|
|
1468
|
+
})
|
|
1469
|
+
return {"actualizado": cached_data["actualitzat"]["dataUpdate"]}
|
|
1470
|
+
|
|
1471
|
+
_LOGGER.error("ALERTAS: No hay caché disponible. Sin datos de alertas.")
|
|
1472
|
+
self.async_set_updated_data({})
|
|
1473
|
+
return {}
|
|
1398
1474
|
|
|
1399
1475
|
@staticmethod
|
|
1400
1476
|
def _is_valid_alert_data(data: dict) -> bool:
|
|
@@ -1822,16 +1898,40 @@ class MeteocatQuotesCoordinator(DataUpdateCoordinator):
|
|
|
1822
1898
|
_LOGGER.error("Error al obtener cuotas de la API de Meteocat: %s", err)
|
|
1823
1899
|
raise
|
|
1824
1900
|
except Exception as err:
|
|
1825
|
-
_LOGGER.exception("Error
|
|
1826
|
-
|
|
1827
|
-
#
|
|
1901
|
+
_LOGGER.exception("Error al obtener cuotas: %s", err)
|
|
1902
|
+
|
|
1903
|
+
# === FALLBACK SEGURO ===
|
|
1828
1904
|
cached_data = await load_json_from_file(self.quotes_file)
|
|
1829
|
-
if cached_data:
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1905
|
+
if cached_data and "actualitzat" in cached_data and "dataUpdate" in cached_data["actualitzat"]:
|
|
1906
|
+
update_str = cached_data["actualitzat"]["dataUpdate"]
|
|
1907
|
+
try:
|
|
1908
|
+
update_dt = datetime.fromisoformat(update_str)
|
|
1909
|
+
local_dt = update_dt.astimezone(TIMEZONE)
|
|
1910
|
+
display_time = local_dt.strftime("%d/%m/%Y %H:%M")
|
|
1911
|
+
except (ValueError, TypeError):
|
|
1912
|
+
display_time = update_str.split("T")[0]
|
|
1913
|
+
|
|
1914
|
+
# Contar planes activos
|
|
1915
|
+
plans_count = len(cached_data.get("plans", []))
|
|
1916
|
+
|
|
1917
|
+
_LOGGER.warning(
|
|
1918
|
+
"CUOTAS: API falló → usando caché local:\n"
|
|
1919
|
+
" • Archivo: %s\n"
|
|
1920
|
+
" • Última actualización: %s\n"
|
|
1921
|
+
" • Planes registrados: %d",
|
|
1922
|
+
self.quotes_file.name,
|
|
1923
|
+
display_time,
|
|
1924
|
+
plans_count
|
|
1925
|
+
)
|
|
1926
|
+
|
|
1927
|
+
self.async_set_updated_data({
|
|
1928
|
+
"actualizado": cached_data["actualitzat"]["dataUpdate"]
|
|
1929
|
+
})
|
|
1930
|
+
return {"actualizado": cached_data["actualitzat"]["dataUpdate"]}
|
|
1931
|
+
|
|
1932
|
+
_LOGGER.error("CUOTAS: No hay caché disponible. Sin información de consumo.")
|
|
1933
|
+
self.async_set_updated_data({})
|
|
1934
|
+
return {}
|
|
1835
1935
|
|
|
1836
1936
|
class MeteocatQuotesFileCoordinator(BaseFileCoordinator):
|
|
1837
1937
|
"""Coordinator para manejar la actualización de las cuotas desde quotes.json."""
|
|
@@ -1993,16 +2093,36 @@ class MeteocatLightningCoordinator(DataUpdateCoordinator):
|
|
|
1993
2093
|
_LOGGER.warning("Tiempo de espera agotado al obtener los datos de rayos de la API de Meteocat.")
|
|
1994
2094
|
raise ConfigEntryNotReady from err
|
|
1995
2095
|
except Exception as err:
|
|
1996
|
-
_LOGGER.exception("Error
|
|
1997
|
-
|
|
1998
|
-
#
|
|
2096
|
+
_LOGGER.exception("Error al obtener datos de rayos: %s", err)
|
|
2097
|
+
|
|
2098
|
+
# === FALLBACK SEGURO ===
|
|
1999
2099
|
cached_data = await load_json_from_file(self.lightning_file)
|
|
2000
|
-
if cached_data:
|
|
2001
|
-
|
|
2002
|
-
|
|
2100
|
+
if cached_data and "actualitzat" in cached_data:
|
|
2101
|
+
update_str = cached_data["actualitzat"]["dataUpdate"]
|
|
2102
|
+
try:
|
|
2103
|
+
update_dt = datetime.fromisoformat(update_str)
|
|
2104
|
+
# Convertir a hora local para mostrar
|
|
2105
|
+
local_dt = update_dt.astimezone(TIMEZONE)
|
|
2106
|
+
display_time = local_dt.strftime("%d/%m/%Y %H:%M")
|
|
2107
|
+
except (ValueError, TypeError):
|
|
2108
|
+
display_time = update_str
|
|
2109
|
+
|
|
2110
|
+
_LOGGER.warning(
|
|
2111
|
+
"API rayos falló → usando caché local:\n"
|
|
2112
|
+
" • Archivo: %s\n"
|
|
2113
|
+
" • Última actualización: %s",
|
|
2114
|
+
self.lightning_file.name,
|
|
2115
|
+
display_time
|
|
2116
|
+
)
|
|
2003
2117
|
|
|
2004
|
-
|
|
2005
|
-
|
|
2118
|
+
self.async_set_updated_data({
|
|
2119
|
+
"actualizado": cached_data["actualitzat"]["dataUpdate"]
|
|
2120
|
+
})
|
|
2121
|
+
return {"actualizado": cached_data["actualitzat"]["dataUpdate"]}
|
|
2122
|
+
|
|
2123
|
+
_LOGGER.error("No hay caché de rayos disponible.")
|
|
2124
|
+
self.async_set_updated_data({})
|
|
2125
|
+
return {}
|
|
2006
2126
|
|
|
2007
2127
|
class MeteocatLightningFileCoordinator(BaseFileCoordinator):
|
|
2008
2128
|
"""Coordinator para manejar la actualización de los datos de rayos desde lightning_{region_id}.json."""
|
|
@@ -2440,11 +2560,39 @@ class MeteocatSunCoordinator(DataUpdateCoordinator):
|
|
|
2440
2560
|
|
|
2441
2561
|
except Exception as err:
|
|
2442
2562
|
_LOGGER.exception("Error al calcular/guardar los datos solares: %s", err)
|
|
2563
|
+
|
|
2564
|
+
# === FALLBACK SEGURO ===
|
|
2443
2565
|
cached = await load_json_from_file(self.sun_file)
|
|
2444
|
-
if cached:
|
|
2445
|
-
|
|
2566
|
+
if cached and "actualitzat" in cached and "dades" in cached and cached["dades"]:
|
|
2567
|
+
update_str = cached["actualitzat"]["dataUpdate"]
|
|
2568
|
+
try:
|
|
2569
|
+
update_dt = datetime.fromisoformat(update_str)
|
|
2570
|
+
local_dt = update_dt.astimezone(ZoneInfo(self.timezone_str))
|
|
2571
|
+
display_time = local_dt.strftime("%d/%m/%Y %H:%M")
|
|
2572
|
+
except (ValueError, TypeError):
|
|
2573
|
+
display_time = update_str.split("T")[0]
|
|
2574
|
+
|
|
2575
|
+
sunrise = cached["dades"][0].get("sunrise", "unknown")
|
|
2576
|
+
sunset = cached["dades"][0].get("sunset", "unknown")
|
|
2577
|
+
|
|
2578
|
+
_LOGGER.warning(
|
|
2579
|
+
"SOL: Cálculo falló → usando caché local:\n"
|
|
2580
|
+
" • Archivo: %s\n"
|
|
2581
|
+
" • Última actualización: %s\n"
|
|
2582
|
+
" • Amanecer: %s\n"
|
|
2583
|
+
" • Atardecer: %s",
|
|
2584
|
+
self.sun_file.name,
|
|
2585
|
+
display_time,
|
|
2586
|
+
sunrise.split("T")[1][:5] if "T" in sunrise else sunrise,
|
|
2587
|
+
sunset.split("T")[1][:5] if "T" in sunset else sunset
|
|
2588
|
+
)
|
|
2589
|
+
|
|
2590
|
+
self.async_set_updated_data(cached)
|
|
2446
2591
|
return cached
|
|
2447
|
-
|
|
2592
|
+
|
|
2593
|
+
_LOGGER.error("SOL: No hay caché disponible. Sin datos solares.")
|
|
2594
|
+
self.async_set_updated_data({})
|
|
2595
|
+
return {}
|
|
2448
2596
|
|
|
2449
2597
|
class MeteocatSunFileCoordinator(BaseFileCoordinator):
|
|
2450
2598
|
"""Coordinator para manejar la actualización de los datos de sol desde sun_{town_id}.json."""
|
|
@@ -2868,13 +3016,45 @@ class MeteocatMoonCoordinator(DataUpdateCoordinator):
|
|
|
2868
3016
|
return {"actualizado": data_with_timestamp["actualitzat"]["dataUpdate"]}
|
|
2869
3017
|
|
|
2870
3018
|
except Exception as err:
|
|
2871
|
-
_LOGGER.exception("
|
|
3019
|
+
_LOGGER.exception("Error al calcular datos de la luna: %s", err)
|
|
3020
|
+
|
|
3021
|
+
# === FALLBACK SEGURO ===
|
|
2872
3022
|
cached_data = await load_json_from_file(self.moon_file)
|
|
2873
|
-
if cached_data:
|
|
2874
|
-
|
|
3023
|
+
if cached_data and "actualitzat" in cached_data and "dades" in cached_data:
|
|
3024
|
+
update_str = cached_data["actualitzat"]["dataUpdate"]
|
|
3025
|
+
try:
|
|
3026
|
+
update_dt = datetime.fromisoformat(update_str)
|
|
3027
|
+
local_dt = update_dt.astimezone(ZoneInfo(self.timezone_str))
|
|
3028
|
+
display_time = local_dt.strftime("%d/%m/%Y %H:%M")
|
|
3029
|
+
except (ValueError, TypeError):
|
|
3030
|
+
display_time = update_str.split("T")[0]
|
|
3031
|
+
|
|
3032
|
+
moonrise = cached_data["dades"][0].get("moonrise", "unknown")
|
|
3033
|
+
moonset = cached_data["dades"][0].get("moonset", "unkwnown")
|
|
3034
|
+
phase = cached_data["dades"][0].get("moon_phase_name", "unknown")
|
|
3035
|
+
|
|
3036
|
+
_LOGGER.warning(
|
|
3037
|
+
"LUNA: Cálculo falló → usando caché local:\n"
|
|
3038
|
+
" • Archivo: %s\n"
|
|
3039
|
+
" • Última actualización: %s\n"
|
|
3040
|
+
" • Fase: %s\n"
|
|
3041
|
+
" • Salida: %s\n"
|
|
3042
|
+
" • Atardecer: %s",
|
|
3043
|
+
self.moon_file.name,
|
|
3044
|
+
display_time,
|
|
3045
|
+
phase.title().replace("_", " "),
|
|
3046
|
+
moonrise.split("T")[1][:5] if "T" in moonrise else "—",
|
|
3047
|
+
moonset.split("T")[1][:5] if "T" in moonset else "—"
|
|
3048
|
+
)
|
|
3049
|
+
|
|
3050
|
+
self.async_set_updated_data({
|
|
3051
|
+
"actualizado": cached_data["actualitzat"]["dataUpdate"]
|
|
3052
|
+
})
|
|
2875
3053
|
return {"actualizado": cached_data["actualitzat"]["dataUpdate"]}
|
|
2876
|
-
|
|
2877
|
-
|
|
3054
|
+
|
|
3055
|
+
_LOGGER.error("LUNA: No hay caché disponible. Sin datos lunares.")
|
|
3056
|
+
self.async_set_updated_data({})
|
|
3057
|
+
return {}
|
|
2878
3058
|
|
|
2879
3059
|
class MeteocatMoonFileCoordinator(BaseFileCoordinator):
|
|
2880
3060
|
"""Coordinator para manejar la actualización de los datos de la luna desde moon_{town_id}.json."""
|