meteocat 0.1.38 → 0.1.40
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/CHANGELOG.md +28 -0
- package/conftest.py +11 -0
- package/custom_components/meteocat/__init__.py +31 -5
- package/custom_components/meteocat/condition.py +15 -5
- package/custom_components/meteocat/const.py +9 -4
- package/custom_components/meteocat/coordinator.py +593 -127
- package/custom_components/meteocat/helpers.py +39 -40
- package/custom_components/meteocat/manifest.json +1 -1
- package/custom_components/meteocat/sensor.py +136 -19
- package/custom_components/meteocat/strings.json +20 -0
- package/custom_components/meteocat/translations/ca.json +20 -0
- package/custom_components/meteocat/translations/en.json +20 -0
- package/custom_components/meteocat/translations/es.json +20 -0
- package/custom_components/meteocat/version.py +1 -1
- package/custom_components/meteocat/weather.py +214 -0
- package/filetree.txt +2 -1
- package/package.json +1 -1
- package/pyproject.toml +1 -1
- package/custom_components/meteocat/entity.py +0 -98
|
@@ -1,42 +1,41 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
from
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
"""
|
|
13
|
-
|
|
14
|
-
if
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
"""
|
|
33
|
-
#
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return current_time_only < sunrise_time_only or current_time_only > sunset_time_only
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime, timedelta
|
|
5
|
+
from homeassistant.core import HomeAssistant
|
|
6
|
+
from homeassistant.util.dt import as_local, as_utc
|
|
7
|
+
from homeassistant.helpers.sun import get_astral_event_next
|
|
8
|
+
|
|
9
|
+
_LOGGER = logging.getLogger(__name__)
|
|
10
|
+
|
|
11
|
+
def get_sun_times(hass: HomeAssistant, current_time: datetime | None = None):
|
|
12
|
+
"""Get the sunrise and sunset times."""
|
|
13
|
+
# Usa la hora actual si no se proporciona una hora específica
|
|
14
|
+
if current_time is None:
|
|
15
|
+
current_time = datetime.now()
|
|
16
|
+
|
|
17
|
+
# Asegúrate de que current_time es aware (UTC con offset)
|
|
18
|
+
current_time = as_utc(current_time)
|
|
19
|
+
|
|
20
|
+
# Obtén los tiempos de amanecer y atardecer desde el helper
|
|
21
|
+
sunrise = get_astral_event_next(hass, "sunrise", utc_point_in_time=current_time)
|
|
22
|
+
sunset = get_astral_event_next(hass, "sunset", utc_point_in_time=current_time)
|
|
23
|
+
|
|
24
|
+
# Asegúrate de que no sean None y conviértelos a la zona horaria local
|
|
25
|
+
if sunrise and sunset:
|
|
26
|
+
return as_local(sunrise), as_local(sunset)
|
|
27
|
+
|
|
28
|
+
# Lanza un error si no se pudieron determinar los eventos
|
|
29
|
+
raise ValueError("Sunrise or sunset data is unavailable.")
|
|
30
|
+
|
|
31
|
+
def is_night(current_time: datetime, hass: HomeAssistant) -> bool:
|
|
32
|
+
"""Determine if it is currently night based on sunrise and sunset times."""
|
|
33
|
+
# Convierte current_time a UTC si no tiene información de zona horaria
|
|
34
|
+
if current_time.tzinfo is None:
|
|
35
|
+
current_time = as_utc(current_time)
|
|
36
|
+
|
|
37
|
+
# Obtén los tiempos de amanecer y atardecer
|
|
38
|
+
sunrise, sunset = get_sun_times(hass, current_time)
|
|
39
|
+
|
|
40
|
+
# Compara las horas
|
|
41
|
+
return current_time < sunrise or current_time > sunset
|
|
@@ -45,6 +45,7 @@ from .const import (
|
|
|
45
45
|
MIN_TEMPERATURE,
|
|
46
46
|
WIND_GUST,
|
|
47
47
|
STATION_TIMESTAMP,
|
|
48
|
+
CONDITION,
|
|
48
49
|
WIND_SPEED_CODE,
|
|
49
50
|
WIND_DIRECTION_CODE,
|
|
50
51
|
TEMPERATURE_CODE,
|
|
@@ -61,7 +62,9 @@ from .const import (
|
|
|
61
62
|
|
|
62
63
|
from .coordinator import (
|
|
63
64
|
MeteocatSensorCoordinator,
|
|
65
|
+
MeteocatStaticSensorCoordinator,
|
|
64
66
|
MeteocatUviFileCoordinator,
|
|
67
|
+
MeteocatConditionCoordinator,
|
|
65
68
|
)
|
|
66
69
|
|
|
67
70
|
_LOGGER = logging.getLogger(__name__)
|
|
@@ -137,7 +140,7 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
|
|
|
137
140
|
MeteocatSensorEntityDescription(
|
|
138
141
|
key=UV_INDEX,
|
|
139
142
|
translation_key="uv_index",
|
|
140
|
-
icon="mdi:weather-sunny",
|
|
143
|
+
icon="mdi:weather-sunny-alert",
|
|
141
144
|
),
|
|
142
145
|
MeteocatSensorEntityDescription(
|
|
143
146
|
key=MAX_TEMPERATURE,
|
|
@@ -197,10 +200,15 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
|
|
|
197
200
|
entity_category=EntityCategory.DIAGNOSTIC,
|
|
198
201
|
),
|
|
199
202
|
MeteocatSensorEntityDescription(
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
203
|
+
key=STATION_TIMESTAMP,
|
|
204
|
+
translation_key="station_timestamp",
|
|
205
|
+
icon="mdi:calendar-clock",
|
|
206
|
+
device_class=SensorDeviceClass.TIMESTAMP,
|
|
207
|
+
),
|
|
208
|
+
MeteocatSensorEntityDescription(
|
|
209
|
+
key=CONDITION,
|
|
210
|
+
translation_key="condition",
|
|
211
|
+
icon="mdi:weather-partly-cloudy",
|
|
204
212
|
)
|
|
205
213
|
)
|
|
206
214
|
|
|
@@ -212,12 +220,21 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
|
|
|
212
220
|
# Coordinadores para sensores
|
|
213
221
|
coordinator = entry_data.get("sensor_coordinator")
|
|
214
222
|
uvi_file_coordinator = entry_data.get("uvi_file_coordinator")
|
|
223
|
+
static_sensor_coordinator = entry_data.get("static_sensor_coordinator")
|
|
224
|
+
condition_coordinator = entry_data.get("condition_coordinator")
|
|
215
225
|
|
|
216
226
|
# Sensores generales
|
|
217
227
|
async_add_entities(
|
|
218
228
|
MeteocatSensor(coordinator, description, entry_data)
|
|
219
229
|
for description in SENSOR_TYPES
|
|
220
|
-
if description.key
|
|
230
|
+
if description.key not in {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID, UV_INDEX, CONDITION} # Excluir estáticos y UVI
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
# Sensores estáticos
|
|
234
|
+
async_add_entities(
|
|
235
|
+
MeteocatStaticSensor(static_sensor_coordinator, description, entry_data)
|
|
236
|
+
for description in SENSOR_TYPES
|
|
237
|
+
if description.key in {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID}
|
|
221
238
|
)
|
|
222
239
|
|
|
223
240
|
# Sensor UVI
|
|
@@ -227,6 +244,66 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
|
|
|
227
244
|
if description.key == UV_INDEX # Incluir UVI en el coordinador UVI FILE COORDINATOR
|
|
228
245
|
)
|
|
229
246
|
|
|
247
|
+
# Sensor CONDITION para estado del cielo
|
|
248
|
+
async_add_entities(
|
|
249
|
+
MeteocatConditionSensor(condition_coordinator, description, entry_data)
|
|
250
|
+
for description in SENSOR_TYPES
|
|
251
|
+
if description.key == CONDITION # Incluir CONDITION en el coordinador CONDITION COORDINATOR
|
|
252
|
+
)
|
|
253
|
+
|
|
254
|
+
class MeteocatStaticSensor(CoordinatorEntity[MeteocatStaticSensorCoordinator], SensorEntity):
|
|
255
|
+
"""Representation of a static Meteocat sensor."""
|
|
256
|
+
STATIC_KEYS = {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID}
|
|
257
|
+
|
|
258
|
+
_attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
|
|
259
|
+
|
|
260
|
+
def __init__(self, static_sensor_coordinator, description, entry_data):
|
|
261
|
+
"""Initialize the static sensor."""
|
|
262
|
+
super().__init__(static_sensor_coordinator)
|
|
263
|
+
self.entity_description = description
|
|
264
|
+
self._town_name = entry_data["town_name"]
|
|
265
|
+
self._town_id = entry_data["town_id"]
|
|
266
|
+
self._station_name = entry_data["station_name"]
|
|
267
|
+
self._station_id = entry_data["station_id"]
|
|
268
|
+
|
|
269
|
+
# Unique ID for the entity
|
|
270
|
+
self._attr_unique_id = f"sensor.{DOMAIN}_{self._station_id}_{self.entity_description.key}"
|
|
271
|
+
|
|
272
|
+
# Assign entity_category if defined in the description
|
|
273
|
+
self._attr_entity_category = getattr(description, "entity_category", None)
|
|
274
|
+
|
|
275
|
+
# Log para depuración
|
|
276
|
+
_LOGGER.debug(
|
|
277
|
+
"Inicializando sensor: %s, Unique ID: %s",
|
|
278
|
+
self.entity_description.name,
|
|
279
|
+
self._attr_unique_id,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
@property
|
|
283
|
+
def native_value(self):
|
|
284
|
+
"""Return the state of the sensor."""
|
|
285
|
+
# Información estática
|
|
286
|
+
if self.entity_description.key in self.STATIC_KEYS:
|
|
287
|
+
# Información estática del `entry_data`
|
|
288
|
+
if self.entity_description.key == TOWN_NAME:
|
|
289
|
+
return self._town_name
|
|
290
|
+
if self.entity_description.key == TOWN_ID:
|
|
291
|
+
return self._town_id
|
|
292
|
+
if self.entity_description.key == STATION_NAME:
|
|
293
|
+
return self._station_name
|
|
294
|
+
if self.entity_description.key == STATION_ID:
|
|
295
|
+
return self._station_id
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def device_info(self) -> DeviceInfo:
|
|
299
|
+
"""Return the device info."""
|
|
300
|
+
return DeviceInfo(
|
|
301
|
+
identifiers={(DOMAIN, self._town_id)},
|
|
302
|
+
name="Meteocat " + self._station_id + " " + self._town_name,
|
|
303
|
+
manufacturer="Meteocat",
|
|
304
|
+
model="Meteocat API",
|
|
305
|
+
)
|
|
306
|
+
|
|
230
307
|
class MeteocatUviSensor(CoordinatorEntity[MeteocatUviFileCoordinator], SensorEntity):
|
|
231
308
|
"""Representation of a Meteocat UV Index sensor."""
|
|
232
309
|
|
|
@@ -280,9 +357,61 @@ class MeteocatUviSensor(CoordinatorEntity[MeteocatUviFileCoordinator], SensorEnt
|
|
|
280
357
|
model="Meteocat API",
|
|
281
358
|
)
|
|
282
359
|
|
|
360
|
+
class MeteocatConditionSensor(CoordinatorEntity[MeteocatConditionCoordinator], SensorEntity):
|
|
361
|
+
"""Representation of a Meteocat UV Index sensor."""
|
|
362
|
+
|
|
363
|
+
_attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
|
|
364
|
+
|
|
365
|
+
def __init__(self, condition_coordinator, description, entry_data):
|
|
366
|
+
"""Initialize the UV Index sensor."""
|
|
367
|
+
super().__init__(condition_coordinator)
|
|
368
|
+
self.entity_description = description
|
|
369
|
+
self._town_name = entry_data["town_name"]
|
|
370
|
+
self._town_id = entry_data["town_id"]
|
|
371
|
+
self._station_id = entry_data["station_id"]
|
|
372
|
+
|
|
373
|
+
# Unique ID for the entity
|
|
374
|
+
self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
|
|
375
|
+
|
|
376
|
+
# Asigna entity_category desde description (si está definido)
|
|
377
|
+
self._attr_entity_category = getattr(description, "entity_category", None)
|
|
378
|
+
|
|
379
|
+
# Log para depuración
|
|
380
|
+
_LOGGER.debug(
|
|
381
|
+
"Inicializando sensor: %s, Unique ID: %s",
|
|
382
|
+
self.entity_description.name,
|
|
383
|
+
self._attr_unique_id,
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
@property
|
|
387
|
+
def native_value(self):
|
|
388
|
+
"""Return the current UV index value."""
|
|
389
|
+
if self.entity_description.key == CONDITION:
|
|
390
|
+
condition_data = self.coordinator.data or {}
|
|
391
|
+
return condition_data.get("condition", None)
|
|
392
|
+
|
|
393
|
+
@property
|
|
394
|
+
def extra_state_attributes(self):
|
|
395
|
+
"""Return additional attributes for the sensor."""
|
|
396
|
+
attributes = super().extra_state_attributes or {}
|
|
397
|
+
if self.entity_description.key == CONDITION:
|
|
398
|
+
condition_data = self.coordinator.data or {}
|
|
399
|
+
# Add the "hour" attribute if it exists
|
|
400
|
+
attributes["hour"] = condition_data.get("hour", None)
|
|
401
|
+
return attributes
|
|
402
|
+
|
|
403
|
+
@property
|
|
404
|
+
def device_info(self) -> DeviceInfo:
|
|
405
|
+
"""Return the device info."""
|
|
406
|
+
return DeviceInfo(
|
|
407
|
+
identifiers={(DOMAIN, self._town_id)},
|
|
408
|
+
name="Meteocat " + self._station_id + " " + self._town_name,
|
|
409
|
+
manufacturer="Meteocat",
|
|
410
|
+
model="Meteocat API",
|
|
411
|
+
)
|
|
412
|
+
|
|
283
413
|
class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity):
|
|
284
414
|
"""Representation of a Meteocat sensor."""
|
|
285
|
-
STATIC_KEYS = {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID}
|
|
286
415
|
|
|
287
416
|
CODE_MAPPING = {
|
|
288
417
|
WIND_SPEED: WIND_SPEED_CODE,
|
|
@@ -326,19 +455,7 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
|
|
|
326
455
|
@property
|
|
327
456
|
def native_value(self):
|
|
328
457
|
"""Return the state of the sensor."""
|
|
329
|
-
# Información estática
|
|
330
|
-
if self.entity_description.key in self.STATIC_KEYS:
|
|
331
|
-
# Información estática del `entry_data`
|
|
332
|
-
if self.entity_description.key == TOWN_NAME:
|
|
333
|
-
return self._town_name
|
|
334
|
-
if self.entity_description.key == TOWN_ID:
|
|
335
|
-
return self._town_id
|
|
336
|
-
if self.entity_description.key == STATION_NAME:
|
|
337
|
-
return self._station_name
|
|
338
|
-
if self.entity_description.key == STATION_ID:
|
|
339
|
-
return self._station_id
|
|
340
458
|
# Información dinámica
|
|
341
|
-
|
|
342
459
|
if self.entity_description.key == FEELS_LIKE:
|
|
343
460
|
stations = self.coordinator.data or []
|
|
344
461
|
|
|
@@ -107,6 +107,26 @@
|
|
|
107
107
|
},
|
|
108
108
|
"station_timestamp": {
|
|
109
109
|
"name": "Station Timestamp"
|
|
110
|
+
},
|
|
111
|
+
"condition": {
|
|
112
|
+
"name": "Condition",
|
|
113
|
+
"state": {
|
|
114
|
+
"sunny": "Sunny",
|
|
115
|
+
"clear-night": "Clear Night",
|
|
116
|
+
"partlycloudy": "Partly Cloudy",
|
|
117
|
+
"cloudy": "Cloudy",
|
|
118
|
+
"rainy": "Rainy",
|
|
119
|
+
"pouring": "Pouring",
|
|
120
|
+
"lightning-rainy": "Lightning Rainy",
|
|
121
|
+
"hail": "Hail",
|
|
122
|
+
"snowy": "Snowy",
|
|
123
|
+
"snow-rainy": "Snow Rainy"
|
|
124
|
+
},
|
|
125
|
+
"state_attributes": {
|
|
126
|
+
"hour": {
|
|
127
|
+
"name": "Hour"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
110
130
|
}
|
|
111
131
|
}
|
|
112
132
|
}
|
|
@@ -107,6 +107,26 @@
|
|
|
107
107
|
},
|
|
108
108
|
"station_timestamp": {
|
|
109
109
|
"name": "Estació Timestamp"
|
|
110
|
+
},
|
|
111
|
+
"condition": {
|
|
112
|
+
"name": "Estat del Cel",
|
|
113
|
+
"state": {
|
|
114
|
+
"sunny": "Assolellat",
|
|
115
|
+
"clear-night": "Nit Clara",
|
|
116
|
+
"partlycloudy": "Parcialment ennuvolat",
|
|
117
|
+
"cloudy": "Ennuvolat",
|
|
118
|
+
"rainy": "Plovent",
|
|
119
|
+
"pouring": "Tormenta",
|
|
120
|
+
"lightning-rainy": "Tormenta amb Llamps",
|
|
121
|
+
"hail": "Calamarsa",
|
|
122
|
+
"snowy": "Nevant",
|
|
123
|
+
"snow-rainy": "Neu amb Pluja"
|
|
124
|
+
},
|
|
125
|
+
"state_attributes": {
|
|
126
|
+
"hour": {
|
|
127
|
+
"name": "Hora"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
110
130
|
}
|
|
111
131
|
}
|
|
112
132
|
}
|
|
@@ -107,6 +107,26 @@
|
|
|
107
107
|
},
|
|
108
108
|
"station_timestamp": {
|
|
109
109
|
"name": "Station Timestamp"
|
|
110
|
+
},
|
|
111
|
+
"condition": {
|
|
112
|
+
"name": "Condition",
|
|
113
|
+
"state": {
|
|
114
|
+
"sunny": "Sunny",
|
|
115
|
+
"clear-night": "Clear Night",
|
|
116
|
+
"partlycloudy": "Partly Cloudy",
|
|
117
|
+
"cloudy": "Cloudy",
|
|
118
|
+
"rainy": "Rainy",
|
|
119
|
+
"pouring": "Pouring",
|
|
120
|
+
"lightning-rainy": "Lightning Rainy",
|
|
121
|
+
"hail": "Hail",
|
|
122
|
+
"snowy": "Snowy",
|
|
123
|
+
"snow-rainy": "Snow Rainy"
|
|
124
|
+
},
|
|
125
|
+
"state_attributes": {
|
|
126
|
+
"hour": {
|
|
127
|
+
"name": "Hour"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
110
130
|
}
|
|
111
131
|
}
|
|
112
132
|
}
|
|
@@ -107,6 +107,26 @@
|
|
|
107
107
|
},
|
|
108
108
|
"station_timestamp": {
|
|
109
109
|
"name": "Estación Timestamp"
|
|
110
|
+
},
|
|
111
|
+
"condition": {
|
|
112
|
+
"name": "Estado del cielo",
|
|
113
|
+
"state": {
|
|
114
|
+
"sunny": "Soleado",
|
|
115
|
+
"clear-night": "Noche Despejada",
|
|
116
|
+
"partlycloudy": "Parcialmente Nublado",
|
|
117
|
+
"cloudy": "Nublado",
|
|
118
|
+
"rainy": "Lluvioso",
|
|
119
|
+
"pouring": "Tormenta",
|
|
120
|
+
"lightning-rainy": "Tormenta con Rayos",
|
|
121
|
+
"hail": "Granizo",
|
|
122
|
+
"snowy": "Nevando",
|
|
123
|
+
"snow-rainy": "Nieve con Lluvia"
|
|
124
|
+
},
|
|
125
|
+
"state_attributes": {
|
|
126
|
+
"hour": {
|
|
127
|
+
"name": "Hora"
|
|
128
|
+
}
|
|
129
|
+
}
|
|
110
130
|
}
|
|
111
131
|
}
|
|
112
132
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
# version.py
|
|
2
|
-
__version__ = "0.1.
|
|
2
|
+
__version__ = "0.1.40"
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
|
7
|
+
from homeassistant.components.weather import (
|
|
8
|
+
WeatherEntity,
|
|
9
|
+
WeatherEntityFeature,
|
|
10
|
+
Forecast,
|
|
11
|
+
)
|
|
12
|
+
from homeassistant.core import callback
|
|
13
|
+
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
|
14
|
+
from homeassistant.helpers.device_registry import DeviceInfo
|
|
15
|
+
from homeassistant.const import (
|
|
16
|
+
DEGREE,
|
|
17
|
+
PERCENTAGE,
|
|
18
|
+
UnitOfPrecipitationDepth,
|
|
19
|
+
UnitOfPressure,
|
|
20
|
+
UnitOfSpeed,
|
|
21
|
+
UnitOfTemperature,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
from .const import (
|
|
25
|
+
DOMAIN,
|
|
26
|
+
ATTRIBUTION,
|
|
27
|
+
WIND_SPEED_CODE,
|
|
28
|
+
WIND_DIRECTION_CODE,
|
|
29
|
+
TEMPERATURE_CODE,
|
|
30
|
+
HUMIDITY_CODE,
|
|
31
|
+
PRESSURE_CODE,
|
|
32
|
+
PRECIPITATION_CODE,
|
|
33
|
+
WIND_GUST_CODE,
|
|
34
|
+
)
|
|
35
|
+
from .coordinator import (
|
|
36
|
+
HourlyForecastCoordinator,
|
|
37
|
+
DailyForecastCoordinator,
|
|
38
|
+
MeteocatSensorCoordinator,
|
|
39
|
+
MeteocatUviFileCoordinator,
|
|
40
|
+
MeteocatConditionCoordinator,
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
_LOGGER = logging.getLogger(__name__)
|
|
44
|
+
|
|
45
|
+
@callback
|
|
46
|
+
async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback) -> None:
|
|
47
|
+
"""Set up Meteocat weather entity from a config entry."""
|
|
48
|
+
entry_data = hass.data[DOMAIN][entry.entry_id]
|
|
49
|
+
|
|
50
|
+
hourly_forecast_coordinator = entry_data.get("hourly_forecast_coordinator")
|
|
51
|
+
daily_forecast_coordinator = entry_data.get("daily_forecast_coordinator")
|
|
52
|
+
sensor_coordinator = entry_data.get("sensor_coordinator")
|
|
53
|
+
uvi_file_coordinator = entry_data.get("uvi_file_coordinator")
|
|
54
|
+
condition_coordinator = entry_data.get("condition_coordinator")
|
|
55
|
+
|
|
56
|
+
async_add_entities([
|
|
57
|
+
MeteocatWeatherEntity(
|
|
58
|
+
hourly_forecast_coordinator,
|
|
59
|
+
daily_forecast_coordinator,
|
|
60
|
+
sensor_coordinator,
|
|
61
|
+
uvi_file_coordinator,
|
|
62
|
+
condition_coordinator,
|
|
63
|
+
entry_data
|
|
64
|
+
)
|
|
65
|
+
])
|
|
66
|
+
|
|
67
|
+
class MeteocatWeatherEntity(CoordinatorEntity, WeatherEntity):
|
|
68
|
+
"""Representation of a Meteocat Weather Entity."""
|
|
69
|
+
|
|
70
|
+
_attr_attribution = ATTRIBUTION
|
|
71
|
+
_attr_has_entity_name = True
|
|
72
|
+
_attr_native_precipitation_unit = UnitOfPrecipitationDepth.MILLIMETERS
|
|
73
|
+
_attr_native_precipitation_probability_unit = PERCENTAGE
|
|
74
|
+
_attr_native_pressure_unit = UnitOfPressure.HPA
|
|
75
|
+
_attr_native_temperature_unit = UnitOfTemperature.CELSIUS
|
|
76
|
+
_attr_native_wind_speed_unit = UnitOfSpeed.KILOMETERS_PER_HOUR
|
|
77
|
+
_attr_native_wind_bearing_unit = DEGREE
|
|
78
|
+
_attr_supported_features = (
|
|
79
|
+
WeatherEntityFeature.FORECAST_HOURLY | WeatherEntityFeature.FORECAST_DAILY
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
def __init__(
|
|
83
|
+
self,
|
|
84
|
+
hourly_forecast_coordinator: HourlyForecastCoordinator,
|
|
85
|
+
daily_forecast_coordinator: DailyForecastCoordinator,
|
|
86
|
+
sensor_coordinator: MeteocatSensorCoordinator,
|
|
87
|
+
uvi_file_coordinator: MeteocatUviFileCoordinator,
|
|
88
|
+
condition_coordinator: MeteocatConditionCoordinator,
|
|
89
|
+
entry_data: dict,
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Initialize the weather entity."""
|
|
92
|
+
super().__init__(daily_forecast_coordinator)
|
|
93
|
+
self._hourly_forecast_coordinator = hourly_forecast_coordinator
|
|
94
|
+
self._daily_forecast_coordinator = daily_forecast_coordinator
|
|
95
|
+
self._sensor_coordinator = sensor_coordinator
|
|
96
|
+
self._uvi_file_coordinator = uvi_file_coordinator
|
|
97
|
+
self._condition_coordinator = condition_coordinator
|
|
98
|
+
self._town_name = entry_data["town_name"]
|
|
99
|
+
self._town_id = entry_data["town_id"]
|
|
100
|
+
self._station_id = entry_data["station_id"]
|
|
101
|
+
|
|
102
|
+
@property
|
|
103
|
+
def name(self) -> str:
|
|
104
|
+
"""Return the name of the entity."""
|
|
105
|
+
return f"Weather {self._town_name}"
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def unique_id(self) -> str:
|
|
109
|
+
"""Return the unique ID of the entity."""
|
|
110
|
+
return f"weather.{DOMAIN}_{self._station_id}"
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def condition(self) -> Optional[str]:
|
|
114
|
+
"""Return the current weather condition."""
|
|
115
|
+
condition_data = self._condition_coordinator.data or {}
|
|
116
|
+
return condition_data.get("condition")
|
|
117
|
+
|
|
118
|
+
def _get_latest_sensor_value(self, code: str) -> Optional[float]:
|
|
119
|
+
"""Helper method to retrieve the latest sensor value."""
|
|
120
|
+
sensor_code = code
|
|
121
|
+
if not sensor_code:
|
|
122
|
+
return None
|
|
123
|
+
|
|
124
|
+
stations = self._sensor_coordinator.data or []
|
|
125
|
+
for station in stations:
|
|
126
|
+
variables = station.get("variables", [])
|
|
127
|
+
variable_data = next(
|
|
128
|
+
(var for var in variables if var.get("codi") == sensor_code),
|
|
129
|
+
None,
|
|
130
|
+
)
|
|
131
|
+
if variable_data:
|
|
132
|
+
lectures = variable_data.get("lectures", [])
|
|
133
|
+
if lectures:
|
|
134
|
+
return lectures[-1].get("valor")
|
|
135
|
+
return None
|
|
136
|
+
|
|
137
|
+
@property
|
|
138
|
+
def native_temperature(self) -> Optional[float]:
|
|
139
|
+
return self._get_latest_sensor_value(TEMPERATURE_CODE)
|
|
140
|
+
|
|
141
|
+
@property
|
|
142
|
+
def humidity(self) -> Optional[float]:
|
|
143
|
+
return self._get_latest_sensor_value(HUMIDITY_CODE)
|
|
144
|
+
|
|
145
|
+
@property
|
|
146
|
+
def native_pressure(self) -> Optional[float]:
|
|
147
|
+
return self._get_latest_sensor_value(PRESSURE_CODE)
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def native_wind_speed(self) -> Optional[float]:
|
|
151
|
+
return self._get_latest_sensor_value(WIND_SPEED_CODE)
|
|
152
|
+
|
|
153
|
+
@property
|
|
154
|
+
def native_wind_gust_speed(self) -> Optional[float]:
|
|
155
|
+
return self._get_latest_sensor_value(WIND_GUST_CODE)
|
|
156
|
+
|
|
157
|
+
@property
|
|
158
|
+
def uv_index(self) -> Optional[float]:
|
|
159
|
+
"""Return the UV index."""
|
|
160
|
+
uvi_data = self._uvi_file_coordinator.data or {}
|
|
161
|
+
return uvi_data.get("uvi")
|
|
162
|
+
|
|
163
|
+
async def async_forecast_daily(self) -> list[Forecast] | None:
|
|
164
|
+
"""Return the daily forecast."""
|
|
165
|
+
await self._daily_forecast_coordinator.async_request_refresh()
|
|
166
|
+
daily_forecasts = self._daily_forecast_coordinator.get_all_daily_forecasts()
|
|
167
|
+
if not daily_forecasts:
|
|
168
|
+
return None
|
|
169
|
+
|
|
170
|
+
return [
|
|
171
|
+
Forecast(
|
|
172
|
+
datetime=forecast["date"],
|
|
173
|
+
temperature=forecast["temperature_max"],
|
|
174
|
+
templow=forecast["temperature_min"],
|
|
175
|
+
precipitation_probability=forecast["precipitation"],
|
|
176
|
+
condition=forecast["condition"],
|
|
177
|
+
)
|
|
178
|
+
for forecast in daily_forecasts
|
|
179
|
+
]
|
|
180
|
+
|
|
181
|
+
async def async_forecast_hourly(self) -> list[Forecast] | None:
|
|
182
|
+
"""Return the hourly forecast."""
|
|
183
|
+
await self._hourly_forecast_coordinator.async_request_refresh()
|
|
184
|
+
hourly_forecasts = self._hourly_forecast_coordinator.get_all_hourly_forecasts()
|
|
185
|
+
if not hourly_forecasts:
|
|
186
|
+
return None
|
|
187
|
+
|
|
188
|
+
return [
|
|
189
|
+
Forecast(
|
|
190
|
+
datetime=forecast["datetime"],
|
|
191
|
+
temperature=forecast["temperature"],
|
|
192
|
+
precipitation=forecast["precipitation"],
|
|
193
|
+
condition=forecast["condition"],
|
|
194
|
+
wind_speed=forecast["wind_speed"],
|
|
195
|
+
wind_bearing=forecast["wind_bearing"],
|
|
196
|
+
humidity=forecast["humidity"],
|
|
197
|
+
)
|
|
198
|
+
for forecast in hourly_forecasts
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
async def async_update(self) -> None:
|
|
202
|
+
"""Update the weather entity."""
|
|
203
|
+
await self._hourly_forecast_coordinator.async_request_refresh()
|
|
204
|
+
await self._daily_forecast_coordinator.async_request_refresh()
|
|
205
|
+
|
|
206
|
+
@property
|
|
207
|
+
def device_info(self) -> DeviceInfo:
|
|
208
|
+
"""Return the device info."""
|
|
209
|
+
return DeviceInfo(
|
|
210
|
+
identifiers={(DOMAIN, self._town_id)},
|
|
211
|
+
name=f"Meteocat {self._station_id}",
|
|
212
|
+
manufacturer="Meteocat",
|
|
213
|
+
model="Meteocat API",
|
|
214
|
+
)
|
package/filetree.txt
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
├── CHANGELOG.md
|
|
11
11
|
├── LICENSE
|
|
12
12
|
├── README.md
|
|
13
|
+
├── conftest.py
|
|
13
14
|
└── custom_components/
|
|
14
15
|
└── meteocat/
|
|
15
16
|
├── __init__.py
|
|
@@ -17,7 +18,6 @@
|
|
|
17
18
|
├── config_flow.py
|
|
18
19
|
├── const.py
|
|
19
20
|
├── coordinator.py
|
|
20
|
-
├── entity.py
|
|
21
21
|
├── helpers.py
|
|
22
22
|
├── manifest.json
|
|
23
23
|
├── options_flow.py
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
├── en.json
|
|
29
29
|
├── es.json
|
|
30
30
|
├── version.py
|
|
31
|
+
├── weather.py
|
|
31
32
|
├── filetree.py
|
|
32
33
|
├── filetree.txt
|
|
33
34
|
├── hacs.json
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "meteocat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.40",
|
|
4
4
|
"description": "[](https://opensource.org/licenses/Apache-2.0)\r [](https://pypi.org/project/meteocat)\r [](https://gitlab.com/figorr/meteocat/commits/master)",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"directories": {
|