meteocat 0.1.25 → 0.1.26

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.
Files changed (35) hide show
  1. package/.github/workflows/release.yml +33 -33
  2. package/.pre-commit-config.yaml +37 -37
  3. package/.releaserc +23 -23
  4. package/.releaserc.toml +14 -14
  5. package/AUTHORS.md +12 -12
  6. package/CHANGELOG.md +414 -401
  7. package/README.md +40 -40
  8. package/custom_components/meteocat/__init__.py +136 -107
  9. package/custom_components/meteocat/condition.py +28 -28
  10. package/custom_components/meteocat/config_flow.py +192 -192
  11. package/custom_components/meteocat/const.py +55 -55
  12. package/custom_components/meteocat/coordinator.py +194 -195
  13. package/custom_components/meteocat/entity.py +98 -98
  14. package/custom_components/meteocat/helpers.py +42 -42
  15. package/custom_components/meteocat/manifest.json +12 -12
  16. package/custom_components/meteocat/options_flow.py +71 -71
  17. package/custom_components/meteocat/sensor.py +325 -303
  18. package/custom_components/meteocat/strings.json +25 -25
  19. package/custom_components/meteocat/translations/ca.json +25 -25
  20. package/custom_components/meteocat/translations/en.json +25 -25
  21. package/custom_components/meteocat/translations/es.json +25 -25
  22. package/custom_components/meteocat/version.py +2 -2
  23. package/filetree.py +48 -48
  24. package/filetree.txt +46 -46
  25. package/hacs.json +5 -5
  26. package/package.json +22 -22
  27. package/poetry.lock +3216 -3216
  28. package/pyproject.toml +64 -64
  29. package/releaserc.json +17 -17
  30. package/requirements.test.txt +3 -3
  31. package/setup.cfg +64 -64
  32. package/setup.py +10 -10
  33. package/tests/bandit.yaml +17 -17
  34. package/tests/conftest.py +19 -19
  35. package/tests/test_init.py +9 -9
@@ -1,192 +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)
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)
@@ -1,55 +1,55 @@
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 = "wind_speed" # Velocidad del viento
14
- WIND_DIRECTION = "wind_direction" # Dirección del viento
15
- TEMPERATURE = "temperature" # Temperatura
16
- HUMIDITY = "humidity" # Humedad relativa
17
- PRESSURE = "pressure" # Presión atmosférica
18
- PRECIPITATION = "precipitation" # Precipitación
19
- SOLAR_GLOBAL_IRRADIANCE = "solar_global_irradiance" # Irradiación solar global
20
- UV_INDEX = "uv_index" # UV
21
- MAX_TEMPERATURE = "max_temperature" # Temperatura máxima
22
- MIN_TEMPERATURE = "min_temperature" # Temperatura mínima
23
- WIND_GUST = "wind_gust" # Racha de viento
24
- STATION_TIMESTAMP = "station_timestamp" # Código de tiempo de la estación
25
-
26
- # Definición de códigos para variables
27
- WIND_SPEED_CODE = 30
28
- WIND_DIRECTION_CODE = 31
29
- TEMPERATURE_CODE = 32
30
- HUMIDITY_CODE = 33
31
- PRESSURE_CODE = 34
32
- PRECIPITATION_CODE = 35
33
- SOLAR_GLOBAL_IRRADIANCE_CODE = 36
34
- UV_INDEX_CODE = 39
35
- MAX_TEMPERATURE_CODE = 40
36
- MIN_TEMPERATURE_CODE = 42
37
- WIND_GUST_CODE = 50
38
-
39
- # Mapeo de códigos 'estatCel' a condiciones de Home Assistant
40
- CONDITION_MAPPING = {
41
- "sunny": [1],
42
- "clear-night": [1],
43
- "partlycloudy": [2, 3],
44
- "cloudy": [4, 20, 21, 22],
45
- "rainy": [5, 6, 23],
46
- "pouring": [7, 8, 25],
47
- "lightning-rainy": [8, 24],
48
- "hail": [9],
49
- "snowy": [10, 26, 27, 28],
50
- "fog": [11, 12],
51
- "snow-rainy": [27, 29, 30],
52
- }
53
-
54
- # Platforms
55
- PLATFORMS = ["sensor"]
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 = "wind_speed" # Velocidad del viento
14
+ WIND_DIRECTION = "wind_direction" # Dirección del viento
15
+ TEMPERATURE = "temperature" # Temperatura
16
+ HUMIDITY = "humidity" # Humedad relativa
17
+ PRESSURE = "pressure" # Presión atmosférica
18
+ PRECIPITATION = "precipitation" # Precipitación
19
+ SOLAR_GLOBAL_IRRADIANCE = "solar_global_irradiance" # Irradiación solar global
20
+ UV_INDEX = "uv_index" # UV
21
+ MAX_TEMPERATURE = "max_temperature" # Temperatura máxima
22
+ MIN_TEMPERATURE = "min_temperature" # Temperatura mínima
23
+ WIND_GUST = "wind_gust" # Racha de viento
24
+ STATION_TIMESTAMP = "station_timestamp" # Código de tiempo de la estación
25
+
26
+ # Definición de códigos para variables
27
+ WIND_SPEED_CODE = 30
28
+ WIND_DIRECTION_CODE = 31
29
+ TEMPERATURE_CODE = 32
30
+ HUMIDITY_CODE = 33
31
+ PRESSURE_CODE = 34
32
+ PRECIPITATION_CODE = 35
33
+ SOLAR_GLOBAL_IRRADIANCE_CODE = 36
34
+ UV_INDEX_CODE = 39
35
+ MAX_TEMPERATURE_CODE = 40
36
+ MIN_TEMPERATURE_CODE = 42
37
+ WIND_GUST_CODE = 50
38
+
39
+ # Mapeo de códigos 'estatCel' a condiciones de Home Assistant
40
+ CONDITION_MAPPING = {
41
+ "sunny": [1],
42
+ "clear-night": [1],
43
+ "partlycloudy": [2, 3],
44
+ "cloudy": [4, 20, 21, 22],
45
+ "rainy": [5, 6, 23],
46
+ "pouring": [7, 8, 25],
47
+ "lightning-rainy": [8, 24],
48
+ "hail": [9],
49
+ "snowy": [10, 26, 27, 28],
50
+ "fog": [11, 12],
51
+ "snow-rainy": [27, 29, 30],
52
+ }
53
+
54
+ # Platforms
55
+ PLATFORMS = ["sensor"]