meteocat 0.1.22 → 0.1.25

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 +401 -372
  7. package/README.md +40 -40
  8. package/custom_components/meteocat/__init__.py +107 -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 -52
  12. package/custom_components/meteocat/coordinator.py +195 -171
  13. package/custom_components/meteocat/entity.py +98 -91
  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 +303 -185
  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,12 +1,12 @@
1
- {
2
- "domain": "meteocat",
3
- "name": "Meteocat",
4
- "codeowners": ["@figorr"],
5
- "config_flow": true,
6
- "dependencies": ["persistent_notification", "http"],
7
- "iot_class": "cloud_polling",
8
- "documentation": "https://gitlab.com/figorr/meteocat",
9
- "loggers": ["meteocatpy"],
10
- "requirements": ["meteocatpy==0.0.7", "packaging>=20.3", "wrapt>=1.14.0"],
11
- "version": "0.1.22"
12
- }
1
+ {
2
+ "domain": "meteocat",
3
+ "name": "Meteocat",
4
+ "codeowners": ["@figorr"],
5
+ "config_flow": true,
6
+ "dependencies": ["persistent_notification", "http"],
7
+ "iot_class": "cloud_polling",
8
+ "documentation": "https://gitlab.com/figorr/meteocat",
9
+ "loggers": ["meteocatpy"],
10
+ "requirements": ["meteocatpy==0.0.8", "packaging>=20.3", "wrapt>=1.14.0"],
11
+ "version": "0.1.25"
12
+ }
@@ -1,71 +1,71 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- from homeassistant.config_entries import ConfigEntry, OptionsFlow
5
- from homeassistant.exceptions import HomeAssistantError
6
- from homeassistant.helpers import config_validation as cv
7
- import voluptuous as vol
8
-
9
- from .const import (
10
- CONF_API_KEY
11
- )
12
- from meteocatpy.town import MeteocatTown
13
- from meteocatpy.exceptions import (
14
- BadRequestError,
15
- ForbiddenError,
16
- TooManyRequestsError,
17
- InternalServerError,
18
- UnknownAPIError,
19
- )
20
-
21
- _LOGGER = logging.getLogger(__name__)
22
-
23
- class MeteocatOptionsFlowHandler(OptionsFlow):
24
- """Manejo del flujo de opciones para Meteocat."""
25
-
26
- def __init__(self, config_entry: ConfigEntry):
27
- """Inicializa el flujo de opciones."""
28
- self.config_entry = config_entry
29
- self.api_key: str | None = None
30
-
31
- async def async_step_init(self, user_input: dict | None = None):
32
- """Paso inicial del flujo de opciones."""
33
- return await self.async_step_update_api_key()
34
-
35
- async def async_step_update_api_key(self, user_input: dict | None = None):
36
- """Permite al usuario actualizar la API Key."""
37
- errors = {}
38
-
39
- if user_input is not None:
40
- self.api_key = user_input[CONF_API_KEY]
41
-
42
- # Validar la nueva API Key utilizando MeteocatTown
43
- town_client = MeteocatTown(self.api_key)
44
-
45
- try:
46
- await town_client.get_municipis() # Verificar que la API Key sea válida
47
- except (
48
- BadRequestError,
49
- ForbiddenError,
50
- TooManyRequestsError,
51
- InternalServerError,
52
- UnknownAPIError,
53
- ) as ex:
54
- _LOGGER.error("Error al validar la nueva API Key: %s", ex)
55
- errors["base"] = "cannot_connect"
56
- except Exception as ex:
57
- _LOGGER.error("Error inesperado al validar la nueva API Key: %s", ex)
58
- errors["base"] = "unknown"
59
-
60
- if not errors:
61
- # Actualizar la configuración de la entrada con la nueva API Key
62
- self.hass.config_entries.async_update_entry(
63
- self.config_entry,
64
- data={**self.config_entry.data, CONF_API_KEY: self.api_key},
65
- )
66
- return self.async_create_entry(title="", data={})
67
-
68
- schema = vol.Schema({vol.Required(CONF_API_KEY): str})
69
- return self.async_show_form(
70
- step_id="update_api_key", data_schema=schema, errors=errors
71
- )
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ from homeassistant.config_entries import ConfigEntry, OptionsFlow
5
+ from homeassistant.exceptions import HomeAssistantError
6
+ from homeassistant.helpers import config_validation as cv
7
+ import voluptuous as vol
8
+
9
+ from .const import (
10
+ CONF_API_KEY
11
+ )
12
+ from meteocatpy.town import MeteocatTown
13
+ from meteocatpy.exceptions import (
14
+ BadRequestError,
15
+ ForbiddenError,
16
+ TooManyRequestsError,
17
+ InternalServerError,
18
+ UnknownAPIError,
19
+ )
20
+
21
+ _LOGGER = logging.getLogger(__name__)
22
+
23
+ class MeteocatOptionsFlowHandler(OptionsFlow):
24
+ """Manejo del flujo de opciones para Meteocat."""
25
+
26
+ def __init__(self, config_entry: ConfigEntry):
27
+ """Inicializa el flujo de opciones."""
28
+ self.config_entry = config_entry
29
+ self.api_key: str | None = None
30
+
31
+ async def async_step_init(self, user_input: dict | None = None):
32
+ """Paso inicial del flujo de opciones."""
33
+ return await self.async_step_update_api_key()
34
+
35
+ async def async_step_update_api_key(self, user_input: dict | None = None):
36
+ """Permite al usuario actualizar la API Key."""
37
+ errors = {}
38
+
39
+ if user_input is not None:
40
+ self.api_key = user_input[CONF_API_KEY]
41
+
42
+ # Validar la nueva API Key utilizando MeteocatTown
43
+ town_client = MeteocatTown(self.api_key)
44
+
45
+ try:
46
+ await town_client.get_municipis() # Verificar que la API Key sea válida
47
+ except (
48
+ BadRequestError,
49
+ ForbiddenError,
50
+ TooManyRequestsError,
51
+ InternalServerError,
52
+ UnknownAPIError,
53
+ ) as ex:
54
+ _LOGGER.error("Error al validar la nueva API Key: %s", ex)
55
+ errors["base"] = "cannot_connect"
56
+ except Exception as ex:
57
+ _LOGGER.error("Error inesperado al validar la nueva API Key: %s", ex)
58
+ errors["base"] = "unknown"
59
+
60
+ if not errors:
61
+ # Actualizar la configuración de la entrada con la nueva API Key
62
+ self.hass.config_entries.async_update_entry(
63
+ self.config_entry,
64
+ data={**self.config_entry.data, CONF_API_KEY: self.api_key},
65
+ )
66
+ return self.async_create_entry(title="", data={})
67
+
68
+ schema = vol.Schema({vol.Required(CONF_API_KEY): str})
69
+ return self.async_show_form(
70
+ step_id="update_api_key", data_schema=schema, errors=errors
71
+ )
@@ -1,185 +1,303 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass
4
- from homeassistant.helpers.entity import DeviceInfo
5
- from homeassistant.components.sensor import (
6
- SensorDeviceClass,
7
- SensorEntity,
8
- SensorEntityDescription,
9
- SensorStateClass,
10
- )
11
- from homeassistant.core import callback
12
- from homeassistant.helpers.entity_platform import AddEntitiesCallback
13
- from homeassistant.helpers.update_coordinator import CoordinatorEntity
14
- from homeassistant.const import PERCENTAGE, UnitOfTemperature
15
-
16
- from .const import (
17
- DOMAIN,
18
- WIND_SPEED,
19
- WIND_DIRECTION,
20
- TEMPERATURE,
21
- HUMIDITY,
22
- PRESSURE,
23
- PRECIPITATION,
24
- UV_INDEX,
25
- MAX_TEMPERATURE,
26
- MIN_TEMPERATURE,
27
- WIND_GUST,
28
- WIND_SPEED_UNIT,
29
- PRESSURE_UNIT,
30
- PRECIPITATION_UNIT,
31
- UV_INDEX_UNIT,
32
- WIND_DIRECTION_UNIT,
33
- )
34
-
35
- from .coordinator import MeteocatSensorCoordinator
36
-
37
-
38
- @dataclass
39
- class MeteocatSensorEntityDescription(SensorEntityDescription):
40
- """A class that describes Meteocat sensor entities."""
41
-
42
-
43
- SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
44
- MeteocatSensorEntityDescription(
45
- key=WIND_SPEED,
46
- name="Wind Speed",
47
- icon="mdi:weather-windy",
48
- device_class=SensorDeviceClass.WIND_SPEED,
49
- state_class=SensorStateClass.MEASUREMENT,
50
- native_unit_of_measurement=WIND_SPEED_UNIT,
51
- ),
52
- MeteocatSensorEntityDescription(
53
- key=WIND_DIRECTION,
54
- name="Wind Direction",
55
- icon="mdi:compass",
56
- device_class=None,
57
- native_unit_of_measurement=WIND_DIRECTION_UNIT,
58
- ),
59
- MeteocatSensorEntityDescription(
60
- key=TEMPERATURE,
61
- name="Temperature",
62
- icon="mdi:thermometer",
63
- device_class=SensorDeviceClass.TEMPERATURE,
64
- state_class=SensorStateClass.MEASUREMENT,
65
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
66
- ),
67
- MeteocatSensorEntityDescription(
68
- key=HUMIDITY,
69
- name="Humidity",
70
- icon="mdi:water-percent",
71
- device_class=SensorDeviceClass.HUMIDITY,
72
- state_class=SensorStateClass.MEASUREMENT,
73
- native_unit_of_measurement=PERCENTAGE,
74
- ),
75
- MeteocatSensorEntityDescription(
76
- key=PRESSURE,
77
- name="Pressure",
78
- icon="mdi:gauge",
79
- device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
80
- state_class=SensorStateClass.MEASUREMENT,
81
- native_unit_of_measurement=PRESSURE_UNIT,
82
- ),
83
- MeteocatSensorEntityDescription(
84
- key=PRECIPITATION,
85
- name="Precipitation",
86
- icon="mdi:weather-rainy",
87
- device_class=None,
88
- state_class=SensorStateClass.MEASUREMENT,
89
- native_unit_of_measurement=PRECIPITATION_UNIT,
90
- ),
91
- MeteocatSensorEntityDescription(
92
- key=UV_INDEX,
93
- name="UV Index",
94
- icon="mdi:weather-sunny",
95
- state_class=SensorStateClass.MEASUREMENT,
96
- native_unit_of_measurement=UV_INDEX_UNIT,
97
- ),
98
- MeteocatSensorEntityDescription(
99
- key=MAX_TEMPERATURE,
100
- name="Max Temperature",
101
- icon="mdi:thermometer-plus",
102
- device_class=SensorDeviceClass.TEMPERATURE,
103
- state_class=SensorStateClass.MEASUREMENT,
104
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
105
- ),
106
- MeteocatSensorEntityDescription(
107
- key=MIN_TEMPERATURE,
108
- name="Min Temperature",
109
- icon="mdi:thermometer-minus",
110
- device_class=SensorDeviceClass.TEMPERATURE,
111
- state_class=SensorStateClass.MEASUREMENT,
112
- native_unit_of_measurement=UnitOfTemperature.CELSIUS,
113
- ),
114
- MeteocatSensorEntityDescription(
115
- key=WIND_GUST,
116
- name="Wind Gust",
117
- icon="mdi:weather-windy",
118
- device_class=SensorDeviceClass.WIND_SPEED,
119
- state_class=SensorStateClass.MEASUREMENT,
120
- native_unit_of_measurement=WIND_SPEED_UNIT,
121
- ),
122
- )
123
-
124
-
125
- @callback
126
- async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback) -> None:
127
- """Set up Meteocat sensors from a config entry."""
128
- entry_data = hass.data[DOMAIN][entry.entry_id]
129
- coordinator = entry_data["sensor_coordinator"]
130
-
131
- async_add_entities(
132
- MeteocatSensor(coordinator, description, entry_data)
133
- for description in SENSOR_TYPES
134
- )
135
-
136
-
137
- class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity):
138
- """Representation of a Meteocat sensor."""
139
-
140
- def __init__(self, coordinator, description, entry_data):
141
- """Initialize the sensor."""
142
- super().__init__(coordinator)
143
- self.entity_description = description
144
- self.api_key = entry_data["api_key"] # Usamos la API key de la configuración
145
- self._town_name = entry_data["town_name"] # Usamos el nombre del municipio
146
- self._town_id = entry_data["town_id"] # Usamos el ID del municipio
147
- self._variable_name = entry_data["variable_name"] # Usamos el nombre de la variable
148
- self._variable_id = entry_data["variable_id"] # Usamos el ID de la variable
149
- self._station_name = entry_data["station_name"] # Usamos el nombre de la estación
150
- self._station_id = entry_data["station_id"] # Usamos el ID de la estación
151
-
152
- # Unique ID for the entity
153
- self._attr_unique_id = f"{self._town_id}_{self.entity_description.key}"
154
-
155
- @property
156
- def native_value(self):
157
- """Return the state of the sensor."""
158
- value = getattr(self.coordinator.data, self.entity_description.key, None)
159
-
160
- if self.entity_description.key == WIND_DIRECTION:
161
- return self._convert_degrees_to_cardinal(value)
162
-
163
- return value
164
-
165
- @staticmethod
166
- def _convert_degrees_to_cardinal(degree: float | None) -> str | None:
167
- """Convert degrees to cardinal direction."""
168
- if degree is None:
169
- return None
170
- directions = [
171
- "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
172
- "S", "SSW", "SW", "WSW", "W", "WNW", "NW", "NNW", "N",
173
- ]
174
- index = int(((degree + 11.25) / 22.5)) % 16
175
- return directions[index]
176
-
177
- @property
178
- def device_info(self) -> DeviceInfo:
179
- """Return the device info."""
180
- return DeviceInfo(
181
- identifiers={(DOMAIN, self._town_id)},
182
- name=self._town_name,
183
- manufacturer="Meteocat",
184
- model="Meteocat API"
185
- )
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from datetime import datetime
5
+ from homeassistant.helpers.entity import DeviceInfo
6
+ from homeassistant.components.sensor import (
7
+ SensorDeviceClass,
8
+ SensorEntity,
9
+ SensorEntityDescription,
10
+ SensorStateClass,
11
+ )
12
+ from homeassistant.core import callback
13
+ from homeassistant.helpers.entity_platform import AddEntitiesCallback
14
+ from homeassistant.helpers.update_coordinator import CoordinatorEntity
15
+ from homeassistant.const import (
16
+ DEGREE,
17
+ PERCENTAGE,
18
+ UnitOfPressure,
19
+ UnitOfSpeed,
20
+ UnitOfTemperature,
21
+ UnitOfVolumetricFlux,
22
+ UnitOfIrradiance,
23
+ )
24
+
25
+ from .const import (
26
+ DOMAIN,
27
+ TOWN_NAME,
28
+ TOWN_ID,
29
+ STATION_NAME,
30
+ STATION_ID,
31
+ WIND_SPEED,
32
+ WIND_DIRECTION,
33
+ TEMPERATURE,
34
+ HUMIDITY,
35
+ PRESSURE,
36
+ PRECIPITATION,
37
+ SOLAR_GLOBAL_IRRADIANCE,
38
+ UV_INDEX,
39
+ MAX_TEMPERATURE,
40
+ MIN_TEMPERATURE,
41
+ WIND_GUST,
42
+ STATION_TIMESTAMP,
43
+ WIND_SPEED_CODE,
44
+ WIND_DIRECTION_CODE,
45
+ TEMPERATURE_CODE,
46
+ HUMIDITY_CODE,
47
+ PRESSURE_CODE,
48
+ PRECIPITATION_CODE,
49
+ SOLAR_GLOBAL_IRRADIANCE_CODE,
50
+ UV_INDEX_CODE,
51
+ MAX_TEMPERATURE_CODE,
52
+ MIN_TEMPERATURE_CODE,
53
+ WIND_GUST_CODE,
54
+ )
55
+
56
+ from .coordinator import MeteocatSensorCoordinator
57
+
58
+ @dataclass
59
+ class MeteocatSensorEntityDescription(SensorEntityDescription):
60
+ """A class that describes Meteocat sensor entities."""
61
+
62
+ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
63
+ # Sensores dinámicos
64
+ MeteocatSensorEntityDescription(
65
+ key=WIND_SPEED,
66
+ name="Wind Speed",
67
+ icon="mdi:weather-windy",
68
+ device_class=SensorDeviceClass.WIND_SPEED,
69
+ state_class=SensorStateClass.MEASUREMENT,
70
+ native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
71
+ ),
72
+ MeteocatSensorEntityDescription(
73
+ key=WIND_DIRECTION,
74
+ name="Wind Direction",
75
+ icon="mdi:compass",
76
+ device_class=None,
77
+ ),
78
+ MeteocatSensorEntityDescription(
79
+ key=TEMPERATURE,
80
+ name="Temperature",
81
+ icon="mdi:thermometer",
82
+ device_class=SensorDeviceClass.TEMPERATURE,
83
+ state_class=SensorStateClass.MEASUREMENT,
84
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
85
+ ),
86
+ MeteocatSensorEntityDescription(
87
+ key=HUMIDITY,
88
+ name="Humidity",
89
+ icon="mdi:water-percent",
90
+ device_class=SensorDeviceClass.HUMIDITY,
91
+ state_class=SensorStateClass.MEASUREMENT,
92
+ native_unit_of_measurement=PERCENTAGE,
93
+ ),
94
+ MeteocatSensorEntityDescription(
95
+ key=PRESSURE,
96
+ name="Pressure",
97
+ icon="mdi:gauge",
98
+ device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
99
+ state_class=SensorStateClass.MEASUREMENT,
100
+ native_unit_of_measurement=UnitOfPressure.HPA,
101
+ ),
102
+ MeteocatSensorEntityDescription(
103
+ key=PRECIPITATION,
104
+ name="Precipitation",
105
+ icon="mdi:weather-rainy",
106
+ device_class=None,
107
+ state_class=SensorStateClass.MEASUREMENT,
108
+ native_unit_of_measurement=UnitOfVolumetricFlux.MILLIMETERS_PER_HOUR,
109
+ ),
110
+ MeteocatSensorEntityDescription(
111
+ key=SOLAR_GLOBAL_IRRADIANCE,
112
+ name="Solar Global Irradiance",
113
+ icon="mdi:weather-sunny",
114
+ device_class=SensorDeviceClass.IRRADIANCE,
115
+ state_class=SensorStateClass.MEASUREMENT,
116
+ native_unit_of_measurement = UnitOfIrradiance.WATTS_PER_SQUARE_METER,
117
+ ),
118
+ MeteocatSensorEntityDescription(
119
+ key=UV_INDEX,
120
+ name="UV Index",
121
+ icon="mdi:weather-sunny",
122
+ ),
123
+ MeteocatSensorEntityDescription(
124
+ key=MAX_TEMPERATURE,
125
+ name="Max Temperature",
126
+ icon="mdi:thermometer-plus",
127
+ device_class=SensorDeviceClass.TEMPERATURE,
128
+ state_class=SensorStateClass.MEASUREMENT,
129
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
130
+ ),
131
+ MeteocatSensorEntityDescription(
132
+ key=MIN_TEMPERATURE,
133
+ name="Min Temperature",
134
+ icon="mdi:thermometer-minus",
135
+ device_class=SensorDeviceClass.TEMPERATURE,
136
+ state_class=SensorStateClass.MEASUREMENT,
137
+ native_unit_of_measurement=UnitOfTemperature.CELSIUS,
138
+ ),
139
+ MeteocatSensorEntityDescription(
140
+ key=WIND_GUST,
141
+ name="Wind Gust",
142
+ icon="mdi:weather-windy",
143
+ device_class=SensorDeviceClass.WIND_SPEED,
144
+ state_class=SensorStateClass.MEASUREMENT,
145
+ native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
146
+ ),
147
+ # Sensores estáticos
148
+ MeteocatSensorEntityDescription(
149
+ key=TOWN_NAME,
150
+ name="Town Name",
151
+ icon="mdi:home-city",
152
+ ),
153
+ MeteocatSensorEntityDescription(
154
+ key=TOWN_ID,
155
+ name="Town ID",
156
+ icon="mdi:identifier",
157
+ ),
158
+ MeteocatSensorEntityDescription(
159
+ key=STATION_NAME,
160
+ name="Station Name",
161
+ icon="mdi:broadcast",
162
+ ),
163
+ MeteocatSensorEntityDescription(
164
+ key=STATION_ID,
165
+ name="Station ID",
166
+ icon="mdi:identifier",
167
+ ),
168
+ MeteocatSensorEntityDescription(
169
+ key=STATION_TIMESTAMP,
170
+ name="Station Timestamp",
171
+ icon="mdi:calendar-clock",
172
+ device_class=SensorDeviceClass.TIMESTAMP,
173
+ )
174
+ )
175
+
176
+
177
+ @callback
178
+ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback) -> None:
179
+ """Set up Meteocat sensors from a config entry."""
180
+ entry_data = hass.data[DOMAIN][entry.entry_id]
181
+ coordinator = entry_data["sensor_coordinator"]
182
+
183
+ async_add_entities(
184
+ MeteocatSensor(coordinator, description, entry_data)
185
+ for description in SENSOR_TYPES
186
+ )
187
+
188
+ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity):
189
+ """Representation of a Meteocat sensor."""
190
+ STATIC_KEYS = {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID}
191
+
192
+ CODE_MAPPING = {
193
+ WIND_SPEED: WIND_SPEED_CODE,
194
+ WIND_DIRECTION: WIND_DIRECTION_CODE,
195
+ TEMPERATURE: TEMPERATURE_CODE,
196
+ HUMIDITY: HUMIDITY_CODE,
197
+ PRESSURE: PRESSURE_CODE,
198
+ PRECIPITATION: PRECIPITATION_CODE,
199
+ SOLAR_GLOBAL_IRRADIANCE: SOLAR_GLOBAL_IRRADIANCE_CODE,
200
+ UV_INDEX: UV_INDEX_CODE,
201
+ MAX_TEMPERATURE: MAX_TEMPERATURE_CODE,
202
+ MIN_TEMPERATURE: MIN_TEMPERATURE_CODE,
203
+ WIND_GUST: WIND_GUST_CODE,
204
+ }
205
+
206
+ def __init__(self, coordinator, description, entry_data):
207
+ """Initialize the sensor."""
208
+ super().__init__(coordinator)
209
+ self.entity_description = description
210
+ self.api_key = entry_data["api_key"]
211
+ self._town_name = entry_data["town_name"]
212
+ self._town_id = entry_data["town_id"]
213
+ self._station_name = entry_data["station_name"]
214
+ self._station_id = entry_data["station_id"]
215
+
216
+ # Unique ID for the entity
217
+ self._attr_unique_id = f"{self._town_id}_{self.entity_description.key}"
218
+
219
+ @property
220
+ def native_value(self):
221
+ """Return the state of the sensor."""
222
+ # Información estática
223
+ if self.entity_description.key in self.STATIC_KEYS:
224
+ # Información estática del `entry_data`
225
+ if self.entity_description.key == TOWN_NAME:
226
+ return self._town_name
227
+ if self.entity_description.key == TOWN_ID:
228
+ return self._town_id
229
+ if self.entity_description.key == STATION_NAME:
230
+ return self._station_name
231
+ if self.entity_description.key == STATION_ID:
232
+ return self._station_id
233
+ # Información dinámica
234
+ sensor_code = self.CODE_MAPPING.get(self.entity_description.key)
235
+
236
+ if sensor_code is not None:
237
+ # Accedemos a las estaciones en el JSON recibido
238
+ stations = self.coordinator.data or []
239
+ for station in stations:
240
+ variables = station.get("variables", [])
241
+
242
+ # Filtramos por código
243
+ variable_data = next(
244
+ (var for var in variables if var.get("codi") == sensor_code),
245
+ None,
246
+ )
247
+
248
+ if variable_data:
249
+ # Obtenemos la última lectura
250
+ lectures = variable_data.get("lectures", [])
251
+ if lectures:
252
+ latest_reading = lectures[-1]
253
+ value = latest_reading.get("valor")
254
+
255
+ # Convertimos grados a dirección cardinal para WIND_DIRECTION
256
+ if self.entity_description.key == WIND_DIRECTION and isinstance(value, (int, float)):
257
+ return self._convert_degrees_to_cardinal(value)
258
+
259
+ return value
260
+ # Lógica específica para el sensor de timestamp
261
+ if self.entity_description.key == "station_timestamp":
262
+ stations = self.coordinator.data or []
263
+ for station in stations:
264
+ variables = station.get("variables", [])
265
+ for variable in variables:
266
+ lectures = variable.get("lectures", [])
267
+ if lectures:
268
+ # Obtenemos el campo `data` de la última lectura
269
+ latest_reading = lectures[-1]
270
+ raw_timestamp = latest_reading.get("data")
271
+
272
+ if raw_timestamp:
273
+ # Convertir el timestamp a un objeto datetime
274
+ try:
275
+ return datetime.fromisoformat(raw_timestamp.replace("Z", "+00:00"))
276
+ except ValueError:
277
+ # Manejo de errores si el formato no es válido
278
+ return None
279
+
280
+ return None
281
+
282
+ @staticmethod
283
+ def _convert_degrees_to_cardinal(degree: float) -> str:
284
+ """Convert degrees to cardinal direction."""
285
+ if not isinstance(degree, (int, float)):
286
+ return "Unknown" # Retorna "Unknown" si el valor no es un número válido
287
+
288
+ directions = [
289
+ "N", "NNE", "NE", "ENE", "E", "ESE", "SE", "SSE",
290
+ "S", "SSO", "SO", "OSO", "O", "ONO", "NO", "NNO", "N",
291
+ ]
292
+ index = round(degree / 22.5) % 16
293
+ return directions[index]
294
+
295
+ @property
296
+ def device_info(self) -> DeviceInfo:
297
+ """Return the device info."""
298
+ return DeviceInfo(
299
+ identifiers={(DOMAIN, self._town_id)},
300
+ name=self._town_name,
301
+ manufacturer="Meteocat",
302
+ model="Meteocat API",
303
+ )