meteocat 2.3.0 → 3.1.0
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 +8 -2
- package/.github/ISSUE_TEMPLATE/config.yml +7 -0
- package/.github/ISSUE_TEMPLATE/improvement.md +39 -0
- package/.github/ISSUE_TEMPLATE/new_function.md +41 -0
- package/.github/labels.yml +63 -0
- package/.github/workflows/autocloser.yaml +11 -9
- package/.github/workflows/close-on-label.yml +48 -0
- package/.github/workflows/force-sync-labels.yml +18 -0
- package/.github/workflows/release.yml +1 -31
- package/.github/workflows/sync-gitlab.yml +17 -4
- package/.github/workflows/sync-labels.yml +21 -0
- package/.releaserc +21 -0
- package/AUTHORS.md +1 -0
- package/CHANGELOG.md +58 -1
- package/README.md +81 -6
- package/custom_components/meteocat/__init__.py +51 -41
- package/custom_components/meteocat/config_flow.py +52 -3
- package/custom_components/meteocat/const.py +3 -0
- package/custom_components/meteocat/coordinator.py +188 -2
- package/custom_components/meteocat/manifest.json +1 -1
- package/custom_components/meteocat/options_flow.py +61 -3
- package/custom_components/meteocat/sensor.py +305 -254
- package/custom_components/meteocat/strings.json +61 -15
- package/custom_components/meteocat/translations/ca.json +67 -22
- package/custom_components/meteocat/translations/en.json +61 -15
- package/custom_components/meteocat/translations/es.json +61 -15
- package/custom_components/meteocat/version.py +1 -1
- package/custom_components/meteocat/weather.py +1 -1
- package/hacs.json +1 -1
- package/images/daily_forecast_2_alerts.png +0 -0
- package/images/regenerate_assets.png +0 -0
- package/images/setup_options.png +0 -0
- package/images/system_options.png +0 -0
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/scripts/update_version.sh +22 -0
- package/.github/workflows/close-duplicates.yml +0 -57
|
@@ -5,6 +5,7 @@ import voluptuous as vol
|
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
import aiofiles
|
|
7
7
|
import json
|
|
8
|
+
import importlib # Para importaciones lazy
|
|
8
9
|
|
|
9
10
|
from homeassistant import core
|
|
10
11
|
from homeassistant.config_entries import ConfigEntry
|
|
@@ -14,24 +15,6 @@ from homeassistant.helpers.entity_platform import async_get_platforms
|
|
|
14
15
|
from homeassistant.helpers import config_validation as cv
|
|
15
16
|
|
|
16
17
|
from .helpers import get_storage_dir
|
|
17
|
-
from .coordinator import (
|
|
18
|
-
MeteocatSensorCoordinator,
|
|
19
|
-
MeteocatStaticSensorCoordinator,
|
|
20
|
-
MeteocatEntityCoordinator,
|
|
21
|
-
MeteocatUviCoordinator,
|
|
22
|
-
MeteocatUviFileCoordinator,
|
|
23
|
-
HourlyForecastCoordinator,
|
|
24
|
-
DailyForecastCoordinator,
|
|
25
|
-
MeteocatConditionCoordinator,
|
|
26
|
-
MeteocatTempForecastCoordinator,
|
|
27
|
-
MeteocatAlertsCoordinator,
|
|
28
|
-
MeteocatAlertsRegionCoordinator,
|
|
29
|
-
MeteocatQuotesCoordinator,
|
|
30
|
-
MeteocatQuotesFileCoordinator,
|
|
31
|
-
MeteocatLightningCoordinator,
|
|
32
|
-
MeteocatLightningFileCoordinator,
|
|
33
|
-
)
|
|
34
|
-
|
|
35
18
|
from meteocatpy.town import MeteocatTown
|
|
36
19
|
from meteocatpy.symbols import MeteocatSymbols
|
|
37
20
|
from meteocatpy.variables import MeteocatVariables
|
|
@@ -41,7 +24,7 @@ from .const import DOMAIN, PLATFORMS
|
|
|
41
24
|
_LOGGER = logging.getLogger(__name__)
|
|
42
25
|
|
|
43
26
|
# Versión
|
|
44
|
-
__version__ = ""
|
|
27
|
+
__version__ = "3.1.0"
|
|
45
28
|
|
|
46
29
|
# Definir el esquema de configuración CONFIG_SCHEMA
|
|
47
30
|
CONFIG_SCHEMA = vol.Schema(
|
|
@@ -59,6 +42,8 @@ CONFIG_SCHEMA = vol.Schema(
|
|
|
59
42
|
vol.Optional("province_id"): cv.string,
|
|
60
43
|
vol.Optional("region_name"): cv.string,
|
|
61
44
|
vol.Optional("region_id"): cv.string,
|
|
45
|
+
vol.Required("latitude"): cv.latitude,
|
|
46
|
+
vol.Required("longitude"): cv.longitude,
|
|
62
47
|
}
|
|
63
48
|
)
|
|
64
49
|
},
|
|
@@ -119,6 +104,11 @@ async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
|
|
|
119
104
|
"""Configuración inicial del componente Meteocat."""
|
|
120
105
|
return True
|
|
121
106
|
|
|
107
|
+
def _get_coordinator_module(cls_name: str):
|
|
108
|
+
"""Importa dinámicamente un coordinador para evitar blocking imports."""
|
|
109
|
+
module = importlib.import_module(".coordinator", "custom_components.meteocat")
|
|
110
|
+
return getattr(module, cls_name)
|
|
111
|
+
|
|
122
112
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
123
113
|
"""Configura una entrada de configuración para Meteocat."""
|
|
124
114
|
_LOGGER.info("Configurando la integración de Meteocat...")
|
|
@@ -129,14 +119,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
|
129
119
|
# Validar campos requeridos
|
|
130
120
|
required_fields = [
|
|
131
121
|
"api_key", "town_name", "town_id", "variable_name",
|
|
132
|
-
"variable_id", "station_name", "station_id", "province_name",
|
|
133
|
-
"province_id", "region_name", "region_id"
|
|
122
|
+
"variable_id", "station_name", "station_id", "province_name",
|
|
123
|
+
"province_id", "region_name", "region_id", "latitude", "longitude"
|
|
134
124
|
]
|
|
135
125
|
missing_fields = [field for field in required_fields if field not in entry_data]
|
|
136
126
|
if missing_fields:
|
|
137
127
|
_LOGGER.error(f"Faltan los siguientes campos en la configuración: {missing_fields}")
|
|
138
128
|
return False
|
|
139
|
-
|
|
129
|
+
|
|
130
|
+
# Validar coordenadas válidas para Cataluña
|
|
131
|
+
latitude = entry_data.get("latitude")
|
|
132
|
+
longitude = entry_data.get("longitude")
|
|
133
|
+
if not (40.5 <= latitude <= 42.5 and 0.1 <= longitude <= 3.3): # Rango aproximado para Cataluña
|
|
134
|
+
_LOGGER.warning(
|
|
135
|
+
"Coordenadas inválidas (latitude: %s, longitude: %s). Usando coordenadas de Barcelona por defecto para MeteocatSunCoordinator.",
|
|
136
|
+
latitude, longitude
|
|
137
|
+
)
|
|
138
|
+
entry_data = {
|
|
139
|
+
**entry_data,
|
|
140
|
+
"latitude": 41.38879,
|
|
141
|
+
"longitude": 2.15899
|
|
142
|
+
}
|
|
143
|
+
|
|
140
144
|
# Crear los assets básicos si faltan
|
|
141
145
|
await ensure_assets_exist(
|
|
142
146
|
hass,
|
|
@@ -150,33 +154,38 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
|
150
154
|
f"Variable '{entry_data['variable_name']}' (ID: {entry_data['variable_id']}), "
|
|
151
155
|
f"Estación '{entry_data['station_name']}' (ID: {entry_data['station_id']}), "
|
|
152
156
|
f"Provincia '{entry_data['province_name']}' (ID: {entry_data['province_id']}), "
|
|
153
|
-
f"Comarca '{entry_data['region_name']}' (ID: {entry_data['region_id']})
|
|
157
|
+
f"Comarca '{entry_data['region_name']}' (ID: {entry_data['region_id']}), "
|
|
158
|
+
f"Coordenadas: ({entry_data['latitude']}, {entry_data['longitude']})."
|
|
154
159
|
)
|
|
155
160
|
|
|
156
|
-
#
|
|
157
|
-
|
|
158
|
-
("sensor_coordinator", MeteocatSensorCoordinator),
|
|
159
|
-
("static_sensor_coordinator", MeteocatStaticSensorCoordinator),
|
|
160
|
-
("entity_coordinator", MeteocatEntityCoordinator),
|
|
161
|
-
("uvi_coordinator", MeteocatUviCoordinator),
|
|
162
|
-
("uvi_file_coordinator", MeteocatUviFileCoordinator),
|
|
163
|
-
("hourly_forecast_coordinator", HourlyForecastCoordinator),
|
|
164
|
-
("daily_forecast_coordinator", DailyForecastCoordinator),
|
|
165
|
-
("condition_coordinator", MeteocatConditionCoordinator),
|
|
166
|
-
("temp_forecast_coordinator", MeteocatTempForecastCoordinator),
|
|
167
|
-
("alerts_coordinator", MeteocatAlertsCoordinator),
|
|
168
|
-
("alerts_region_coordinator", MeteocatAlertsRegionCoordinator),
|
|
169
|
-
("quotes_coordinator", MeteocatQuotesCoordinator),
|
|
170
|
-
("quotes_file_coordinator", MeteocatQuotesFileCoordinator),
|
|
171
|
-
("lightning_coordinator", MeteocatLightningCoordinator),
|
|
172
|
-
("lightning_file_coordinator", MeteocatLightningFileCoordinator),
|
|
161
|
+
# Lista de coordinadores con sus clases
|
|
162
|
+
coordinator_configs = [
|
|
163
|
+
("sensor_coordinator", "MeteocatSensorCoordinator"),
|
|
164
|
+
("static_sensor_coordinator", "MeteocatStaticSensorCoordinator"),
|
|
165
|
+
("entity_coordinator", "MeteocatEntityCoordinator"),
|
|
166
|
+
("uvi_coordinator", "MeteocatUviCoordinator"),
|
|
167
|
+
("uvi_file_coordinator", "MeteocatUviFileCoordinator"),
|
|
168
|
+
("hourly_forecast_coordinator", "HourlyForecastCoordinator"),
|
|
169
|
+
("daily_forecast_coordinator", "DailyForecastCoordinator"),
|
|
170
|
+
("condition_coordinator", "MeteocatConditionCoordinator"),
|
|
171
|
+
("temp_forecast_coordinator", "MeteocatTempForecastCoordinator"),
|
|
172
|
+
("alerts_coordinator", "MeteocatAlertsCoordinator"),
|
|
173
|
+
("alerts_region_coordinator", "MeteocatAlertsRegionCoordinator"),
|
|
174
|
+
("quotes_coordinator", "MeteocatQuotesCoordinator"),
|
|
175
|
+
("quotes_file_coordinator", "MeteocatQuotesFileCoordinator"),
|
|
176
|
+
("lightning_coordinator", "MeteocatLightningCoordinator"),
|
|
177
|
+
("lightning_file_coordinator", "MeteocatLightningFileCoordinator"),
|
|
178
|
+
("sun_coordinator", "MeteocatSunCoordinator"),
|
|
179
|
+
("sun_file_coordinator", "MeteocatSunFileCoordinator"),
|
|
173
180
|
]
|
|
174
181
|
|
|
175
182
|
hass.data.setdefault(DOMAIN, {})
|
|
176
183
|
hass.data[DOMAIN][entry.entry_id] = {}
|
|
177
184
|
|
|
178
185
|
try:
|
|
179
|
-
for key,
|
|
186
|
+
for key, cls_name in coordinator_configs:
|
|
187
|
+
# Importación lazy: importa la clase solo cuando sea necesario
|
|
188
|
+
cls = await hass.async_add_executor_job(_get_coordinator_module, cls_name)
|
|
180
189
|
coordinator = cls(hass=hass, entry_data=entry_data)
|
|
181
190
|
await coordinator.async_config_entry_first_refresh()
|
|
182
191
|
hass.data[DOMAIN][entry.entry_id][key] = coordinator
|
|
@@ -251,6 +260,7 @@ async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|
|
251
260
|
files_folder / f"uvi_{town_id.lower()}_data.json",
|
|
252
261
|
files_folder / f"forecast_{town_id.lower()}_hourly_data.json",
|
|
253
262
|
files_folder / f"forecast_{town_id.lower()}_daily_data.json",
|
|
263
|
+
files_folder / f"sun_{town_id.lower()}_data.json",
|
|
254
264
|
])
|
|
255
265
|
|
|
256
266
|
# 3. Archivos de comarca (region_id)
|
|
@@ -12,6 +12,8 @@ import voluptuous as vol
|
|
|
12
12
|
import aiofiles
|
|
13
13
|
import unicodedata
|
|
14
14
|
|
|
15
|
+
from astral import LocationInfo
|
|
16
|
+
from astral.sun import sun
|
|
15
17
|
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
|
16
18
|
from homeassistant.core import callback
|
|
17
19
|
from homeassistant.exceptions import HomeAssistantError
|
|
@@ -39,8 +41,8 @@ from .const import (
|
|
|
39
41
|
LIMIT_XEMA,
|
|
40
42
|
LIMIT_PREDICCIO,
|
|
41
43
|
LIMIT_XDDE,
|
|
42
|
-
LIMIT_BASIC,
|
|
43
44
|
LIMIT_QUOTA,
|
|
45
|
+
LIMIT_BASIC,
|
|
44
46
|
)
|
|
45
47
|
|
|
46
48
|
from .options_flow import MeteocatOptionsFlowHandler
|
|
@@ -172,7 +174,7 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
172
174
|
"Archivo regional %s creado con plantilla inicial", alerts_region_file
|
|
173
175
|
)
|
|
174
176
|
|
|
175
|
-
|
|
177
|
+
# Archivo lightning regional
|
|
176
178
|
lightning_file = alerts_dir / f"lightning_{self.region_id}.json"
|
|
177
179
|
if not lightning_file.exists():
|
|
178
180
|
async with aiofiles.open(lightning_file, "w", encoding="utf-8") as file:
|
|
@@ -183,6 +185,51 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
183
185
|
"Archivo lightning %s creado con plantilla inicial", lightning_file
|
|
184
186
|
)
|
|
185
187
|
|
|
188
|
+
async def create_sun_file(self):
|
|
189
|
+
"""Crea el archivo sun_{town_id}_data.json con datos iniciales de sunrise y sunset."""
|
|
190
|
+
if not self.selected_municipi or not self.latitude or not self.longitude:
|
|
191
|
+
_LOGGER.warning("No se puede crear sun_{town_id}_data.json: faltan municipio o coordenadas")
|
|
192
|
+
return
|
|
193
|
+
|
|
194
|
+
town_id = self.selected_municipi["codi"]
|
|
195
|
+
files_dir = get_storage_dir(self.hass, "files")
|
|
196
|
+
sun_file = files_dir / f"sun_{town_id}_data.json"
|
|
197
|
+
|
|
198
|
+
if not sun_file.exists():
|
|
199
|
+
try:
|
|
200
|
+
# Crear objeto LocationInfo con las coordenadas y zona horaria
|
|
201
|
+
location = LocationInfo(
|
|
202
|
+
name=self.selected_municipi["nom"],
|
|
203
|
+
region="Catalonia",
|
|
204
|
+
timezone="Europe/Madrid",
|
|
205
|
+
latitude=self.latitude,
|
|
206
|
+
longitude=self.longitude
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
# Calcular sunrise y sunset para el día actual
|
|
210
|
+
current_time = datetime.now(timezone.utc).astimezone(TIMEZONE)
|
|
211
|
+
sun_data = sun(location.observer, date=current_time.date(), tzinfo=TIMEZONE)
|
|
212
|
+
|
|
213
|
+
# Formatear los datos para el archivo
|
|
214
|
+
sun_data_formatted = {
|
|
215
|
+
"actualitzat": {"dataUpdate": current_time.isoformat()},
|
|
216
|
+
"dades": [
|
|
217
|
+
{
|
|
218
|
+
"sunrise": sun_data["sunrise"].isoformat(),
|
|
219
|
+
"sunset": sun_data["sunset"].isoformat(),
|
|
220
|
+
"date": current_time.date().isoformat()
|
|
221
|
+
}
|
|
222
|
+
]
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
# Guardar el archivo
|
|
226
|
+
async with aiofiles.open(sun_file, "w", encoding="utf-8") as file:
|
|
227
|
+
await file.write(json.dumps(sun_data_formatted, ensure_ascii=False, indent=4))
|
|
228
|
+
_LOGGER.info("Archivo sun_%s_data.json creado con datos iniciales", town_id)
|
|
229
|
+
|
|
230
|
+
except Exception as ex:
|
|
231
|
+
_LOGGER.error("Error al crear sun_%s_data.json: %s", town_id, ex)
|
|
232
|
+
|
|
186
233
|
async def async_step_user(
|
|
187
234
|
self, user_input: dict[str, Any] | None = None
|
|
188
235
|
) -> ConfigFlowResult:
|
|
@@ -319,7 +366,9 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
319
366
|
self.province_name = station_metadata.get("provincia", {}).get("nom", "")
|
|
320
367
|
self.station_status = station_metadata.get("estats", [{}])[0].get("codi", "")
|
|
321
368
|
|
|
369
|
+
# Crear archivos de alertas y sun
|
|
322
370
|
await self.create_alerts_file()
|
|
371
|
+
await self.create_sun_file()
|
|
323
372
|
return await self.async_step_set_api_limits()
|
|
324
373
|
except Exception as ex:
|
|
325
374
|
_LOGGER.error("Error al obtener los metadatos de la estación: %s", ex)
|
|
@@ -384,4 +433,4 @@ class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
|
384
433
|
@callback
|
|
385
434
|
def async_get_options_flow(config_entry: ConfigEntry) -> MeteocatOptionsFlowHandler:
|
|
386
435
|
"""Devuelve el flujo de opciones para esta configuración."""
|
|
387
|
-
return MeteocatOptionsFlowHandler(config_entry)
|
|
436
|
+
return MeteocatOptionsFlowHandler(config_entry)
|
|
@@ -42,6 +42,9 @@ QUOTA_BASIC = "quota_basic"
|
|
|
42
42
|
QUOTA_XEMA = "quota_xema"
|
|
43
43
|
QUOTA_QUERIES = "quota_queries"
|
|
44
44
|
LIGHTNING_FILE_STATUS = "lightning_file_status"
|
|
45
|
+
SUNRISE = "sunrise"
|
|
46
|
+
SUNSET = "sunset"
|
|
47
|
+
SUN_FILE_STATUS = "sun_file_status"
|
|
45
48
|
|
|
46
49
|
from homeassistant.const import Platform
|
|
47
50
|
|
|
@@ -6,11 +6,13 @@ import logging
|
|
|
6
6
|
import asyncio
|
|
7
7
|
import unicodedata
|
|
8
8
|
from pathlib import Path
|
|
9
|
-
from
|
|
9
|
+
from astral.sun import sun
|
|
10
|
+
from astral import LocationInfo
|
|
11
|
+
from datetime import date, datetime, timedelta, timezone, time
|
|
10
12
|
from zoneinfo import ZoneInfo
|
|
11
13
|
from typing import Dict, Any
|
|
12
14
|
|
|
13
|
-
from homeassistant.core import HomeAssistant
|
|
15
|
+
from homeassistant.core import HomeAssistant, EVENT_HOMEASSISTANT_START
|
|
14
16
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
15
17
|
from homeassistant.exceptions import ConfigEntryNotReady
|
|
16
18
|
from homeassistant.components.weather import Forecast
|
|
@@ -67,6 +69,8 @@ DEFAULT_QUOTES_UPDATE_INTERVAL = timedelta(minutes=10)
|
|
|
67
69
|
DEFAULT_QUOTES_FILE_UPDATE_INTERVAL = timedelta(minutes=5)
|
|
68
70
|
DEFAULT_LIGHTNING_UPDATE_INTERVAL = timedelta(minutes=10)
|
|
69
71
|
DEFAULT_LIGHTNING_FILE_UPDATE_INTERVAL = timedelta(minutes=5)
|
|
72
|
+
DEFAULT_SUN_UPDATE_INTERVAL = timedelta(minutes=1)
|
|
73
|
+
DEFAULT_SUN_FILE_UPDATE_INTERVAL = timedelta(seconds=30)
|
|
70
74
|
|
|
71
75
|
# Definir la zona horaria local
|
|
72
76
|
TIMEZONE = ZoneInfo("Europe/Madrid")
|
|
@@ -1924,3 +1928,185 @@ class MeteocatLightningFileCoordinator(DataUpdateCoordinator):
|
|
|
1924
1928
|
"cg+": 0,
|
|
1925
1929
|
"total": 0
|
|
1926
1930
|
}
|
|
1931
|
+
|
|
1932
|
+
class MeteocatSunCoordinator(DataUpdateCoordinator):
|
|
1933
|
+
"""Coordinator para manejar la actualización de los datos de sol calculados con Astral."""
|
|
1934
|
+
|
|
1935
|
+
def __init__(
|
|
1936
|
+
self,
|
|
1937
|
+
hass: HomeAssistant,
|
|
1938
|
+
entry_data: dict,
|
|
1939
|
+
):
|
|
1940
|
+
"""
|
|
1941
|
+
Inicializa el coordinador de sol de Meteocat.
|
|
1942
|
+
|
|
1943
|
+
Args:
|
|
1944
|
+
hass (HomeAssistant): Instancia de Home Assistant.
|
|
1945
|
+
entry_data (dict): Datos de configuración obtenidos de core.config_entries.
|
|
1946
|
+
"""
|
|
1947
|
+
self.latitude = entry_data.get("latitude")
|
|
1948
|
+
self.longitude = entry_data.get("longitude")
|
|
1949
|
+
self.timezone_str = hass.config.time_zone or "Europe/Madrid"
|
|
1950
|
+
self.town_id = entry_data.get("town_id")
|
|
1951
|
+
|
|
1952
|
+
self.location = LocationInfo(
|
|
1953
|
+
name=entry_data.get("town_name", "Municipio"),
|
|
1954
|
+
region="Spain",
|
|
1955
|
+
timezone=self.timezone_str,
|
|
1956
|
+
latitude=self.latitude,
|
|
1957
|
+
longitude=self.longitude,
|
|
1958
|
+
)
|
|
1959
|
+
|
|
1960
|
+
# Ruta persistente en /config/meteocat_files/files
|
|
1961
|
+
files_folder = get_storage_dir(hass, "files")
|
|
1962
|
+
self.sun_file = files_folder / f"sun_{self.town_id.lower()}_data.json"
|
|
1963
|
+
|
|
1964
|
+
super().__init__(
|
|
1965
|
+
hass,
|
|
1966
|
+
_LOGGER,
|
|
1967
|
+
name=f"{DOMAIN} Sun Coordinator",
|
|
1968
|
+
update_interval=DEFAULT_SUN_UPDATE_INTERVAL, # Ej. timedelta(minutes=1)
|
|
1969
|
+
)
|
|
1970
|
+
|
|
1971
|
+
async def _async_update_data(self) -> Dict:
|
|
1972
|
+
"""Actualiza los datos de sol calculados o usa datos en caché según si los eventos han pasado."""
|
|
1973
|
+
existing_data = await load_json_from_file(self.sun_file) or {}
|
|
1974
|
+
|
|
1975
|
+
now = datetime.now(tz=ZoneInfo(self.timezone_str))
|
|
1976
|
+
|
|
1977
|
+
if not existing_data or "dades" not in existing_data or not existing_data["dades"]:
|
|
1978
|
+
return await self._calculate_and_save_new_data()
|
|
1979
|
+
|
|
1980
|
+
last_update_str = existing_data.get('actualitzat', {}).get('dataUpdate')
|
|
1981
|
+
if not last_update_str:
|
|
1982
|
+
return await self._calculate_and_save_new_data()
|
|
1983
|
+
|
|
1984
|
+
last_update = datetime.fromisoformat(last_update_str)
|
|
1985
|
+
|
|
1986
|
+
dades = existing_data["dades"][0]
|
|
1987
|
+
saved_sunrise = datetime.fromisoformat(dades["sunrise"])
|
|
1988
|
+
saved_sunset = datetime.fromisoformat(dades["sunset"])
|
|
1989
|
+
|
|
1990
|
+
# Verificar si los datos necesitan actualización
|
|
1991
|
+
if now > saved_sunrise or now > saved_sunset:
|
|
1992
|
+
return await self._calculate_and_save_new_data()
|
|
1993
|
+
else:
|
|
1994
|
+
_LOGGER.debug("Usando datos existentes de sol: %s", existing_data)
|
|
1995
|
+
return {"actualizado": existing_data['actualitzat']['dataUpdate']}
|
|
1996
|
+
|
|
1997
|
+
async def _calculate_and_save_new_data(self):
|
|
1998
|
+
"""Calcula nuevos datos de sol y los guarda en el archivo JSON."""
|
|
1999
|
+
try:
|
|
2000
|
+
now = datetime.now(tz=ZoneInfo(self.timezone_str))
|
|
2001
|
+
today = now.date()
|
|
2002
|
+
|
|
2003
|
+
sun_data_today = sun(self.location.observer, date=today, tzinfo=ZoneInfo(self.timezone_str))
|
|
2004
|
+
sunrise = sun_data_today["sunrise"]
|
|
2005
|
+
sunset = sun_data_today["sunset"]
|
|
2006
|
+
|
|
2007
|
+
if now > sunset:
|
|
2008
|
+
next_day = today + timedelta(days=1)
|
|
2009
|
+
sun_data_next = sun(self.location.observer, date=next_day, tzinfo=ZoneInfo(self.timezone_str))
|
|
2010
|
+
sunrise = sun_data_next["sunrise"]
|
|
2011
|
+
sunset = sun_data_next["sunset"]
|
|
2012
|
+
elif now > sunrise:
|
|
2013
|
+
next_day = today + timedelta(days=1)
|
|
2014
|
+
sun_data_next = sun(self.location.observer, date=next_day, tzinfo=ZoneInfo(self.timezone_str))
|
|
2015
|
+
sunrise = sun_data_next["sunrise"]
|
|
2016
|
+
# sunset permanece como el de hoy
|
|
2017
|
+
|
|
2018
|
+
# Estructurar los datos en el formato correcto
|
|
2019
|
+
current_time = now.isoformat()
|
|
2020
|
+
data_with_timestamp = {
|
|
2021
|
+
"actualitzat": {
|
|
2022
|
+
"dataUpdate": current_time
|
|
2023
|
+
},
|
|
2024
|
+
"dades": [
|
|
2025
|
+
{
|
|
2026
|
+
"sunrise": sunrise.isoformat(),
|
|
2027
|
+
"sunset": sunset.isoformat()
|
|
2028
|
+
}
|
|
2029
|
+
]
|
|
2030
|
+
}
|
|
2031
|
+
|
|
2032
|
+
# Guardar los datos en un archivo JSON
|
|
2033
|
+
await save_json_to_file(data_with_timestamp, self.sun_file)
|
|
2034
|
+
|
|
2035
|
+
_LOGGER.debug("Datos de sol actualizados exitosamente: %s", data_with_timestamp)
|
|
2036
|
+
|
|
2037
|
+
return {"actualizado": data_with_timestamp['actualitzat']['dataUpdate']}
|
|
2038
|
+
|
|
2039
|
+
except Exception as err:
|
|
2040
|
+
_LOGGER.exception("Error inesperado al calcular los datos de sol: %s", err)
|
|
2041
|
+
|
|
2042
|
+
# Intentar cargar datos en caché si falla el cálculo (aunque es improbable)
|
|
2043
|
+
cached_data = await load_json_from_file(self.sun_file)
|
|
2044
|
+
if cached_data:
|
|
2045
|
+
_LOGGER.warning("Usando datos en caché para los datos de sol.")
|
|
2046
|
+
return {"actualizado": cached_data['actualitzat']['dataUpdate']}
|
|
2047
|
+
|
|
2048
|
+
_LOGGER.error("No se pudo calcular datos actualizados ni cargar datos en caché.")
|
|
2049
|
+
return None
|
|
2050
|
+
|
|
2051
|
+
class MeteocatSunFileCoordinator(DataUpdateCoordinator):
|
|
2052
|
+
"""Coordinator para manejar la actualización de los datos de sol desde sun_{town_id}.json."""
|
|
2053
|
+
|
|
2054
|
+
def __init__(
|
|
2055
|
+
self,
|
|
2056
|
+
hass: HomeAssistant,
|
|
2057
|
+
entry_data: dict,
|
|
2058
|
+
):
|
|
2059
|
+
"""
|
|
2060
|
+
Inicializa el coordinador de sol desde archivo.
|
|
2061
|
+
|
|
2062
|
+
Args:
|
|
2063
|
+
hass (HomeAssistant): Instancia de Home Assistant.
|
|
2064
|
+
entry_data (dict): Datos de configuración de la entrada.
|
|
2065
|
+
"""
|
|
2066
|
+
self.town_id = entry_data["town_id"]
|
|
2067
|
+
self.timezone_str = hass.config.time_zone or "Europe/Madrid"
|
|
2068
|
+
|
|
2069
|
+
# Ruta persistente en /config/meteocat_files/files
|
|
2070
|
+
files_folder = get_storage_dir(hass, "files")
|
|
2071
|
+
self.sun_file = files_folder / f"sun_{self.town_id.lower()}_data.json"
|
|
2072
|
+
|
|
2073
|
+
super().__init__(
|
|
2074
|
+
hass,
|
|
2075
|
+
_LOGGER,
|
|
2076
|
+
name="Meteocat Sun File Coordinator",
|
|
2077
|
+
update_interval=DEFAULT_SUN_FILE_UPDATE_INTERVAL, # Ej. timedelta(seconds=30)
|
|
2078
|
+
)
|
|
2079
|
+
|
|
2080
|
+
async def _async_update_data(self) -> Dict[str, Any]:
|
|
2081
|
+
"""Carga los datos de sol desde el archivo JSON y procesa la información."""
|
|
2082
|
+
existing_data = await load_json_from_file(self.sun_file)
|
|
2083
|
+
|
|
2084
|
+
if not existing_data or "dades" not in existing_data or not existing_data["dades"]:
|
|
2085
|
+
_LOGGER.warning("No se encontraron datos en %s.", self.sun_file)
|
|
2086
|
+
return self._reset_data()
|
|
2087
|
+
|
|
2088
|
+
update_date_str = existing_data.get("actualitzat", {}).get("dataUpdate", "")
|
|
2089
|
+
update_date = datetime.fromisoformat(update_date_str) if update_date_str else None
|
|
2090
|
+
now = datetime.now(ZoneInfo(self.timezone_str))
|
|
2091
|
+
|
|
2092
|
+
dades = existing_data["dades"][0]
|
|
2093
|
+
saved_sunrise = datetime.fromisoformat(dades["sunrise"])
|
|
2094
|
+
saved_sunset = datetime.fromisoformat(dades["sunset"])
|
|
2095
|
+
|
|
2096
|
+
if saved_sunrise < now and saved_sunset < now:
|
|
2097
|
+
_LOGGER.info("Los datos de sol están caducados. Reiniciando valores.")
|
|
2098
|
+
return self._reset_data()
|
|
2099
|
+
else:
|
|
2100
|
+
return {
|
|
2101
|
+
"actualizado": update_date.isoformat() if update_date else now.isoformat(),
|
|
2102
|
+
"sunrise": dades.get("sunrise"),
|
|
2103
|
+
"sunset": dades.get("sunset")
|
|
2104
|
+
}
|
|
2105
|
+
|
|
2106
|
+
def _reset_data(self):
|
|
2107
|
+
"""Resetea los datos a valores nulos."""
|
|
2108
|
+
return {
|
|
2109
|
+
"actualizado": datetime.now(ZoneInfo(self.timezone_str)).isoformat(),
|
|
2110
|
+
"sunrise": None,
|
|
2111
|
+
"sunset": None
|
|
2112
|
+
}
|
|
@@ -13,8 +13,11 @@ from .const import (
|
|
|
13
13
|
LIMIT_PREDICCIO,
|
|
14
14
|
LIMIT_XDDE,
|
|
15
15
|
LIMIT_QUOTA,
|
|
16
|
-
LIMIT_BASIC
|
|
16
|
+
LIMIT_BASIC,
|
|
17
|
+
LATITUDE,
|
|
18
|
+
LONGITUDE,
|
|
17
19
|
)
|
|
20
|
+
|
|
18
21
|
from meteocatpy.town import MeteocatTown
|
|
19
22
|
from meteocatpy.exceptions import (
|
|
20
23
|
BadRequestError,
|
|
@@ -35,6 +38,11 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
|
|
|
35
38
|
self.api_key: str | None = None
|
|
36
39
|
self.limit_xema: int | None = None
|
|
37
40
|
self.limit_prediccio: int | None = None
|
|
41
|
+
self.limit_xdde: int | None = None
|
|
42
|
+
self.limit_quota: int | None = None
|
|
43
|
+
self.limit_basic: int | None = None
|
|
44
|
+
self.latitude: float | None = None
|
|
45
|
+
self.longitude: float | None = None
|
|
38
46
|
|
|
39
47
|
async def async_step_init(self, user_input: dict | None = None):
|
|
40
48
|
"""Paso inicial del flujo de opciones."""
|
|
@@ -45,6 +53,8 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
|
|
|
45
53
|
return await self.async_step_update_limits_only()
|
|
46
54
|
elif user_input["option"] == "regenerate_assets":
|
|
47
55
|
return await self.async_step_confirm_regenerate_assets()
|
|
56
|
+
elif user_input["option"] == "update_coordinates":
|
|
57
|
+
return await self.async_step_update_coordinates()
|
|
48
58
|
|
|
49
59
|
return self.async_show_form(
|
|
50
60
|
step_id="init",
|
|
@@ -54,7 +64,8 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
|
|
|
54
64
|
options=[
|
|
55
65
|
"update_api_and_limits",
|
|
56
66
|
"update_limits_only",
|
|
57
|
-
"regenerate_assets"
|
|
67
|
+
"regenerate_assets",
|
|
68
|
+
"update_coordinates" # Nueva opción
|
|
58
69
|
],
|
|
59
70
|
translation_key="option"
|
|
60
71
|
)
|
|
@@ -177,7 +188,54 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
|
|
|
177
188
|
return self.async_show_form(
|
|
178
189
|
step_id="update_limits_only", data_schema=schema, errors=errors
|
|
179
190
|
)
|
|
180
|
-
|
|
191
|
+
|
|
192
|
+
async def async_step_update_coordinates(self, user_input: dict | None = None):
|
|
193
|
+
"""Permite al usuario actualizar las coordenadas (latitude, longitude)."""
|
|
194
|
+
errors = {}
|
|
195
|
+
|
|
196
|
+
if user_input is not None:
|
|
197
|
+
self.latitude = user_input.get(LATITUDE)
|
|
198
|
+
self.longitude = user_input.get(LONGITUDE)
|
|
199
|
+
|
|
200
|
+
# Validar que las coordenadas estén dentro del rango de Cataluña
|
|
201
|
+
if not (40.5 <= self.latitude <= 42.5 and 0.1 <= self.longitude <= 3.3):
|
|
202
|
+
_LOGGER.error(
|
|
203
|
+
"Coordenadas fuera del rango de Cataluña (latitude: %s, longitude: %s).",
|
|
204
|
+
self.latitude, self.longitude
|
|
205
|
+
)
|
|
206
|
+
errors["base"] = "invalid_coordinates"
|
|
207
|
+
else:
|
|
208
|
+
# Actualizar la configuración con las nuevas coordenadas
|
|
209
|
+
self.hass.config_entries.async_update_entry(
|
|
210
|
+
self._config_entry,
|
|
211
|
+
data={
|
|
212
|
+
**self._config_entry.data,
|
|
213
|
+
LATITUDE: self.latitude,
|
|
214
|
+
LONGITUDE: self.longitude
|
|
215
|
+
},
|
|
216
|
+
)
|
|
217
|
+
# Recargar la integración para aplicar los cambios
|
|
218
|
+
await self.hass.config_entries.async_reload(self._config_entry.entry_id)
|
|
219
|
+
_LOGGER.info(
|
|
220
|
+
"Coordenadas actualizadas a latitude: %s, longitude: %s",
|
|
221
|
+
self.latitude, self.longitude
|
|
222
|
+
)
|
|
223
|
+
return self.async_create_entry(title="", data={})
|
|
224
|
+
|
|
225
|
+
schema = vol.Schema({
|
|
226
|
+
vol.Required(LATITUDE, default=self._config_entry.data.get(LATITUDE)): cv.latitude,
|
|
227
|
+
vol.Required(LONGITUDE, default=self._config_entry.data.get(LONGITUDE)): cv.longitude,
|
|
228
|
+
})
|
|
229
|
+
return self.async_show_form(
|
|
230
|
+
step_id="update_coordinates",
|
|
231
|
+
data_schema=schema,
|
|
232
|
+
errors=errors,
|
|
233
|
+
description_placeholders={
|
|
234
|
+
"current_latitude": self._config_entry.data.get(LATITUDE),
|
|
235
|
+
"current_longitude": self._config_entry.data.get(LONGITUDE)
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
|
|
181
239
|
async def async_step_confirm_regenerate_assets(self, user_input: dict | None = None):
|
|
182
240
|
"""Confirma si el usuario realmente quiere regenerar los assets."""
|
|
183
241
|
if user_input is not None:
|