meteocat 3.0.0 → 3.2.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/sync-gitlab.yml +15 -4
- package/.github/workflows/sync-labels.yml +21 -0
- package/CHANGELOG.md +80 -11
- package/README.md +16 -4
- package/custom_components/meteocat/__init__.py +57 -42
- package/custom_components/meteocat/condition.py +6 -2
- package/custom_components/meteocat/config_flow.py +231 -4
- package/custom_components/meteocat/const.py +17 -2
- package/custom_components/meteocat/coordinator.py +1122 -101
- package/custom_components/meteocat/helpers.py +31 -36
- package/custom_components/meteocat/manifest.json +3 -2
- package/custom_components/meteocat/options_flow.py +71 -3
- package/custom_components/meteocat/sensor.py +660 -247
- package/custom_components/meteocat/strings.json +252 -15
- package/custom_components/meteocat/translations/ca.json +249 -13
- package/custom_components/meteocat/translations/en.json +252 -15
- package/custom_components/meteocat/translations/es.json +252 -15
- package/custom_components/meteocat/version.py +1 -1
- package/filetree.txt +12 -3
- package/hacs.json +1 -1
- package/images/daily_forecast_2_alerts.png +0 -0
- package/images/daily_forecast_no_alerts.png +0 -0
- package/images/diagnostic_sensors.png +0 -0
- package/images/dynamic_sensors.png +0 -0
- package/images/options.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 +6 -0
- package/.github/workflows/close-duplicates.yml +0 -57
|
@@ -4,8 +4,8 @@ import logging
|
|
|
4
4
|
from datetime import datetime
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
from homeassistant.core import HomeAssistant
|
|
7
|
-
from homeassistant.util
|
|
8
|
-
from
|
|
7
|
+
from homeassistant.util import dt as dt_util
|
|
8
|
+
from solarmoonpy.location import Location
|
|
9
9
|
|
|
10
10
|
_LOGGER = logging.getLogger(__name__)
|
|
11
11
|
|
|
@@ -19,45 +19,40 @@ def get_storage_dir(hass: HomeAssistant, subdir: str | None = None) -> Path:
|
|
|
19
19
|
return base_dir
|
|
20
20
|
|
|
21
21
|
# Cálculo de amanecer y atardecer para definir cuando es de noche
|
|
22
|
-
def get_sun_times(
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
def get_sun_times(location: Location, current_time: datetime | None = None) -> tuple[datetime, datetime]:
|
|
23
|
+
"""Obtiene las horas de amanecer y atardecer para una ubicación usando solarmoonpy."""
|
|
24
|
+
now = dt_util.as_local(current_time or dt_util.now())
|
|
25
|
+
today = now.date()
|
|
26
|
+
sunrise = location.sunrise(date=today, local=True)
|
|
27
|
+
sunset = location.sunset(date=today, local=True)
|
|
26
28
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
today = start_of_local_day(as_local(current_time))
|
|
30
|
-
|
|
31
|
-
# Obtén los eventos de amanecer y atardecer del día actual
|
|
32
|
-
sunrise = get_astral_event_date(hass, "sunrise", today)
|
|
33
|
-
sunset = get_astral_event_date(hass, "sunset", today)
|
|
29
|
+
if not sunrise or not sunset:
|
|
30
|
+
raise ValueError("No se pudieron calcular amanecer o atardecer.")
|
|
34
31
|
|
|
35
32
|
_LOGGER.debug(
|
|
36
|
-
"
|
|
37
|
-
sunrise,
|
|
38
|
-
sunset,
|
|
39
|
-
as_local(current_time),
|
|
33
|
+
"[solarmoonpy] Amanecer: %s, Atardecer: %s, Hora actual: %s",
|
|
34
|
+
sunrise, sunset, now
|
|
40
35
|
)
|
|
36
|
+
return sunrise, sunset
|
|
41
37
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
raise ValueError("No se pudieron determinar los datos de amanecer y atardecer.")
|
|
46
|
-
|
|
47
|
-
def is_night(current_time, hass):
|
|
48
|
-
"""Determina si actualmente es de noche."""
|
|
49
|
-
# Asegúrate de que current_time es aware (UTC)
|
|
38
|
+
def is_night(current_time, location: Location) -> bool:
|
|
39
|
+
"""Determina si actualmente es de noche usando una instancia de Location."""
|
|
40
|
+
# Asegurarse de que current_time sea aware y en zona local
|
|
50
41
|
if current_time.tzinfo is None:
|
|
51
|
-
current_time
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
_LOGGER.warning("current_time sin zona horaria, asumiendo UTC")
|
|
43
|
+
current_time = dt_util.as_local(dt_util.utc_to_local(current_time))
|
|
44
|
+
else:
|
|
45
|
+
current_time = dt_util.as_local(current_time)
|
|
46
|
+
|
|
47
|
+
try:
|
|
48
|
+
sunrise, sunset = get_sun_times(location, current_time)
|
|
49
|
+
except Exception as e:
|
|
50
|
+
_LOGGER.warning("Fallo al calcular amanecer/atardecer con solarmoonpy: %s", e)
|
|
51
|
+
return False # fallback seguro
|
|
52
|
+
|
|
53
|
+
is_night_now = current_time < sunrise or current_time > sunset
|
|
55
54
|
_LOGGER.debug(
|
|
56
|
-
"Hora actual: %s
|
|
57
|
-
|
|
58
|
-
as_local(sunrise),
|
|
59
|
-
as_local(sunset),
|
|
55
|
+
"[solarmoonpy] Hora actual: %s | Amanecer: %s | Atardecer: %s → Noche: %s",
|
|
56
|
+
current_time, sunrise, sunset, is_night_now
|
|
60
57
|
)
|
|
61
|
-
|
|
62
|
-
# Es de noche si es antes del amanecer o después del atardecer
|
|
63
|
-
return current_time < sunrise or current_time > sunset
|
|
58
|
+
return is_night_now
|
|
@@ -13,8 +13,12 @@ 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,
|
|
19
|
+
ALTITUDE,
|
|
17
20
|
)
|
|
21
|
+
|
|
18
22
|
from meteocatpy.town import MeteocatTown
|
|
19
23
|
from meteocatpy.exceptions import (
|
|
20
24
|
BadRequestError,
|
|
@@ -35,6 +39,12 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
|
|
|
35
39
|
self.api_key: str | None = None
|
|
36
40
|
self.limit_xema: int | None = None
|
|
37
41
|
self.limit_prediccio: int | None = None
|
|
42
|
+
self.limit_xdde: int | None = None
|
|
43
|
+
self.limit_quota: int | None = None
|
|
44
|
+
self.limit_basic: int | None = None
|
|
45
|
+
self.latitude: float | None = None
|
|
46
|
+
self.longitude: float | None = None
|
|
47
|
+
self.altitude: float | None = None
|
|
38
48
|
|
|
39
49
|
async def async_step_init(self, user_input: dict | None = None):
|
|
40
50
|
"""Paso inicial del flujo de opciones."""
|
|
@@ -45,6 +55,8 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
|
|
|
45
55
|
return await self.async_step_update_limits_only()
|
|
46
56
|
elif user_input["option"] == "regenerate_assets":
|
|
47
57
|
return await self.async_step_confirm_regenerate_assets()
|
|
58
|
+
elif user_input["option"] == "update_coordinates":
|
|
59
|
+
return await self.async_step_update_coordinates()
|
|
48
60
|
|
|
49
61
|
return self.async_show_form(
|
|
50
62
|
step_id="init",
|
|
@@ -54,7 +66,8 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
|
|
|
54
66
|
options=[
|
|
55
67
|
"update_api_and_limits",
|
|
56
68
|
"update_limits_only",
|
|
57
|
-
"regenerate_assets"
|
|
69
|
+
"regenerate_assets",
|
|
70
|
+
"update_coordinates"
|
|
58
71
|
],
|
|
59
72
|
translation_key="option"
|
|
60
73
|
)
|
|
@@ -177,7 +190,62 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
|
|
|
177
190
|
return self.async_show_form(
|
|
178
191
|
step_id="update_limits_only", data_schema=schema, errors=errors
|
|
179
192
|
)
|
|
180
|
-
|
|
193
|
+
|
|
194
|
+
async def async_step_update_coordinates(self, user_input: dict | None = None):
|
|
195
|
+
"""Permite al usuario actualizar las coordenadas (latitude, longitude)."""
|
|
196
|
+
errors = {}
|
|
197
|
+
|
|
198
|
+
if user_input is not None:
|
|
199
|
+
self.latitude = user_input.get(LATITUDE)
|
|
200
|
+
self.longitude = user_input.get(LONGITUDE)
|
|
201
|
+
self.altitude = user_input.get(ALTITUDE)
|
|
202
|
+
|
|
203
|
+
# Validar que las coordenadas estén dentro del rango de Cataluña
|
|
204
|
+
if not (40.5 <= self.latitude <= 42.5 and 0.1 <= self.longitude <= 3.3):
|
|
205
|
+
_LOGGER.error(
|
|
206
|
+
"Coordenadas fuera del rango de Cataluña (latitude: %s, longitude: %s).",
|
|
207
|
+
self.latitude, self.longitude
|
|
208
|
+
)
|
|
209
|
+
errors["base"] = "invalid_coordinates"
|
|
210
|
+
# Validar que la altitud sea positiva
|
|
211
|
+
elif self.altitude < 0:
|
|
212
|
+
_LOGGER.error("Altitud inválida: %s. Debe ser >= 0.", self.altitude)
|
|
213
|
+
errors["base"] = "invalid_altitude"
|
|
214
|
+
else:
|
|
215
|
+
# Actualizar la configuración con las nuevas coordenadas
|
|
216
|
+
self.hass.config_entries.async_update_entry(
|
|
217
|
+
self._config_entry,
|
|
218
|
+
data={
|
|
219
|
+
**self._config_entry.data,
|
|
220
|
+
LATITUDE: self.latitude,
|
|
221
|
+
LONGITUDE: self.longitude,
|
|
222
|
+
ALTITUDE: self.altitude
|
|
223
|
+
},
|
|
224
|
+
)
|
|
225
|
+
# Recargar la integración para aplicar los cambios
|
|
226
|
+
await self.hass.config_entries.async_reload(self._config_entry.entry_id)
|
|
227
|
+
_LOGGER.info(
|
|
228
|
+
"Coordenadas actualizadas a latitude: %s, longitude: %s, altitude=%s.",
|
|
229
|
+
self.latitude, self.longitude, self.altitude
|
|
230
|
+
)
|
|
231
|
+
return self.async_create_entry(title="", data={})
|
|
232
|
+
|
|
233
|
+
schema = vol.Schema({
|
|
234
|
+
vol.Required(LATITUDE, default=self._config_entry.data.get(LATITUDE)): cv.latitude,
|
|
235
|
+
vol.Required(LONGITUDE, default=self._config_entry.data.get(LONGITUDE)): cv.longitude,
|
|
236
|
+
vol.Required(ALTITUDE, default=self._config_entry.data.get(ALTITUDE)): vol.Coerce(float),
|
|
237
|
+
})
|
|
238
|
+
return self.async_show_form(
|
|
239
|
+
step_id="update_coordinates",
|
|
240
|
+
data_schema=schema,
|
|
241
|
+
errors=errors,
|
|
242
|
+
description_placeholders={
|
|
243
|
+
"current_latitude": self._config_entry.data.get(LATITUDE),
|
|
244
|
+
"current_longitude": self._config_entry.data.get(LONGITUDE),
|
|
245
|
+
"current_altitude": self._config_entry.data.get(ALTITUDE, 0.0)
|
|
246
|
+
}
|
|
247
|
+
)
|
|
248
|
+
|
|
181
249
|
async def async_step_confirm_regenerate_assets(self, user_input: dict | None = None):
|
|
182
250
|
"""Confirma si el usuario realmente quiere regenerar los assets."""
|
|
183
251
|
if user_input is not None:
|