meteocat 0.1.19
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/workflows/release.yml +33 -0
- package/.pre-commit-config.yaml +37 -0
- package/.releaserc +11 -0
- package/.releaserc.toml +14 -0
- package/AUTHORS.md +12 -0
- package/CHANGELOG.md +361 -0
- package/LICENSE +194 -0
- package/README.md +41 -0
- package/custom_components/meteocat/__init__.py +139 -0
- package/custom_components/meteocat/condition.py +28 -0
- package/custom_components/meteocat/config_flow.py +192 -0
- package/custom_components/meteocat/const.py +46 -0
- package/custom_components/meteocat/coordinator.py +177 -0
- package/custom_components/meteocat/entity.py +91 -0
- package/custom_components/meteocat/helpers.py +42 -0
- package/custom_components/meteocat/manifest.json +12 -0
- package/custom_components/meteocat/options_flow.py +71 -0
- package/custom_components/meteocat/sensor.py +190 -0
- package/custom_components/meteocat/strings.json +26 -0
- package/custom_components/meteocat/translations/ca.json +26 -0
- package/custom_components/meteocat/translations/en.json +26 -0
- package/custom_components/meteocat/translations/es.json +26 -0
- package/custom_components/meteocat/version.py +2 -0
- package/filetree.py +48 -0
- package/filetree.txt +43 -0
- package/hacs.json +6 -0
- package/package.json +22 -0
- package/poetry.lock +3205 -0
- package/pyproject.toml +64 -0
- package/releaserc.json +18 -0
- package/requirements.test.txt +3 -0
- package/setup.cfg +64 -0
- package/setup.py +10 -0
- package/tests/__init__.py +0 -0
- package/tests/bandit.yaml +17 -0
- package/tests/conftest.py +19 -0
- package/tests/test_init.py +9 -0
package/README.md
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Meteocat for Home Assistant
|
|
2
|
+
|
|
3
|
+
[](https://opensource.org/licenses/Apache-2.0)
|
|
4
|
+
[](https://pypi.org/project/meteocat)
|
|
5
|
+
[](https://gitlab.com/figorr/meteocat/commits/master)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
This is a project to obtain meteorological data from the Meteocat API.
|
|
9
|
+
|
|
10
|
+
**NOTE:** Meteocat API requires to use an API_KEY, you should ask to (https://apidocs.meteocat.gencat.cat/documentacio/acces-ciutada-i-administracio/)
|
|
11
|
+
|
|
12
|
+
# Credits
|
|
13
|
+
|
|
14
|
+
This is a personal project.
|
|
15
|
+
|
|
16
|
+
Authors:
|
|
17
|
+
- Figorr
|
|
18
|
+
|
|
19
|
+
# Contributing
|
|
20
|
+
|
|
21
|
+
1. [Check for open features/bugs](https://gitlab.com/figorr/meteocat/issues)
|
|
22
|
+
or [initiate a discussion on one](https://gitlab.com/figorr/meteocat/issues/new).
|
|
23
|
+
2. [Fork the repository](https://gitlab.com/figorr/meteocat/forks/new).
|
|
24
|
+
3. Install the dev environment: `make init`.
|
|
25
|
+
4. Enter the virtual environment: `pipenv shell`
|
|
26
|
+
5. Code your new feature or bug fix.
|
|
27
|
+
6. Write a test that covers your new functionality.
|
|
28
|
+
7. Update `README.md` with any new documentation.
|
|
29
|
+
8. Run tests and ensure 100% code coverage for your contribution: `make coverage`
|
|
30
|
+
9. Ensure you have no linting errors: `make lint`
|
|
31
|
+
10. Ensure you have typed your code correctly: `make typing`
|
|
32
|
+
11. Add yourself to `AUTHORS.md`.
|
|
33
|
+
12. Submit a pull request!
|
|
34
|
+
|
|
35
|
+
# License
|
|
36
|
+
|
|
37
|
+
[Apache-2.0](LICENSE). By providing a contribution, you agree the contribution is licensed under Apache-2.0.
|
|
38
|
+
|
|
39
|
+
# API Reference
|
|
40
|
+
|
|
41
|
+
[See the docs 馃摎](https://apidocs.meteocat.gencat.cat/section/informacio-general/).
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import logging
|
|
5
|
+
from homeassistant import core
|
|
6
|
+
from homeassistant.config_entries import ConfigEntry
|
|
7
|
+
from homeassistant.core import HomeAssistant
|
|
8
|
+
from homeassistant.exceptions import HomeAssistantError
|
|
9
|
+
from .coordinator import MeteocatSensorCoordinator, MeteocatEntityCoordinator
|
|
10
|
+
from .const import (
|
|
11
|
+
DOMAIN,
|
|
12
|
+
CONF_API_KEY,
|
|
13
|
+
TOWN_NAME,
|
|
14
|
+
TOWN_ID,
|
|
15
|
+
VARIABLE_NAME,
|
|
16
|
+
VARIABLE_ID,
|
|
17
|
+
STATION_NAME,
|
|
18
|
+
STATION_ID,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
_LOGGER = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
# Versi贸n
|
|
24
|
+
__version__ = "0.1.18"
|
|
25
|
+
|
|
26
|
+
# Plataformas soportadas por la integraci贸n
|
|
27
|
+
PLATFORMS = ["sensor", "entity"]
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
async def async_setup(hass: core.HomeAssistant, config: dict) -> bool:
|
|
31
|
+
"""Configuraci贸n inicial del componente Meteocat."""
|
|
32
|
+
return True
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
36
|
+
"""Configura una entrada de configuraci贸n para Meteocat."""
|
|
37
|
+
_LOGGER.info("Configurando la integraci贸n de Meteocat...")
|
|
38
|
+
|
|
39
|
+
# Extraer los datos necesarios de la entrada de configuraci贸n
|
|
40
|
+
entry_data = entry.data
|
|
41
|
+
required_fields = [
|
|
42
|
+
CONF_API_KEY, TOWN_NAME, TOWN_ID, VARIABLE_ID, STATION_NAME, STATION_ID
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
# Validar que todos los campos requeridos est茅n presentes
|
|
46
|
+
if not all(field in entry_data for field in required_fields):
|
|
47
|
+
_LOGGER.error("Faltan datos en la configuraci贸n. Por favor, reconfigura la integraci贸n.")
|
|
48
|
+
return False
|
|
49
|
+
|
|
50
|
+
api_key = entry_data[CONF_API_KEY]
|
|
51
|
+
town_name = entry_data[TOWN_NAME]
|
|
52
|
+
town_id = entry_data[TOWN_ID]
|
|
53
|
+
variable_name = entry_data[VARIABLE_NAME]
|
|
54
|
+
variable_id = entry_data[VARIABLE_ID]
|
|
55
|
+
station_name = entry_data[STATION_NAME]
|
|
56
|
+
station_id = entry_data[STATION_ID]
|
|
57
|
+
|
|
58
|
+
_LOGGER.info(
|
|
59
|
+
f"Integraci贸n configurada para el municipio '{town_name}' (ID: {town_id}), "
|
|
60
|
+
f"variable '{variable_name}' (ID: {variable_id}), estaci贸n {station_name} (ID: {station_id})."
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Inicializa y refresca los coordinadores
|
|
64
|
+
try:
|
|
65
|
+
# Pasar los datos adicionales al constructor del coordinador
|
|
66
|
+
sensor_coordinator = MeteocatSensorCoordinator(
|
|
67
|
+
hass,
|
|
68
|
+
entry,
|
|
69
|
+
town_name=town_name,
|
|
70
|
+
town_id=town_id,
|
|
71
|
+
station_name=station_name,
|
|
72
|
+
station_id=station_id,
|
|
73
|
+
variable_name=variable_name,
|
|
74
|
+
variable_id=variable_id,
|
|
75
|
+
)
|
|
76
|
+
# Pasar los mismos datos al constructor de MeteocatEntityCoordinator
|
|
77
|
+
entity_coordinator = MeteocatEntityCoordinator(
|
|
78
|
+
hass,
|
|
79
|
+
entry,
|
|
80
|
+
town_name=town_name,
|
|
81
|
+
town_id=town_id,
|
|
82
|
+
station_name=station_name,
|
|
83
|
+
station_id=station_id,
|
|
84
|
+
variable_name=variable_name,
|
|
85
|
+
variable_id=variable_id,
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
await sensor_coordinator.async_config_entry_first_refresh()
|
|
89
|
+
await entity_coordinator.async_config_entry_first_refresh()
|
|
90
|
+
except HomeAssistantError as err:
|
|
91
|
+
_LOGGER.error(f"Error al inicializar los coordinadores: {err}")
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
# Guardar los datos y los coordinadores en hass.data
|
|
95
|
+
hass.data.setdefault(DOMAIN, {})
|
|
96
|
+
hass.data[DOMAIN][entry.entry_id] = {
|
|
97
|
+
"sensor_coordinator": sensor_coordinator,
|
|
98
|
+
"entity_coordinator": entity_coordinator,
|
|
99
|
+
"api_key": api_key,
|
|
100
|
+
"town_id": town_id,
|
|
101
|
+
"town_name": town_name,
|
|
102
|
+
"station_id": station_id,
|
|
103
|
+
"station_name": station_name,
|
|
104
|
+
"variable_id": variable_id,
|
|
105
|
+
"variable_name": variable_name,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
# Configurar las plataformas
|
|
109
|
+
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
|
110
|
+
|
|
111
|
+
return True
|
|
112
|
+
|
|
113
|
+
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
|
114
|
+
"""Desactiva una entrada de configuraci贸n para Meteocat."""
|
|
115
|
+
if entry.entry_id in hass.data.get(DOMAIN, {}):
|
|
116
|
+
hass.data[DOMAIN].pop(entry.entry_id, None)
|
|
117
|
+
if not hass.data[DOMAIN]: # Si no quedan entradas, elimina el dominio
|
|
118
|
+
hass.data.pop(DOMAIN)
|
|
119
|
+
|
|
120
|
+
# Desinstalar las plataformas
|
|
121
|
+
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
|
|
125
|
+
"""Limpia cualquier dato adicional al desinstalar la integraci贸n."""
|
|
126
|
+
_LOGGER.info(f"Eliminando datos residuales de la integraci贸n: {entry.entry_id}")
|
|
127
|
+
|
|
128
|
+
# Definir la ruta del archivo de s铆mbolos
|
|
129
|
+
assets_folder = hass.config.path("custom_components", DOMAIN, "assets")
|
|
130
|
+
symbols_file = os.path.join(assets_folder, "symbols.json")
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
if os.path.exists(symbols_file):
|
|
134
|
+
os.remove(symbols_file)
|
|
135
|
+
_LOGGER.info("Archivo symbols.json eliminado correctamente.")
|
|
136
|
+
else:
|
|
137
|
+
_LOGGER.info("El archivo symbols.json no se encontr贸.")
|
|
138
|
+
except OSError as e:
|
|
139
|
+
_LOGGER.error(f"Error al intentar eliminar el archivo symbols.json: {e}")
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from datetime import datetime
|
|
4
|
+
from .const import CONDITION_MAPPING
|
|
5
|
+
from .helpers import is_night # Importar la funci贸n is_night de helpers.py
|
|
6
|
+
|
|
7
|
+
def get_condition_from_statcel(codi_estatcel, current_time: datetime, hass) -> dict:
|
|
8
|
+
"""
|
|
9
|
+
Convierte el c贸digo 'estatCel' en condici贸n de Home Assistant.
|
|
10
|
+
|
|
11
|
+
:param codi_estatcel: C贸digo del estado del cielo (celestial state code).
|
|
12
|
+
:param current_time: Fecha y hora actual (datetime).
|
|
13
|
+
:param hass: Instancia de Home Assistant.
|
|
14
|
+
:return: Diccionario con la condici贸n y el icono.
|
|
15
|
+
"""
|
|
16
|
+
# Determinar si es de noche usando la l贸gica centralizada en helpers.py
|
|
17
|
+
is_night_flag = is_night(current_time, hass)
|
|
18
|
+
|
|
19
|
+
# Identificar la condici贸n basada en el c贸digo
|
|
20
|
+
for condition, codes in CONDITION_MAPPING.items():
|
|
21
|
+
if codi_estatcel in codes:
|
|
22
|
+
# Ajustar para condiciones nocturnas si aplica
|
|
23
|
+
if condition == "sunny" and is_night_flag:
|
|
24
|
+
return {"condition": "clear-night", "icon": None}
|
|
25
|
+
return {"condition": condition, "icon": None}
|
|
26
|
+
|
|
27
|
+
# Si no coincide ning煤n c贸digo, devolver condici贸n desconocida
|
|
28
|
+
return {"condition": "unknown", "icon": None}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import voluptuous as vol
|
|
9
|
+
from aiohttp import ClientError
|
|
10
|
+
import aiofiles
|
|
11
|
+
|
|
12
|
+
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
|
|
13
|
+
from homeassistant.core import callback
|
|
14
|
+
from homeassistant.exceptions import HomeAssistantError
|
|
15
|
+
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
|
16
|
+
|
|
17
|
+
from .const import (
|
|
18
|
+
DOMAIN,
|
|
19
|
+
CONF_API_KEY,
|
|
20
|
+
TOWN_NAME,
|
|
21
|
+
TOWN_ID,
|
|
22
|
+
VARIABLE_NAME,
|
|
23
|
+
VARIABLE_ID,
|
|
24
|
+
STATION_NAME,
|
|
25
|
+
STATION_ID
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
from .options_flow import MeteocatOptionsFlowHandler
|
|
29
|
+
from meteocatpy.town import MeteocatTown
|
|
30
|
+
from meteocatpy.symbols import MeteocatSymbols
|
|
31
|
+
from meteocatpy.variables import MeteocatVariables
|
|
32
|
+
from meteocatpy.townstations import MeteocatTownStations
|
|
33
|
+
from meteocatpy.exceptions import BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError
|
|
34
|
+
|
|
35
|
+
_LOGGER = logging.getLogger(__name__)
|
|
36
|
+
|
|
37
|
+
class MeteocatConfigFlow(ConfigFlow, domain=DOMAIN):
|
|
38
|
+
"""Flujo de configuraci贸n para Meteocat."""
|
|
39
|
+
|
|
40
|
+
VERSION = 1
|
|
41
|
+
|
|
42
|
+
def __init__(self):
|
|
43
|
+
self.api_key: str | None = None
|
|
44
|
+
self.municipis: list[dict[str, Any]] = []
|
|
45
|
+
self.selected_municipi: dict[str, Any] | None = None
|
|
46
|
+
self.variable_id: str | None = None
|
|
47
|
+
self.station_id: str | None = None
|
|
48
|
+
self.station_name: str | None = None
|
|
49
|
+
|
|
50
|
+
async def async_step_user(
|
|
51
|
+
self, user_input: dict[str, Any] | None = None
|
|
52
|
+
) -> ConfigFlowResult:
|
|
53
|
+
"""Primer paso: Solicitar la API Key."""
|
|
54
|
+
errors = {}
|
|
55
|
+
|
|
56
|
+
if user_input is not None:
|
|
57
|
+
self.api_key = user_input[CONF_API_KEY]
|
|
58
|
+
|
|
59
|
+
town_client = MeteocatTown(self.api_key)
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
self.municipis = await town_client.get_municipis()
|
|
63
|
+
except (BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError) as ex:
|
|
64
|
+
_LOGGER.error("Error al conectar con la API de Meteocat: %s", ex)
|
|
65
|
+
errors["base"] = "cannot_connect"
|
|
66
|
+
except Exception as ex:
|
|
67
|
+
_LOGGER.error("Error inesperado al validar la API Key: %s", ex)
|
|
68
|
+
errors["base"] = "unknown"
|
|
69
|
+
|
|
70
|
+
if not errors:
|
|
71
|
+
return await self.async_step_select_municipi()
|
|
72
|
+
|
|
73
|
+
schema = vol.Schema({vol.Required(CONF_API_KEY): str})
|
|
74
|
+
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
|
|
75
|
+
|
|
76
|
+
async def async_step_select_municipi(self, user_input: dict[str, Any] | None = None) -> ConfigFlowResult:
|
|
77
|
+
"""Segundo paso: Seleccionar el municipio."""
|
|
78
|
+
errors = {}
|
|
79
|
+
|
|
80
|
+
if user_input is not None:
|
|
81
|
+
selected_codi = user_input["municipi"]
|
|
82
|
+
self.selected_municipi = next(
|
|
83
|
+
(m for m in self.municipis if m["codi"] == selected_codi), None
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
if self.selected_municipi:
|
|
87
|
+
await self.fetch_symbols_and_variables()
|
|
88
|
+
|
|
89
|
+
if not errors and self.selected_municipi:
|
|
90
|
+
return await self.async_step_select_station()
|
|
91
|
+
|
|
92
|
+
schema = vol.Schema(
|
|
93
|
+
{vol.Required("municipi"): vol.In({m["codi"]: m["nom"] for m in self.municipis})}
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return self.async_show_form(step_id="select_municipi", data_schema=schema, errors=errors)
|
|
97
|
+
|
|
98
|
+
async def fetch_symbols_and_variables(self):
|
|
99
|
+
"""Descarga los s铆mbolos y las variables despu茅s de seleccionar el municipio."""
|
|
100
|
+
|
|
101
|
+
errors = {}
|
|
102
|
+
|
|
103
|
+
# Descargar y guardar los s铆mbolos
|
|
104
|
+
assets_dir = Path(__file__).parent / "assets"
|
|
105
|
+
assets_dir.mkdir(parents=True, exist_ok=True)
|
|
106
|
+
symbols_file = assets_dir / "symbols.json"
|
|
107
|
+
symbols_client = MeteocatSymbols(self.api_key)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
symbols_data = await symbols_client.fetch_symbols()
|
|
111
|
+
async with aiofiles.open(symbols_file, "w", encoding="utf-8") as file:
|
|
112
|
+
await file.write(json.dumps({"symbols": symbols_data}, ensure_ascii=False, indent=4))
|
|
113
|
+
except (BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError) as ex:
|
|
114
|
+
_LOGGER.error("Error al descargar o guardar los s铆mbolos: %s", ex)
|
|
115
|
+
errors["base"] = "symbols_download_failed"
|
|
116
|
+
|
|
117
|
+
if not errors:
|
|
118
|
+
variables_client = MeteocatVariables(self.api_key)
|
|
119
|
+
try:
|
|
120
|
+
variables_data = await variables_client.get_variables()
|
|
121
|
+
self.variable_id = next(
|
|
122
|
+
(v["codi"] for v in variables_data if v["nom"].lower() == "temperatura"), None
|
|
123
|
+
)
|
|
124
|
+
if not self.variable_id:
|
|
125
|
+
_LOGGER.error("No se encontr贸 la variable 'Temperatura'")
|
|
126
|
+
errors["base"] = "variable_not_found"
|
|
127
|
+
except (BadRequestError, ForbiddenError, TooManyRequestsError, InternalServerError, UnknownAPIError) as ex:
|
|
128
|
+
_LOGGER.error("Error al obtener las variables: %s", ex)
|
|
129
|
+
errors["base"] = "variables_fetch_failed"
|
|
130
|
+
|
|
131
|
+
if errors:
|
|
132
|
+
raise HomeAssistantError(errors)
|
|
133
|
+
|
|
134
|
+
async def async_step_select_station(
|
|
135
|
+
self, user_input: dict[str, Any] | None = None
|
|
136
|
+
) -> ConfigFlowResult:
|
|
137
|
+
"""Tercer paso: Seleccionar la estaci贸n para la variable seleccionada."""
|
|
138
|
+
errors = {}
|
|
139
|
+
|
|
140
|
+
townstations_client = MeteocatTownStations(self.api_key)
|
|
141
|
+
try:
|
|
142
|
+
stations_data = await townstations_client.get_town_stations(
|
|
143
|
+
self.selected_municipi["codi"], self.variable_id
|
|
144
|
+
)
|
|
145
|
+
except Exception as ex:
|
|
146
|
+
_LOGGER.error("Error al obtener las estaciones: %s", ex)
|
|
147
|
+
errors["base"] = "stations_fetch_failed"
|
|
148
|
+
stations_data = []
|
|
149
|
+
|
|
150
|
+
if user_input is not None:
|
|
151
|
+
selected_station_codi = user_input["station"]
|
|
152
|
+
selected_station = next(
|
|
153
|
+
(station for station in stations_data[0]["variables"][0]["estacions"] if station["codi"] == selected_station_codi),
|
|
154
|
+
None
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if selected_station:
|
|
158
|
+
self.station_id = selected_station["codi"]
|
|
159
|
+
self.station_name = selected_station["nom"]
|
|
160
|
+
|
|
161
|
+
return self.async_create_entry(
|
|
162
|
+
title=self.selected_municipi["nom"],
|
|
163
|
+
data={
|
|
164
|
+
CONF_API_KEY: self.api_key,
|
|
165
|
+
TOWN_NAME: self.selected_municipi["nom"],
|
|
166
|
+
TOWN_ID: self.selected_municipi["codi"],
|
|
167
|
+
VARIABLE_NAME: "Temperatura",
|
|
168
|
+
VARIABLE_ID: str(self.variable_id),
|
|
169
|
+
STATION_NAME: self.station_name,
|
|
170
|
+
STATION_ID: self.station_id
|
|
171
|
+
},
|
|
172
|
+
)
|
|
173
|
+
else:
|
|
174
|
+
errors["base"] = "station_not_found"
|
|
175
|
+
|
|
176
|
+
schema = vol.Schema(
|
|
177
|
+
{
|
|
178
|
+
vol.Required("station"): vol.In(
|
|
179
|
+
{station["codi"]: station["nom"] for station in stations_data[0]["variables"][0]["estacions"]}
|
|
180
|
+
)
|
|
181
|
+
}
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return self.async_show_form(
|
|
185
|
+
step_id="select_station", data_schema=schema, errors=errors
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
@staticmethod
|
|
189
|
+
@callback
|
|
190
|
+
def async_get_options_flow(config_entry: ConfigEntry) -> MeteocatOptionsFlowHandler:
|
|
191
|
+
"""Devuelve el flujo de opciones para esta configuraci贸n."""
|
|
192
|
+
return MeteocatOptionsFlowHandler(config_entry)
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
|
|
12
|
+
# C贸digos de sensores de la API
|
|
13
|
+
WIND_SPEED = "30" # Velocidad del viento
|
|
14
|
+
WIND_DIRECTION = "31" # Direcci贸n del viento
|
|
15
|
+
TEMPERATURE = "32" # Temperatura
|
|
16
|
+
HUMIDITY = "33" # Humedad relativa
|
|
17
|
+
PRESSURE = "34" # Presi贸n atmosf茅rica
|
|
18
|
+
PRECIPITATION = "35" # Precipitaci贸n
|
|
19
|
+
UV_INDEX = "39" # UV
|
|
20
|
+
MAX_TEMPERATURE = "40" # Temperatura m谩xima
|
|
21
|
+
MIN_TEMPERATURE = "42" # Temperatura m铆nima
|
|
22
|
+
WIND_GUST = "50" # Racha de viento
|
|
23
|
+
|
|
24
|
+
# Unidades de medida de los sensores
|
|
25
|
+
WIND_SPEED_UNIT = "m/s"
|
|
26
|
+
WIND_DIRECTION_UNIT = "掳"
|
|
27
|
+
TEMPERATURE_UNIT = "掳C"
|
|
28
|
+
HUMIDITY_UNIT = "%"
|
|
29
|
+
PRESSURE_UNIT = "hPa"
|
|
30
|
+
PRECIPITATION_UNIT = "mm"
|
|
31
|
+
UV_INDEX_UNIT = "UV"
|
|
32
|
+
|
|
33
|
+
# Mapeo de c贸digos 'estatCel' a condiciones de Home Assistant
|
|
34
|
+
CONDITION_MAPPING = {
|
|
35
|
+
"sunny": [1],
|
|
36
|
+
"clear-night": [1],
|
|
37
|
+
"partlycloudy": [2, 3],
|
|
38
|
+
"cloudy": [4, 20, 21, 22],
|
|
39
|
+
"rainy": [5, 6, 23],
|
|
40
|
+
"pouring": [7, 8, 25],
|
|
41
|
+
"lightning-rainy": [8, 24],
|
|
42
|
+
"hail": [9],
|
|
43
|
+
"snowy": [10, 26, 27, 28],
|
|
44
|
+
"fog": [11, 12],
|
|
45
|
+
"snow-rainy": [27, 29, 30],
|
|
46
|
+
}
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
from typing import Dict
|
|
6
|
+
|
|
7
|
+
from homeassistant.core import HomeAssistant
|
|
8
|
+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
|
9
|
+
from homeassistant.exceptions import ConfigEntryNotReady
|
|
10
|
+
|
|
11
|
+
from meteocatpy.data import MeteocatStationData
|
|
12
|
+
from meteocatpy.forecast import MeteocatForecast
|
|
13
|
+
from meteocatpy.exceptions import (
|
|
14
|
+
BadRequestError,
|
|
15
|
+
ForbiddenError,
|
|
16
|
+
TooManyRequestsError,
|
|
17
|
+
InternalServerError,
|
|
18
|
+
UnknownAPIError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
from .const import DOMAIN
|
|
22
|
+
|
|
23
|
+
_LOGGER = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
# Valores predeterminados para los intervalos de actualizaci贸n
|
|
26
|
+
DEFAULT_SENSOR_UPDATE_INTERVAL = timedelta(minutes=90)
|
|
27
|
+
DEFAULT_ENTITY_UPDATE_INTERVAL = timedelta(hours=12)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class MeteocatSensorCoordinator(DataUpdateCoordinator):
|
|
31
|
+
"""Coordinator para manejar la actualizaci贸n de datos de los sensores."""
|
|
32
|
+
|
|
33
|
+
def __init__(
|
|
34
|
+
self,
|
|
35
|
+
hass: HomeAssistant,
|
|
36
|
+
api_key: str,
|
|
37
|
+
town_name: str,
|
|
38
|
+
town_id: str,
|
|
39
|
+
station_name: str,
|
|
40
|
+
station_id: str,
|
|
41
|
+
variable_name: str,
|
|
42
|
+
variable_id: str,
|
|
43
|
+
update_interval: timedelta = DEFAULT_SENSOR_UPDATE_INTERVAL,
|
|
44
|
+
):
|
|
45
|
+
"""Inicializa el coordinador de sensores de Meteocat."""
|
|
46
|
+
self.api_key = api_key
|
|
47
|
+
self.town_name = town_name
|
|
48
|
+
self.town_id = town_id
|
|
49
|
+
self.station_name = station_name
|
|
50
|
+
self.station_id = station_id
|
|
51
|
+
self.variable_name = variable_name
|
|
52
|
+
self.variable_id = variable_id
|
|
53
|
+
self.meteocat_station_data = MeteocatStationData(api_key)
|
|
54
|
+
super().__init__(
|
|
55
|
+
hass,
|
|
56
|
+
_LOGGER,
|
|
57
|
+
name=f"{DOMAIN} Sensor Coordinator",
|
|
58
|
+
update_interval=update_interval,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
async def _async_update_data(self) -> Dict:
|
|
62
|
+
"""
|
|
63
|
+
Actualiza los datos de los sensores desde la API de Meteocat.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
dict: Datos actualizados de los sensores.
|
|
67
|
+
"""
|
|
68
|
+
try:
|
|
69
|
+
data = await self.meteocat_station_data.get_station_data_with_variables(self.station_id)
|
|
70
|
+
_LOGGER.debug("Datos de sensores actualizados exitosamente: %s", data)
|
|
71
|
+
return data
|
|
72
|
+
except ForbiddenError as err:
|
|
73
|
+
_LOGGER.error(
|
|
74
|
+
"Acceso denegado al obtener datos de sensores (Station ID: %s): %s",
|
|
75
|
+
self.station_id,
|
|
76
|
+
err,
|
|
77
|
+
)
|
|
78
|
+
raise ConfigEntryNotReady from err
|
|
79
|
+
except TooManyRequestsError as err:
|
|
80
|
+
_LOGGER.warning(
|
|
81
|
+
"L铆mite de solicitudes alcanzado al obtener datos de sensores (Station ID: %s): %s",
|
|
82
|
+
self.station_id,
|
|
83
|
+
err,
|
|
84
|
+
)
|
|
85
|
+
raise ConfigEntryNotReady from err
|
|
86
|
+
except (BadRequestError, InternalServerError, UnknownAPIError) as err:
|
|
87
|
+
_LOGGER.error(
|
|
88
|
+
"Error al obtener datos de sensores (Station ID: %s): %s",
|
|
89
|
+
self.station_id,
|
|
90
|
+
err,
|
|
91
|
+
)
|
|
92
|
+
raise
|
|
93
|
+
except Exception as err:
|
|
94
|
+
_LOGGER.exception(
|
|
95
|
+
"Error inesperado al obtener datos de sensores (Station ID: %s): %s",
|
|
96
|
+
self.station_id,
|
|
97
|
+
err,
|
|
98
|
+
)
|
|
99
|
+
raise
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class MeteocatEntityCoordinator(DataUpdateCoordinator):
|
|
103
|
+
"""Coordinator para manejar la actualizaci贸n de datos de las entidades de predicci贸n."""
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
hass: HomeAssistant,
|
|
108
|
+
api_key: str,
|
|
109
|
+
town_name: str,
|
|
110
|
+
town_id: str,
|
|
111
|
+
station_name: str,
|
|
112
|
+
station_id: str,
|
|
113
|
+
variable_name: str,
|
|
114
|
+
variable_id: str,
|
|
115
|
+
update_interval: timedelta = DEFAULT_ENTITY_UPDATE_INTERVAL,
|
|
116
|
+
):
|
|
117
|
+
"""Inicializa el coordinador de datos para entidades de predicci贸n."""
|
|
118
|
+
self.api_key = api_key
|
|
119
|
+
self.town_name = town_name
|
|
120
|
+
self.town_id = town_id
|
|
121
|
+
self.station_name = station_name
|
|
122
|
+
self.station_id = station_id
|
|
123
|
+
self.variable_name = variable_name
|
|
124
|
+
self.variable_id = variable_id
|
|
125
|
+
self.meteocat_forecast = MeteocatForecast(api_key)
|
|
126
|
+
super().__init__(
|
|
127
|
+
hass,
|
|
128
|
+
_LOGGER,
|
|
129
|
+
name=f"{DOMAIN} Entity Coordinator",
|
|
130
|
+
update_interval=update_interval,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
async def _async_update_data(self) -> Dict:
|
|
134
|
+
"""
|
|
135
|
+
Actualiza los datos de las entidades de predicci贸n desde la API de Meteocat.
|
|
136
|
+
|
|
137
|
+
Returns:
|
|
138
|
+
dict: Datos actualizados de predicci贸n horaria y diaria.
|
|
139
|
+
"""
|
|
140
|
+
try:
|
|
141
|
+
hourly_forecast = await self.meteocat_forecast.get_prediccion_horaria(self.town_id)
|
|
142
|
+
daily_forecast = await self.meteocat_forecast.get_prediccion_diaria(self.town_id)
|
|
143
|
+
_LOGGER.debug(
|
|
144
|
+
"Datos de predicci贸n actualizados exitosamente (Town ID: %s)", self.town_id
|
|
145
|
+
)
|
|
146
|
+
return {
|
|
147
|
+
"hourly_forecast": hourly_forecast,
|
|
148
|
+
"daily_forecast": daily_forecast,
|
|
149
|
+
}
|
|
150
|
+
except ForbiddenError as err:
|
|
151
|
+
_LOGGER.error(
|
|
152
|
+
"Acceso denegado al obtener datos de predicci贸n (Town ID: %s): %s",
|
|
153
|
+
self.town_id,
|
|
154
|
+
err,
|
|
155
|
+
)
|
|
156
|
+
raise ConfigEntryNotReady from err
|
|
157
|
+
except TooManyRequestsError as err:
|
|
158
|
+
_LOGGER.warning(
|
|
159
|
+
"L铆mite de solicitudes alcanzado al obtener datos de predicci贸n (Town ID: %s): %s",
|
|
160
|
+
self.town_id,
|
|
161
|
+
err,
|
|
162
|
+
)
|
|
163
|
+
raise ConfigEntryNotReady from err
|
|
164
|
+
except (BadRequestError, InternalServerError, UnknownAPIError) as err:
|
|
165
|
+
_LOGGER.error(
|
|
166
|
+
"Error al obtener datos de predicci贸n (Town ID: %s): %s",
|
|
167
|
+
self.town_id,
|
|
168
|
+
err,
|
|
169
|
+
)
|
|
170
|
+
raise
|
|
171
|
+
except Exception as err:
|
|
172
|
+
_LOGGER.exception(
|
|
173
|
+
"Error inesperado al obtener datos de predicci贸n (Town ID: %s): %s",
|
|
174
|
+
self.town_id,
|
|
175
|
+
err,
|
|
176
|
+
)
|
|
177
|
+
raise
|