meteocat 2.3.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/.github/ISSUE_TEMPLATE/bug_report.md +8 -2
  2. package/.github/ISSUE_TEMPLATE/config.yml +7 -0
  3. package/.github/ISSUE_TEMPLATE/improvement.md +39 -0
  4. package/.github/ISSUE_TEMPLATE/new_function.md +41 -0
  5. package/.github/labels.yml +63 -0
  6. package/.github/workflows/autocloser.yaml +11 -9
  7. package/.github/workflows/close-on-label.yml +48 -0
  8. package/.github/workflows/force-sync-labels.yml +18 -0
  9. package/.github/workflows/release.yml +1 -31
  10. package/.github/workflows/sync-gitlab.yml +17 -4
  11. package/.github/workflows/sync-labels.yml +21 -0
  12. package/.releaserc +21 -0
  13. package/AUTHORS.md +1 -0
  14. package/CHANGELOG.md +58 -1
  15. package/README.md +81 -6
  16. package/custom_components/meteocat/__init__.py +51 -41
  17. package/custom_components/meteocat/config_flow.py +52 -3
  18. package/custom_components/meteocat/const.py +3 -0
  19. package/custom_components/meteocat/coordinator.py +188 -2
  20. package/custom_components/meteocat/manifest.json +1 -1
  21. package/custom_components/meteocat/options_flow.py +61 -3
  22. package/custom_components/meteocat/sensor.py +305 -254
  23. package/custom_components/meteocat/strings.json +61 -15
  24. package/custom_components/meteocat/translations/ca.json +67 -22
  25. package/custom_components/meteocat/translations/en.json +61 -15
  26. package/custom_components/meteocat/translations/es.json +61 -15
  27. package/custom_components/meteocat/version.py +1 -1
  28. package/custom_components/meteocat/weather.py +1 -1
  29. package/hacs.json +1 -1
  30. package/images/daily_forecast_2_alerts.png +0 -0
  31. package/images/regenerate_assets.png +0 -0
  32. package/images/setup_options.png +0 -0
  33. package/images/system_options.png +0 -0
  34. package/package.json +1 -1
  35. package/pyproject.toml +1 -1
  36. package/scripts/update_version.sh +22 -0
  37. package/.github/workflows/close-duplicates.yml +0 -57
@@ -71,13 +71,13 @@ from .const import (
71
71
  ALERTS,
72
72
  ALERT_FILE_STATUS,
73
73
  ALERT_WIND,
74
- ALERT_RAIN_INTENSITY,
75
- ALERT_RAIN,
76
- ALERT_SEA,
77
- ALERT_COLD,
78
- ALERT_WARM,
79
- ALERT_WARM_NIGHT,
80
- ALERT_SNOW,
74
+ ALERT_RAIN_INTENSITY,
75
+ ALERT_RAIN,
76
+ ALERT_SEA,
77
+ ALERT_COLD,
78
+ ALERT_WARM,
79
+ ALERT_WARM_NIGHT,
80
+ ALERT_SNOW,
81
81
  LIGHTNING_FILE_STATUS,
82
82
  LIGHTNING_REGION,
83
83
  LIGHTNING_TOWN,
@@ -105,6 +105,9 @@ from .const import (
105
105
  DEFAULT_LIGHTNING_VALIDITY_TIME,
106
106
  DEFAULT_LIGHTNING_VALIDITY_HOURS,
107
107
  DEFAULT_LIGHTNING_VALIDITY_MINUTES,
108
+ SUNRISE,
109
+ SUNSET,
110
+ SUN_FILE_STATUS,
108
111
  )
109
112
 
110
113
  from .coordinator import (
@@ -122,6 +125,8 @@ from .coordinator import (
122
125
  MeteocatQuotesFileCoordinator,
123
126
  MeteocatLightningCoordinator,
124
127
  MeteocatLightningFileCoordinator,
128
+ MeteocatSunCoordinator,
129
+ MeteocatSunFileCoordinator,
125
130
  )
126
131
 
127
132
  # Definir la zona horaria local
@@ -208,7 +213,7 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
208
213
  icon="mdi:weather-sunny",
209
214
  device_class=SensorDeviceClass.IRRADIANCE,
210
215
  state_class=SensorStateClass.MEASUREMENT,
211
- native_unit_of_measurement = UnitOfIrradiance.WATTS_PER_SQUARE_METER,
216
+ native_unit_of_measurement=UnitOfIrradiance.WATTS_PER_SQUARE_METER,
212
217
  ),
213
218
  MeteocatSensorEntityDescription(
214
219
  key=UV_INDEX,
@@ -305,7 +310,7 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
305
310
  translation_key="condition",
306
311
  icon="mdi:weather-partly-cloudy",
307
312
  ),
308
- MeteocatSensorEntityDescription(
313
+ MeteocatSensorEntityDescription(
309
314
  key=MAX_TEMPERATURE_FORECAST,
310
315
  translation_key="max_temperature_forecast",
311
316
  icon="mdi:thermometer-plus",
@@ -367,37 +372,37 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
367
372
  translation_key="alert_wind",
368
373
  icon="mdi:alert-outline",
369
374
  ),
370
- MeteocatSensorEntityDescription(
375
+ MeteocatSensorEntityDescription(
371
376
  key=ALERT_RAIN_INTENSITY,
372
377
  translation_key="alert_rain_intensity",
373
378
  icon="mdi:alert-outline",
374
379
  ),
375
- MeteocatSensorEntityDescription(
380
+ MeteocatSensorEntityDescription(
376
381
  key=ALERT_RAIN,
377
382
  translation_key="alert_rain",
378
383
  icon="mdi:alert-outline",
379
384
  ),
380
- MeteocatSensorEntityDescription(
385
+ MeteocatSensorEntityDescription(
381
386
  key=ALERT_SEA,
382
387
  translation_key="alert_sea",
383
388
  icon="mdi:alert-outline",
384
389
  ),
385
- MeteocatSensorEntityDescription(
390
+ MeteocatSensorEntityDescription(
386
391
  key=ALERT_COLD,
387
392
  translation_key="alert_cold",
388
393
  icon="mdi:alert-outline",
389
394
  ),
390
- MeteocatSensorEntityDescription(
395
+ MeteocatSensorEntityDescription(
391
396
  key=ALERT_WARM,
392
397
  translation_key="alert_warm",
393
398
  icon="mdi:alert-outline",
394
399
  ),
395
- MeteocatSensorEntityDescription(
400
+ MeteocatSensorEntityDescription(
396
401
  key=ALERT_WARM_NIGHT,
397
402
  translation_key="alert_warm_night",
398
403
  icon="mdi:alert-outline",
399
404
  ),
400
- MeteocatSensorEntityDescription(
405
+ MeteocatSensorEntityDescription(
401
406
  key=ALERT_SNOW,
402
407
  translation_key="alert_snow",
403
408
  icon="mdi:alert-outline",
@@ -431,7 +436,26 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
431
436
  translation_key="quota_queries",
432
437
  icon="mdi:counter",
433
438
  entity_category=EntityCategory.DIAGNOSTIC,
434
- )
439
+ ),
440
+ # Nuevos sensores de sol
441
+ MeteocatSensorEntityDescription(
442
+ key=SUNRISE,
443
+ translation_key="sunrise",
444
+ icon="mdi:weather-sunset-up",
445
+ device_class=SensorDeviceClass.TIMESTAMP,
446
+ ),
447
+ MeteocatSensorEntityDescription(
448
+ key=SUNSET,
449
+ translation_key="sunset",
450
+ icon="mdi:weather-sunset-down",
451
+ device_class=SensorDeviceClass.TIMESTAMP,
452
+ ),
453
+ MeteocatSensorEntityDescription(
454
+ key=SUN_FILE_STATUS,
455
+ translation_key="sun_file_status",
456
+ icon="mdi:update",
457
+ entity_category=EntityCategory.DIAGNOSTIC,
458
+ ),
435
459
  )
436
460
 
437
461
  @callback
@@ -454,12 +478,29 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
454
478
  quotes_file_coordinator = entry_data.get("quotes_file_coordinator")
455
479
  lightning_coordinator = entry_data.get("lightning_coordinator")
456
480
  lightning_file_coordinator = entry_data.get("lightning_file_coordinator")
481
+ sun_coordinator = entry_data.get("sun_coordinator")
482
+ sun_file_coordinator = entry_data.get("sun_file_coordinator")
457
483
 
458
484
  # Sensores generales
459
485
  async_add_entities(
460
486
  MeteocatSensor(sensor_coordinator, description, entry_data)
461
487
  for description in SENSOR_TYPES
462
- if description.key in {WIND_SPEED, WIND_DIRECTION, WIND_DIRECTION_CARDINAL, TEMPERATURE, HUMIDITY, PRESSURE, PRECIPITATION, PRECIPITATION_ACCUMULATED, SOLAR_GLOBAL_IRRADIANCE, MAX_TEMPERATURE, MIN_TEMPERATURE, FEELS_LIKE, WIND_GUST, STATION_TIMESTAMP} # Incluir sensores generales en el coordinador SENSOR COORDINATOR
488
+ if description.key in {
489
+ WIND_SPEED,
490
+ WIND_DIRECTION,
491
+ WIND_DIRECTION_CARDINAL,
492
+ TEMPERATURE,
493
+ HUMIDITY,
494
+ PRESSURE,
495
+ PRECIPITATION,
496
+ PRECIPITATION_ACCUMULATED,
497
+ SOLAR_GLOBAL_IRRADIANCE,
498
+ MAX_TEMPERATURE,
499
+ MIN_TEMPERATURE,
500
+ FEELS_LIKE,
501
+ WIND_GUST,
502
+ STATION_TIMESTAMP,
503
+ }
463
504
  )
464
505
 
465
506
  # Sensores estáticos
@@ -473,14 +514,14 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
473
514
  async_add_entities(
474
515
  MeteocatUviSensor(uvi_file_coordinator, description, entry_data)
475
516
  for description in SENSOR_TYPES
476
- if description.key == UV_INDEX # Incluir UVI en el coordinador UVI FILE COORDINATOR
517
+ if description.key == UV_INDEX
477
518
  )
478
519
 
479
520
  # Sensor CONDITION para estado del cielo
480
521
  async_add_entities(
481
522
  MeteocatConditionSensor(condition_coordinator, description, entry_data)
482
523
  for description in SENSOR_TYPES
483
- if description.key == CONDITION # Incluir CONDITION en el coordinador CONDITION COORDINATOR
524
+ if description.key == CONDITION
484
525
  )
485
526
 
486
527
  # Sensores temperatura previsión
@@ -536,7 +577,16 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
536
577
  async_add_entities(
537
578
  MeteocatAlertMeteorSensor(alerts_region_coordinator, description, entry_data)
538
579
  for description in SENSOR_TYPES
539
- if description.key in {ALERT_WIND, ALERT_RAIN_INTENSITY, ALERT_RAIN, ALERT_SEA, ALERT_COLD, ALERT_WARM, ALERT_WARM_NIGHT, ALERT_SNOW}
580
+ if description.key in {
581
+ ALERT_WIND,
582
+ ALERT_RAIN_INTENSITY,
583
+ ALERT_RAIN,
584
+ ALERT_SEA,
585
+ ALERT_COLD,
586
+ ALERT_WARM,
587
+ ALERT_WARM_NIGHT,
588
+ ALERT_SNOW,
589
+ }
540
590
  )
541
591
 
542
592
  # Sensores de estado de cuotas
@@ -567,6 +617,21 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
567
617
  if description.key in {LIGHTNING_REGION, LIGHTNING_TOWN}
568
618
  )
569
619
 
620
+ # Sensor de estado de archivo de sol
621
+ async_add_entities(
622
+ MeteocatSunStatusSensor(sun_coordinator, description, entry_data)
623
+ for description in SENSOR_TYPES
624
+ if description.key == SUN_FILE_STATUS
625
+ )
626
+
627
+ # Sensores de sol
628
+ async_add_entities(
629
+ MeteocatSunSensor(sun_file_coordinator, description, entry_data)
630
+ for description in SENSOR_TYPES
631
+ if description.key in {SUNRISE, SUNSET}
632
+ )
633
+
634
+
570
635
  # Cambiar UTC a la zona horaria local
571
636
  def convert_to_local_time(utc_time: str, local_tz: str = "Europe/Madrid") -> datetime | None:
572
637
  """
@@ -594,7 +659,7 @@ class MeteocatStaticSensor(CoordinatorEntity[MeteocatStaticSensorCoordinator], S
594
659
  """Representation of a static Meteocat sensor."""
595
660
  STATIC_KEYS = {TOWN_NAME, TOWN_ID, STATION_NAME, STATION_ID, REGION_NAME, REGION_ID}
596
661
 
597
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
662
+ _attr_has_entity_name = True
598
663
 
599
664
  def __init__(self, static_sensor_coordinator, description, entry_data):
600
665
  """Initialize the static sensor."""
@@ -607,13 +672,9 @@ class MeteocatStaticSensor(CoordinatorEntity[MeteocatStaticSensorCoordinator], S
607
672
  self._region_name = entry_data["region_name"]
608
673
  self._region_id = entry_data["region_id"]
609
674
 
610
- # Unique ID for the entity
611
- self._attr_unique_id = f"sensor.{DOMAIN}_{self._station_id}_{self.entity_description.key}"
612
-
613
- # Assign entity_category if defined in the description
675
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
614
676
  self._attr_entity_category = getattr(description, "entity_category", None)
615
677
 
616
- # Log para depuración
617
678
  _LOGGER.debug(
618
679
  "Inicializando sensor: %s, Unique ID: %s",
619
680
  self.entity_description.name,
@@ -623,9 +684,7 @@ class MeteocatStaticSensor(CoordinatorEntity[MeteocatStaticSensorCoordinator], S
623
684
  @property
624
685
  def native_value(self):
625
686
  """Return the state of the sensor."""
626
- # Información estática
627
687
  if self.entity_description.key in self.STATIC_KEYS:
628
- # Información estática del `entry_data`
629
688
  if self.entity_description.key == TOWN_NAME:
630
689
  return self._town_name
631
690
  if self.entity_description.key == TOWN_ID:
@@ -644,15 +703,14 @@ class MeteocatStaticSensor(CoordinatorEntity[MeteocatStaticSensorCoordinator], S
644
703
  """Return the device info."""
645
704
  return DeviceInfo(
646
705
  identifiers={(DOMAIN, self._town_id)},
647
- name="Meteocat " + self._station_id + " " + self._town_name,
706
+ name=f"Meteocat {self._station_id} {self._town_name}",
648
707
  manufacturer="Meteocat",
649
708
  model="Meteocat API",
650
709
  )
651
710
 
652
711
  class MeteocatUviSensor(CoordinatorEntity[MeteocatUviFileCoordinator], SensorEntity):
653
712
  """Representation of a Meteocat UV Index sensor."""
654
-
655
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
713
+ _attr_has_entity_name = True
656
714
 
657
715
  def __init__(self, uvi_file_coordinator, description, entry_data):
658
716
  """Initialize the UV Index sensor."""
@@ -661,14 +719,9 @@ class MeteocatUviSensor(CoordinatorEntity[MeteocatUviFileCoordinator], SensorEnt
661
719
  self._town_name = entry_data["town_name"]
662
720
  self._town_id = entry_data["town_id"]
663
721
  self._station_id = entry_data["station_id"]
664
-
665
- # Unique ID for the entity
666
722
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
667
-
668
- # Asigna entity_category desde description (si está definido)
669
723
  self._attr_entity_category = getattr(description, "entity_category", None)
670
724
 
671
- # Log para depuración
672
725
  _LOGGER.debug(
673
726
  "Inicializando sensor: %s, Unique ID: %s",
674
727
  self.entity_description.name,
@@ -688,7 +741,6 @@ class MeteocatUviSensor(CoordinatorEntity[MeteocatUviFileCoordinator], SensorEnt
688
741
  attributes = super().extra_state_attributes or {}
689
742
  if self.entity_description.key == UV_INDEX:
690
743
  uvi_data = self.coordinator.data or {}
691
- # Add the "hour" attribute if it exists
692
744
  attributes["hour"] = uvi_data.get("hour")
693
745
  return attributes
694
746
 
@@ -697,31 +749,25 @@ class MeteocatUviSensor(CoordinatorEntity[MeteocatUviFileCoordinator], SensorEnt
697
749
  """Return the device info."""
698
750
  return DeviceInfo(
699
751
  identifiers={(DOMAIN, self._town_id)},
700
- name="Meteocat " + self._station_id + " " + self._town_name,
752
+ name=f"Meteocat {self._station_id} {self._town_name}",
701
753
  manufacturer="Meteocat",
702
754
  model="Meteocat API",
703
755
  )
704
756
 
705
757
  class MeteocatConditionSensor(CoordinatorEntity[MeteocatConditionCoordinator], SensorEntity):
706
- """Representation of a Meteocat UV Index sensor."""
707
-
708
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
758
+ """Representation of a Meteocat Condition sensor."""
759
+ _attr_has_entity_name = True
709
760
 
710
761
  def __init__(self, condition_coordinator, description, entry_data):
711
- """Initialize the UV Index sensor."""
762
+ """Initialize the Condition sensor."""
712
763
  super().__init__(condition_coordinator)
713
764
  self.entity_description = description
714
765
  self._town_name = entry_data["town_name"]
715
766
  self._town_id = entry_data["town_id"]
716
767
  self._station_id = entry_data["station_id"]
717
-
718
- # Unique ID for the entity
719
768
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
720
-
721
- # Asigna entity_category desde description (si está definido)
722
769
  self._attr_entity_category = getattr(description, "entity_category", None)
723
770
 
724
- # Log para depuración
725
771
  _LOGGER.debug(
726
772
  "Inicializando sensor: %s, Unique ID: %s",
727
773
  self.entity_description.name,
@@ -730,7 +776,7 @@ class MeteocatConditionSensor(CoordinatorEntity[MeteocatConditionCoordinator], S
730
776
 
731
777
  @property
732
778
  def native_value(self):
733
- """Return the current UV index value."""
779
+ """Return the current condition value."""
734
780
  if self.entity_description.key == CONDITION:
735
781
  condition_data = self.coordinator.data or {}
736
782
  return condition_data.get("condition", None)
@@ -741,7 +787,6 @@ class MeteocatConditionSensor(CoordinatorEntity[MeteocatConditionCoordinator], S
741
787
  attributes = super().extra_state_attributes or {}
742
788
  if self.entity_description.key == CONDITION:
743
789
  condition_data = self.coordinator.data or {}
744
- # Add the "hour" attribute if it exists
745
790
  attributes["hour"] = condition_data.get("hour", None)
746
791
  return attributes
747
792
 
@@ -750,14 +795,59 @@ class MeteocatConditionSensor(CoordinatorEntity[MeteocatConditionCoordinator], S
750
795
  """Return the device info."""
751
796
  return DeviceInfo(
752
797
  identifiers={(DOMAIN, self._town_id)},
753
- name="Meteocat " + self._station_id + " " + self._town_name,
798
+ name=f"Meteocat {self._station_id} {self._town_name}",
799
+ manufacturer="Meteocat",
800
+ model="Meteocat API",
801
+ )
802
+
803
+ class MeteocatSunSensor(CoordinatorEntity[MeteocatSunCoordinator], SensorEntity):
804
+ """Representation of a Meteocat Sun sensor (sunrise/sunset)."""
805
+ _attr_has_entity_name = True
806
+
807
+ def __init__(self, sun_coordinator, description, entry_data):
808
+ """Initialize the Sun sensor."""
809
+ super().__init__(sun_coordinator)
810
+ self.entity_description = description
811
+ self._town_name = entry_data["town_name"]
812
+ self._town_id = entry_data["town_id"]
813
+ self._station_id = entry_data["station_id"]
814
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
815
+ self._attr_entity_category = getattr(description, "entity_category", None)
816
+
817
+ _LOGGER.debug(
818
+ "Inicializando sensor: %s, Unique ID: %s",
819
+ self.entity_description.name,
820
+ self._attr_unique_id,
821
+ )
822
+
823
+ @property
824
+ def native_value(self):
825
+ """Return the sunrise or sunset time as a datetime."""
826
+ if self.entity_description.key in {SUNRISE, SUNSET}:
827
+ return self.coordinator.data.get(self.entity_description.key)
828
+ return None
829
+
830
+ @property
831
+ def extra_state_attributes(self):
832
+ """Return additional attributes for the sensor."""
833
+ attributes = super().extra_state_attributes or {}
834
+ if self.entity_description.key in {SUNRISE, SUNSET}:
835
+ dt = self.coordinator.data.get(self.entity_description.key)
836
+ attributes["friendly_time"] = dt.strftime("%H:%M") if dt else None
837
+ return attributes
838
+
839
+ @property
840
+ def device_info(self) -> DeviceInfo:
841
+ """Return the device info."""
842
+ return DeviceInfo(
843
+ identifiers={(DOMAIN, self._town_id)},
844
+ name=f"Meteocat {self._station_id} {self._town_name}",
754
845
  manufacturer="Meteocat",
755
846
  model="Meteocat API",
756
847
  )
757
848
 
758
849
  class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity):
759
850
  """Representation of a Meteocat sensor."""
760
-
761
851
  CODE_MAPPING = {
762
852
  WIND_SPEED: WIND_SPEED_CODE,
763
853
  WIND_DIRECTION: WIND_DIRECTION_CODE,
@@ -766,13 +856,11 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
766
856
  PRESSURE: PRESSURE_CODE,
767
857
  PRECIPITATION: PRECIPITATION_CODE,
768
858
  SOLAR_GLOBAL_IRRADIANCE: SOLAR_GLOBAL_IRRADIANCE_CODE,
769
- # UV_INDEX: UV_INDEX_CODE,
770
859
  MAX_TEMPERATURE: MAX_TEMPERATURE_CODE,
771
860
  MIN_TEMPERATURE: MIN_TEMPERATURE_CODE,
772
861
  WIND_GUST: WIND_GUST_CODE,
773
862
  }
774
-
775
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
863
+ _attr_has_entity_name = True
776
864
 
777
865
  def __init__(self, sensor_coordinator, description, entry_data):
778
866
  """Initialize the sensor."""
@@ -783,14 +871,8 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
783
871
  self._town_id = entry_data["town_id"]
784
872
  self._station_name = entry_data["station_name"]
785
873
  self._station_id = entry_data["station_id"]
786
-
787
- # Unique ID for the entity
788
- self._attr_unique_id = f"sensor.{DOMAIN}_{self._station_id}_{self.entity_description.key}"
789
-
790
- # Asigna entity_category desde description (si está definido)
874
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
791
875
  self._attr_entity_category = getattr(description, "entity_category", None)
792
-
793
- # Log para depuración
794
876
  _LOGGER.debug(
795
877
  "Inicializando sensor: %s, Unique ID: %s",
796
878
  self.entity_description.name,
@@ -800,16 +882,11 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
800
882
  @property
801
883
  def native_value(self):
802
884
  """Return the state of the sensor."""
803
- # Información dinámica
804
885
  if self.entity_description.key == FEELS_LIKE:
805
886
  stations = self.coordinator.data or []
806
-
807
- # Variables necesarias
808
887
  temperature = None
809
888
  humidity = None
810
889
  wind_speed = None
811
-
812
- # Obtener valores de las variables
813
890
  for station in stations:
814
891
  variables = station.get("variables", [])
815
892
  for var in variables:
@@ -818,25 +895,19 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
818
895
  if not lectures:
819
896
  continue
820
897
  latest_reading = lectures[-1].get("valor")
821
-
822
898
  if code == TEMPERATURE_CODE:
823
899
  temperature = float(latest_reading)
824
900
  elif code == HUMIDITY_CODE:
825
901
  humidity = float(latest_reading)
826
902
  elif code == WIND_SPEED_CODE:
827
903
  wind_speed = float(latest_reading)
828
-
829
- # Verificar que todas las variables necesarias están presentes
830
904
  if temperature is not None and humidity is not None and wind_speed is not None:
831
- # Cálculo del windchill
832
905
  windchill = (
833
906
  13.1267 +
834
907
  0.6215 * temperature -
835
908
  11.37 * (wind_speed ** 0.16) +
836
909
  0.3965 * temperature * (wind_speed ** 0.16)
837
910
  )
838
-
839
- # Cálculo del heat_index
840
911
  heat_index = (
841
912
  -8.78469476 +
842
913
  1.61139411 * temperature +
@@ -848,8 +919,6 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
848
919
  0.00072546 * temperature * (humidity ** 2) -
849
920
  0.000003582 * (temperature ** 2) * (humidity ** 2)
850
921
  )
851
-
852
- # Lógica de selección
853
922
  if -50 <= temperature <= 10 and wind_speed > 4.8:
854
923
  _LOGGER.debug(f"Sensación térmica por frío, calculada según la fórmula de Wind Chill: {windchill} ºC")
855
924
  return round(windchill, 1)
@@ -861,50 +930,36 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
861
930
  return round(temperature, 1)
862
931
 
863
932
  sensor_code = self.CODE_MAPPING.get(self.entity_description.key)
864
-
865
933
  if sensor_code is not None:
866
- # Accedemos a las estaciones en el JSON recibido
867
934
  stations = self.coordinator.data or []
868
935
  for station in stations:
869
936
  variables = station.get("variables", [])
870
-
871
- # Filtramos por código
872
937
  variable_data = next(
873
938
  (var for var in variables if var.get("codi") == sensor_code),
874
939
  None,
875
940
  )
876
-
877
941
  if variable_data:
878
- # Obtenemos la última lectura
879
942
  lectures = variable_data.get("lectures", [])
880
943
  if lectures:
881
944
  latest_reading = lectures[-1]
882
945
  value = latest_reading.get("valor")
883
-
884
946
  return value
885
-
886
- # Para el sensor WIND_DIRECTION_CARDINAL, convertir grados a dirección cardinal
947
+
887
948
  if self.entity_description.key == WIND_DIRECTION_CARDINAL:
888
949
  stations = self.coordinator.data or []
889
950
  for station in stations:
890
951
  variables = station.get("variables", [])
891
-
892
- # Filtramos por código
893
952
  variable_data = next(
894
953
  (var for var in variables if var.get("codi") == WIND_DIRECTION_CODE),
895
954
  None,
896
955
  )
897
-
898
956
  if variable_data:
899
- # Obtenemos la última lectura
900
957
  lectures = variable_data.get("lectures", [])
901
958
  if lectures:
902
959
  latest_reading = lectures[-1]
903
960
  value = latest_reading.get("valor")
904
-
905
961
  return self._convert_degrees_to_cardinal(value)
906
-
907
- # Lógica específica para el sensor de timestamp
962
+
908
963
  if self.entity_description.key == STATION_TIMESTAMP:
909
964
  stations = self.coordinator.data or []
910
965
  for station in stations:
@@ -912,42 +967,30 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
912
967
  for variable in variables:
913
968
  lectures = variable.get("lectures", [])
914
969
  if lectures:
915
- # Obtenemos el campo `data` de la última lectura
916
970
  latest_reading = lectures[-1]
917
971
  raw_timestamp = latest_reading.get("data")
918
-
919
972
  if raw_timestamp:
920
- # Convertir el timestamp a un objeto datetime
921
973
  try:
922
- # Convertimos raw_timestamp a hora local
923
974
  local_time = convert_to_local_time(raw_timestamp)
924
975
  _LOGGER.debug("Hora UTC: %s convertida a hora local: %s", raw_timestamp, local_time)
925
976
  return local_time
926
977
  except ValueError:
927
- # Manejo de errores si el formato no es válido
928
978
  _LOGGER.error(f"Error al convertir el timestamp '{raw_timestamp}' a hora local.")
929
979
  return None
930
980
 
931
- # Nuevo sensor para la precipitación acumulada
932
981
  if self.entity_description.key == PRECIPITATION_ACCUMULATED:
933
982
  stations = self.coordinator.data or []
934
- total_precipitation = 0.0 # Usa float para permitir acumulación de decimales
935
-
983
+ total_precipitation = 0.0
936
984
  for station in stations:
937
985
  variables = station.get("variables", [])
938
-
939
- # Filtramos por código de precipitación
940
986
  variable_data = next(
941
987
  (var for var in variables if var.get("codi") == PRECIPITATION_CODE),
942
988
  None,
943
989
  )
944
-
945
990
  if variable_data:
946
- # Sumamos las lecturas de precipitación
947
991
  lectures = variable_data.get("lectures", [])
948
992
  for lecture in lectures:
949
- total_precipitation += float(lecture.get("valor", 0.0)) # Convertimos a float
950
-
993
+ total_precipitation += float(lecture.get("valor", 0.0))
951
994
  _LOGGER.debug(f"Total precipitación acumulada: {total_precipitation} mm")
952
995
  return total_precipitation
953
996
 
@@ -957,11 +1000,10 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
957
1000
  def _convert_degrees_to_cardinal(degree: float) -> str:
958
1001
  """Convert degrees to cardinal direction."""
959
1002
  if not isinstance(degree, (int, float)):
960
- return "Unknown" # Retorna "Unknown" si el valor no es un número válido
961
-
1003
+ return "Unknown"
962
1004
  directions = [
963
- "north", "north_northeast", "northeast", "east_northeast", "east", "east_southeast", "southeast", "south_southeast",
964
- "south", "south_southwest", "southwest", "west_southwest", "west", "west_northwest", "northwest", "north_northwest"
1005
+ "north", "north_northeast", "northeast", "east_northeast", "east", "east_southeast", "southeast", "south_southeast",
1006
+ "south", "south_southwest", "southwest", "west_southwest", "west", "west_northwest", "northwest", "north_northwest"
965
1007
  ]
966
1008
  index = round(degree / 22.5) % 16
967
1009
  return directions[index]
@@ -971,31 +1013,24 @@ class MeteocatSensor(CoordinatorEntity[MeteocatSensorCoordinator], SensorEntity)
971
1013
  """Return the device info."""
972
1014
  return DeviceInfo(
973
1015
  identifiers={(DOMAIN, self._town_id)},
974
- name="Meteocat " + self._station_id + " " + self._town_name,
1016
+ name=f"Meteocat {self._station_id} {self._town_name}",
975
1017
  manufacturer="Meteocat",
976
1018
  model="Meteocat API",
977
1019
  )
978
1020
 
979
1021
  class MeteocatTempForecast(CoordinatorEntity[MeteocatTempForecastCoordinator], SensorEntity):
980
1022
  """Representation of a Meteocat Min and Max Temperature sensors."""
981
-
982
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1023
+ _attr_has_entity_name = True
983
1024
 
984
1025
  def __init__(self, temp_forecast_coordinator, description, entry_data):
985
- """Initialize the Mina and Max Temperature sensors."""
1026
+ """Initialize the Min and Max Temperature sensors."""
986
1027
  super().__init__(temp_forecast_coordinator)
987
1028
  self.entity_description = description
988
1029
  self._town_name = entry_data["town_name"]
989
1030
  self._town_id = entry_data["town_id"]
990
1031
  self._station_id = entry_data["station_id"]
991
-
992
- # Unique ID for the entity
993
1032
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
994
-
995
- # Asigna entity_category desde description (si está definido)
996
1033
  self._attr_entity_category = getattr(description, "entity_category", None)
997
-
998
- # Log para depuración
999
1034
  _LOGGER.debug(
1000
1035
  "Inicializando sensor: %s, Unique ID: %s",
1001
1036
  self.entity_description.name,
@@ -1006,7 +1041,6 @@ class MeteocatTempForecast(CoordinatorEntity[MeteocatTempForecastCoordinator], S
1006
1041
  def native_value(self):
1007
1042
  """Return the Max and Min Temp Forecast value."""
1008
1043
  temp_forecast_data = self.coordinator.data or {}
1009
-
1010
1044
  if self.entity_description.key == MAX_TEMPERATURE_FORECAST:
1011
1045
  return temp_forecast_data.get("max_temp_forecast", None)
1012
1046
  if self.entity_description.key == MIN_TEMPERATURE_FORECAST:
@@ -1018,15 +1052,14 @@ class MeteocatTempForecast(CoordinatorEntity[MeteocatTempForecastCoordinator], S
1018
1052
  """Return the device info."""
1019
1053
  return DeviceInfo(
1020
1054
  identifiers={(DOMAIN, self._town_id)},
1021
- name="Meteocat " + self._station_id + " " + self._town_name,
1055
+ name=f"Meteocat {self._station_id} {self._town_name}",
1022
1056
  manufacturer="Meteocat",
1023
1057
  model="Meteocat API",
1024
1058
  )
1025
1059
 
1026
1060
  class MeteocatPrecipitationProbabilitySensor(CoordinatorEntity[DailyForecastCoordinator], SensorEntity):
1027
1061
  """Representation of a Meteocat precipitation probability sensor."""
1028
-
1029
- _attr_has_entity_name = True # Enable device-based naming
1062
+ _attr_has_entity_name = True
1030
1063
 
1031
1064
  def __init__(self, daily_forecast_coordinator, description, entry_data):
1032
1065
  super().__init__(daily_forecast_coordinator)
@@ -1034,10 +1067,8 @@ class MeteocatPrecipitationProbabilitySensor(CoordinatorEntity[DailyForecastCoor
1034
1067
  self._town_name = entry_data["town_name"]
1035
1068
  self._town_id = entry_data["town_id"]
1036
1069
  self._station_id = entry_data["station_id"]
1037
-
1038
1070
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
1039
1071
  self._attr_entity_category = getattr(description, "entity_category", None)
1040
-
1041
1072
  _LOGGER.debug(
1042
1073
  "Initializing sensor: %s, Unique ID: %s",
1043
1074
  self.entity_description.name,
@@ -1058,14 +1089,13 @@ class MeteocatPrecipitationProbabilitySensor(CoordinatorEntity[DailyForecastCoor
1058
1089
  def device_info(self) -> DeviceInfo:
1059
1090
  return DeviceInfo(
1060
1091
  identifiers={(DOMAIN, self._town_id)},
1061
- name="Meteocat " + self._station_id + " " + self._town_name,
1092
+ name=f"Meteocat {self._station_id} {self._town_name}",
1062
1093
  manufacturer="Meteocat",
1063
1094
  model="Meteocat API",
1064
1095
  )
1065
1096
 
1066
1097
  class MeteocatHourlyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordinator], SensorEntity):
1067
-
1068
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1098
+ _attr_has_entity_name = True
1069
1099
 
1070
1100
  def __init__(self, entity_coordinator, description, entry_data):
1071
1101
  super().__init__(entity_coordinator)
@@ -1073,11 +1103,7 @@ class MeteocatHourlyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordin
1073
1103
  self._town_name = entry_data["town_name"]
1074
1104
  self._town_id = entry_data["town_id"]
1075
1105
  self._station_id = entry_data["station_id"]
1076
-
1077
- # Unique ID for the entity
1078
- self._attr_unique_id = f"sensor.{DOMAIN}_{self._station_id}_hourly_status"
1079
-
1080
- # Assign entity_category if defined in the description
1106
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_hourly_status"
1081
1107
  self._attr_entity_category = getattr(description, "entity_category", None)
1082
1108
 
1083
1109
  def _get_first_date(self):
@@ -1101,8 +1127,6 @@ class MeteocatHourlyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordin
1101
1127
  f"hora de contacto a la API >= {DEFAULT_VALIDITY_HOURS}, "
1102
1128
  f"minutos de contacto a la API >= {DEFAULT_VALIDITY_MINUTES}."
1103
1129
  )
1104
-
1105
- # Validar fecha y hora según la lógica del coordinador
1106
1130
  if days_difference > DEFAULT_VALIDITY_DAYS and current_time >= time(DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES):
1107
1131
  return "obsolete"
1108
1132
  return "updated"
@@ -1121,14 +1145,13 @@ class MeteocatHourlyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordin
1121
1145
  """Return the device info."""
1122
1146
  return DeviceInfo(
1123
1147
  identifiers={(DOMAIN, self._town_id)},
1124
- name="Meteocat " + self._station_id + " " + self._town_name,
1148
+ name=f"Meteocat {self._station_id} {self._town_name}",
1125
1149
  manufacturer="Meteocat",
1126
1150
  model="Meteocat API",
1127
1151
  )
1128
1152
 
1129
1153
  class MeteocatDailyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordinator], SensorEntity):
1130
-
1131
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1154
+ _attr_has_entity_name = True
1132
1155
 
1133
1156
  def __init__(self, entity_coordinator, description, entry_data):
1134
1157
  super().__init__(entity_coordinator)
@@ -1136,11 +1159,7 @@ class MeteocatDailyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordina
1136
1159
  self._town_name = entry_data["town_name"]
1137
1160
  self._town_id = entry_data["town_id"]
1138
1161
  self._station_id = entry_data["station_id"]
1139
-
1140
- # Unique ID for the entity
1141
- self._attr_unique_id = f"sensor.{DOMAIN}_{self._station_id}_daily_status"
1142
-
1143
- # Assign entity_category if defined in the description
1162
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_daily_status"
1144
1163
  self._attr_entity_category = getattr(description, "entity_category", None)
1145
1164
 
1146
1165
  def _get_first_date(self):
@@ -1164,8 +1183,6 @@ class MeteocatDailyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordina
1164
1183
  f"hora de contacto a la API >= {DEFAULT_VALIDITY_HOURS}, "
1165
1184
  f"minutos de contacto a la API >= {DEFAULT_VALIDITY_MINUTES}."
1166
1185
  )
1167
-
1168
- # Validar fecha y hora según la lógica del coordinador
1169
1186
  if days_difference > DEFAULT_VALIDITY_DAYS and current_time >= time(DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES):
1170
1187
  return "obsolete"
1171
1188
  return "updated"
@@ -1184,14 +1201,13 @@ class MeteocatDailyForecastStatusSensor(CoordinatorEntity[MeteocatEntityCoordina
1184
1201
  """Return the device info."""
1185
1202
  return DeviceInfo(
1186
1203
  identifiers={(DOMAIN, self._town_id)},
1187
- name="Meteocat " + self._station_id + " " + self._town_name,
1204
+ name=f"Meteocat {self._station_id} {self._town_name}",
1188
1205
  manufacturer="Meteocat",
1189
1206
  model="Meteocat API",
1190
1207
  )
1191
1208
 
1192
1209
  class MeteocatUviStatusSensor(CoordinatorEntity[MeteocatUviCoordinator], SensorEntity):
1193
-
1194
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1210
+ _attr_has_entity_name = True
1195
1211
 
1196
1212
  def __init__(self, uvi_coordinator, description, entry_data):
1197
1213
  super().__init__(uvi_coordinator)
@@ -1199,11 +1215,7 @@ class MeteocatUviStatusSensor(CoordinatorEntity[MeteocatUviCoordinator], SensorE
1199
1215
  self._town_name = entry_data["town_name"]
1200
1216
  self._town_id = entry_data["town_id"]
1201
1217
  self._station_id = entry_data["station_id"]
1202
-
1203
- # Unique ID for the entity
1204
- self._attr_unique_id = f"sensor.{DOMAIN}_{self._station_id}_uvi_status"
1205
-
1206
- # Assign entity_category if defined in the description
1218
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_uvi_status"
1207
1219
  self._attr_entity_category = getattr(description, "entity_category", None)
1208
1220
 
1209
1221
  def _get_first_date(self):
@@ -1226,8 +1238,6 @@ class MeteocatUviStatusSensor(CoordinatorEntity[MeteocatUviCoordinator], SensorE
1226
1238
  f"hora de contacto a la API >= {DEFAULT_VALIDITY_HOURS}, "
1227
1239
  f"minutos de contacto a la API >= {DEFAULT_VALIDITY_MINUTES}."
1228
1240
  )
1229
-
1230
- # Validar fecha y hora según la lógica del coordinador
1231
1241
  if days_difference > DEFAULT_VALIDITY_DAYS and current_time >= time(DEFAULT_VALIDITY_HOURS, DEFAULT_VALIDITY_MINUTES):
1232
1242
  return "obsolete"
1233
1243
  return "updated"
@@ -1246,13 +1256,13 @@ class MeteocatUviStatusSensor(CoordinatorEntity[MeteocatUviCoordinator], SensorE
1246
1256
  """Return the device info."""
1247
1257
  return DeviceInfo(
1248
1258
  identifiers={(DOMAIN, self._town_id)},
1249
- name="Meteocat " + self._station_id + " " + self._town_name,
1259
+ name=f"Meteocat {self._station_id} {self._town_name}",
1250
1260
  manufacturer="Meteocat",
1251
1261
  model="Meteocat API",
1252
1262
  )
1253
1263
 
1254
1264
  class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], SensorEntity):
1255
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1265
+ _attr_has_entity_name = True
1256
1266
 
1257
1267
  def __init__(self, alerts_coordinator, description, entry_data):
1258
1268
  super().__init__(alerts_coordinator)
@@ -1262,11 +1272,7 @@ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], Se
1262
1272
  self._station_id = entry_data["station_id"]
1263
1273
  self._region_id = entry_data["region_id"]
1264
1274
  self._limit_prediccio = entry_data["limit_prediccio"]
1265
-
1266
- # Unique ID for the entity
1267
- self._attr_unique_id = f"sensor.{DOMAIN}_{self._region_id}_alert_status"
1268
-
1269
- # Assign entity_category if defined in the description
1275
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_alert_status"
1270
1276
  self._attr_entity_category = getattr(description, "entity_category", None)
1271
1277
 
1272
1278
  def _get_data_update(self):
@@ -1289,7 +1295,6 @@ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], Se
1289
1295
  multiplier = ALERT_VALIDITY_MULTIPLIER_500
1290
1296
  else:
1291
1297
  multiplier = ALERT_VALIDITY_MULTIPLIER_DEFAULT
1292
-
1293
1298
  return timedelta(minutes=DEFAULT_ALERT_VALIDITY_TIME * multiplier)
1294
1299
 
1295
1300
  @property
@@ -1298,11 +1303,8 @@ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], Se
1298
1303
  data_update = self._get_data_update()
1299
1304
  if not data_update:
1300
1305
  return "unknown"
1301
-
1302
1306
  current_time = datetime.now(ZoneInfo("UTC"))
1303
1307
  validity_duration = self._get_validity_duration()
1304
-
1305
- # Comprobar si el archivo de alertas está obsoleto
1306
1308
  if current_time - data_update >= validity_duration:
1307
1309
  return "obsolete"
1308
1310
  return "updated"
@@ -1328,7 +1330,6 @@ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], Se
1328
1330
 
1329
1331
  class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinator], SensorEntity):
1330
1332
  """Sensor dinámico que muestra el estado de las alertas por región."""
1331
-
1332
1333
  METEOR_MAPPING = {
1333
1334
  "Temps violent": "violent_weather",
1334
1335
  "Intensitat de pluja": "rain_intensity",
@@ -1340,8 +1341,7 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1340
1341
  "Calor": "heat",
1341
1342
  "Calor nocturna": "night_heat",
1342
1343
  }
1343
-
1344
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1344
+ _attr_has_entity_name = True
1345
1345
 
1346
1346
  def __init__(self, alerts_region_coordinator, description, entry_data):
1347
1347
  super().__init__(alerts_region_coordinator)
@@ -1350,11 +1350,7 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1350
1350
  self._town_id = entry_data["town_id"]
1351
1351
  self._station_id = entry_data["station_id"]
1352
1352
  self._region_id = entry_data["region_id"]
1353
-
1354
- # Unique ID for the entity
1355
- self._attr_unique_id = f"sensor.{DOMAIN}_{self._region_id}_alerts"
1356
-
1357
- # Assign entity_category if defined in the description
1353
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_alerts"
1358
1354
  self._attr_entity_category = getattr(description, "entity_category", None)
1359
1355
 
1360
1356
  def _map_meteor_case_insensitive(self, meteor: str) -> str:
@@ -1373,8 +1369,6 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1373
1369
  def extra_state_attributes(self):
1374
1370
  """Devuelve los atributos extra del sensor con los nombres traducidos."""
1375
1371
  meteor_details = self.coordinator.data.get("detalles", {}).get("meteor", {})
1376
-
1377
- # Convertimos las claves al formato deseado usando el mapping
1378
1372
  attributes = {}
1379
1373
  for i, meteor in enumerate(meteor_details.keys()):
1380
1374
  mapped_name = self._map_meteor_case_insensitive(meteor)
@@ -1382,7 +1376,6 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1382
1376
  _LOGGER.warning("Meteor desconocido sin mapeo: '%s'. Añadirlo a 'METEOR_MAPPING' del coordinador 'MeteocatAlertRegionSensor' si es necesario.", meteor)
1383
1377
  mapped_name = "unknown"
1384
1378
  attributes[f"alert_{i+1}"] = mapped_name
1385
-
1386
1379
  _LOGGER.info("Atributos traducidos del sensor: %s", attributes)
1387
1380
  return attributes
1388
1381
 
@@ -1391,7 +1384,7 @@ class MeteocatAlertRegionSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1391
1384
  """Devuelve la información del dispositivo."""
1392
1385
  return DeviceInfo(
1393
1386
  identifiers={(DOMAIN, self._town_id)},
1394
- name="Meteocat " + self._station_id + " " + self._town_name,
1387
+ name=f"Meteocat {self._station_id} {self._town_name}",
1395
1388
  manufacturer="Meteocat",
1396
1389
  model="Meteocat API",
1397
1390
  )
@@ -1408,12 +1401,10 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1408
1401
  ALERT_WARM_NIGHT: "Calor nocturna",
1409
1402
  ALERT_SNOW: "Neu acumulada en 24 hores",
1410
1403
  }
1411
-
1412
1404
  STATE_MAPPING = {
1413
1405
  "Obert": "opened",
1414
1406
  "Tancat": "closed",
1415
1407
  }
1416
-
1417
1408
  UMBRAL_MAPPING = {
1418
1409
  "Ratxes de vent > 25 m/s": "wind_gusts_25",
1419
1410
  "Esclafits": "microburst",
@@ -1447,8 +1438,7 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1447
1438
  "gruix > 2 cm a cotes superiors a 300 metres fins a 600 metres": "thickness_2_at_300",
1448
1439
  "gruix ≥ 0 cm a cotes inferiors a 300 metres": "thickness_0_at_300",
1449
1440
  }
1450
-
1451
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1441
+ _attr_has_entity_name = True
1452
1442
 
1453
1443
  def __init__(self, alerts_region_coordinator, description, entry_data):
1454
1444
  super().__init__(alerts_region_coordinator)
@@ -1457,14 +1447,8 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1457
1447
  self._town_id = entry_data["town_id"]
1458
1448
  self._station_id = entry_data["station_id"]
1459
1449
  self._region_id = entry_data["region_id"]
1460
-
1461
- # Unique ID for the entity
1462
- self._attr_unique_id = f"sensor.{DOMAIN}_{self._region_id}_{self.entity_description.key}"
1463
-
1464
- # Assign entity_category if defined in the description
1450
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
1465
1451
  self._attr_entity_category = getattr(description, "entity_category", None)
1466
-
1467
- # Log para depuración
1468
1452
  _LOGGER.debug(
1469
1453
  "Inicializando sensor: %s, Unique ID: %s",
1470
1454
  self.entity_description.name,
@@ -1494,10 +1478,7 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1494
1478
  meteor_type = self.METEOR_MAPPING.get(self.entity_description.key)
1495
1479
  if not meteor_type:
1496
1480
  return "Desconocido"
1497
-
1498
1481
  meteor_data = self._get_meteor_data_case_insensitive(meteor_type)
1499
-
1500
- # Convertir estado para translation_key
1501
1482
  estado_original = meteor_data.get("estado", "Tancat")
1502
1483
  return self.STATE_MAPPING.get(estado_original, "unknown")
1503
1484
 
@@ -1512,22 +1493,17 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1512
1493
  self.coordinator.data.get("detalles", {}).get("meteor", {}).keys(),
1513
1494
  )
1514
1495
  return "unknown"
1515
-
1516
1496
  meteor_data = self._get_meteor_data_case_insensitive(meteor_type)
1517
1497
  if not meteor_data:
1518
1498
  return {}
1519
-
1520
- # Convertir umbral para translation_key
1521
1499
  umbral_original = meteor_data.get("umbral")
1522
1500
  umbral_convertido = self._get_umbral_case_insensitive(umbral_original)
1523
-
1524
1501
  if umbral_convertido == "unknown" and umbral_original is not None:
1525
1502
  _LOGGER.warning(
1526
1503
  "Umbral desconocido para sensor %s: '%s'. Añadirlo a 'UMBRAL_MAPPING' del coordinador 'MeteocatAlertMeteorSensor' si es necesario.",
1527
1504
  self.entity_description.key,
1528
1505
  umbral_original
1529
1506
  )
1530
-
1531
1507
  return {
1532
1508
  "inicio": meteor_data.get("inicio"),
1533
1509
  "fin": meteor_data.get("fin"),
@@ -1544,13 +1520,13 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1544
1520
  """Devuelve la información del dispositivo."""
1545
1521
  return DeviceInfo(
1546
1522
  identifiers={(DOMAIN, self._town_id)},
1547
- name="Meteocat " + self._station_id + " " + self._town_name,
1523
+ name=f"Meteocat {self._station_id} {self._town_name}",
1548
1524
  manufacturer="Meteocat",
1549
1525
  model="Meteocat API",
1550
1526
  )
1551
1527
 
1552
1528
  class MeteocatQuotaStatusSensor(CoordinatorEntity[MeteocatQuotesCoordinator], SensorEntity):
1553
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1529
+ _attr_has_entity_name = True
1554
1530
 
1555
1531
  def __init__(self, quotes_coordinator, description, entry_data):
1556
1532
  super().__init__(quotes_coordinator)
@@ -1558,11 +1534,7 @@ class MeteocatQuotaStatusSensor(CoordinatorEntity[MeteocatQuotesCoordinator], Se
1558
1534
  self._town_name = entry_data["town_name"]
1559
1535
  self._town_id = entry_data["town_id"]
1560
1536
  self._station_id = entry_data["station_id"]
1561
-
1562
- # Unique ID for the entity
1563
1537
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_quota_status"
1564
-
1565
- # Assign entity_category if defined in the description
1566
1538
  self._attr_entity_category = getattr(description, "entity_category", None)
1567
1539
 
1568
1540
  def _get_data_update(self):
@@ -1581,13 +1553,9 @@ class MeteocatQuotaStatusSensor(CoordinatorEntity[MeteocatQuotesCoordinator], Se
1581
1553
  data_update = self._get_data_update()
1582
1554
  if not data_update:
1583
1555
  return "unknown"
1584
-
1585
1556
  current_time = datetime.now(ZoneInfo("UTC"))
1586
-
1587
- # Comprobar si el archivo de alertas está obsoleto
1588
1557
  if current_time - data_update >= timedelta(minutes=DEFAULT_QUOTES_VALIDITY_TIME):
1589
1558
  return "obsolete"
1590
-
1591
1559
  return "updated"
1592
1560
 
1593
1561
  @property
@@ -1611,8 +1579,6 @@ class MeteocatQuotaStatusSensor(CoordinatorEntity[MeteocatQuotesCoordinator], Se
1611
1579
 
1612
1580
  class MeteocatQuotaSensor(CoordinatorEntity[MeteocatQuotesFileCoordinator], SensorEntity):
1613
1581
  """Representation of Meteocat Quota sensors."""
1614
-
1615
- # Mapeo de claves en sensor.py a nombres en quotes.json
1616
1582
  QUOTA_MAPPING = {
1617
1583
  "quota_xdde": "XDDE",
1618
1584
  "quota_prediccio": "Prediccio",
@@ -1620,15 +1586,12 @@ class MeteocatQuotaSensor(CoordinatorEntity[MeteocatQuotesFileCoordinator], Sens
1620
1586
  "quota_xema": "XEMA",
1621
1587
  "quota_queries": "Quota",
1622
1588
  }
1623
-
1624
- # Mapeo de periodos para facilitar la traducción del estado
1625
1589
  PERIOD_STATE_MAPPING = {
1626
1590
  "Setmanal": "weekly",
1627
1591
  "Mensual": "monthly",
1628
1592
  "Anual": "annual",
1629
1593
  }
1630
-
1631
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1594
+ _attr_has_entity_name = True
1632
1595
 
1633
1596
  def __init__(self, quotes_file_coordinator, description, entry_data):
1634
1597
  super().__init__(quotes_file_coordinator)
@@ -1636,28 +1599,20 @@ class MeteocatQuotaSensor(CoordinatorEntity[MeteocatQuotesFileCoordinator], Sens
1636
1599
  self._town_name = entry_data["town_name"]
1637
1600
  self._town_id = entry_data["town_id"]
1638
1601
  self._station_id = entry_data["station_id"]
1639
-
1640
- # Unique ID for the entity
1641
1602
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
1642
-
1643
- # Assign entity_category if defined in the description
1644
1603
  self._attr_entity_category = getattr(description, "entity_category", None)
1645
1604
 
1646
1605
  def _get_plan_data(self):
1647
1606
  """Encuentra los datos del plan correspondiente al sensor actual."""
1648
1607
  if not self.coordinator.data:
1649
1608
  return None
1650
-
1651
1609
  plan_name = self.QUOTA_MAPPING.get(self.entity_description.key)
1652
-
1653
1610
  if not plan_name:
1654
1611
  _LOGGER.error(f"No se encontró un mapeo para la clave: {self.entity_description.key}")
1655
1612
  return None
1656
-
1657
1613
  for plan in self.coordinator.data.get("plans", []):
1658
1614
  if plan.get("nom") == plan_name:
1659
- return plan # Retorna el plan encontrado
1660
-
1615
+ return plan
1661
1616
  _LOGGER.warning(f"No se encontró el plan '{plan_name}' en los datos del coordinador.")
1662
1617
  return None
1663
1618
 
@@ -1665,18 +1620,14 @@ class MeteocatQuotaSensor(CoordinatorEntity[MeteocatQuotesFileCoordinator], Sens
1665
1620
  def native_value(self):
1666
1621
  """Devuelve el estado de la cuota: 'ok' si no se ha excedido, 'exceeded' si se ha superado."""
1667
1622
  plan = self._get_plan_data()
1668
-
1669
1623
  if not plan:
1670
1624
  return None
1671
-
1672
1625
  max_consultes = plan.get("maxConsultes")
1673
1626
  consultes_realitzades = plan.get("consultesRealitzades")
1674
-
1675
1627
  if max_consultes is None or consultes_realitzades is None or \
1676
1628
  not isinstance(max_consultes, (int, float)) or not isinstance(consultes_realitzades, (int, float)):
1677
1629
  _LOGGER.warning(f"Datos inválidos para el plan '{plan.get('nom', 'unknown')}': {plan}")
1678
1630
  return None
1679
-
1680
1631
  return "ok" if consultes_realitzades <= max_consultes else "exceeded"
1681
1632
 
1682
1633
  @property
@@ -1684,21 +1635,16 @@ class MeteocatQuotaSensor(CoordinatorEntity[MeteocatQuotesFileCoordinator], Sens
1684
1635
  """Devuelve atributos adicionales del estado del sensor."""
1685
1636
  attributes = super().extra_state_attributes or {}
1686
1637
  plan = self._get_plan_data()
1687
-
1688
1638
  if not plan:
1689
1639
  return {}
1690
-
1691
- # Aplicar el mapeo de periodos
1692
1640
  period = plan.get("periode", "desconocido")
1693
- translated_period = self.PERIOD_STATE_MAPPING.get(period, period) # Si no está en el mapping, dejar el original
1694
-
1641
+ translated_period = self.PERIOD_STATE_MAPPING.get(period, period)
1695
1642
  attributes.update({
1696
1643
  "period": translated_period,
1697
1644
  "max_queries": plan.get("maxConsultes"),
1698
1645
  "made_queries": plan.get("consultesRealitzades"),
1699
1646
  "remain_queries": plan.get("consultesRestants"),
1700
1647
  })
1701
-
1702
1648
  return attributes
1703
1649
 
1704
1650
  @property
@@ -1712,7 +1658,7 @@ class MeteocatQuotaSensor(CoordinatorEntity[MeteocatQuotesFileCoordinator], Sens
1712
1658
  )
1713
1659
 
1714
1660
  class MeteocatLightningStatusSensor(CoordinatorEntity[MeteocatLightningCoordinator], SensorEntity):
1715
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1661
+ _attr_has_entity_name = True
1716
1662
 
1717
1663
  def __init__(self, lightning_coordinator, description, entry_data):
1718
1664
  super().__init__(lightning_coordinator)
@@ -1720,11 +1666,7 @@ class MeteocatLightningStatusSensor(CoordinatorEntity[MeteocatLightningCoordinat
1720
1666
  self._town_name = entry_data["town_name"]
1721
1667
  self._town_id = entry_data["town_id"]
1722
1668
  self._station_id = entry_data["station_id"]
1723
-
1724
- # Unique ID for the entity
1725
1669
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_lightning_status"
1726
-
1727
- # Assign entity_category if defined in the description
1728
1670
  self._attr_entity_category = getattr(description, "entity_category", None)
1729
1671
 
1730
1672
  def _get_data_update(self):
@@ -1732,8 +1674,8 @@ class MeteocatLightningStatusSensor(CoordinatorEntity[MeteocatLightningCoordinat
1732
1674
  data_update = self.coordinator.data.get("actualizado")
1733
1675
  if data_update:
1734
1676
  try:
1735
- local_time = datetime.fromisoformat(data_update) # Ya tiene offset (+01:00 o +02:00)
1736
- return local_time.astimezone(ZoneInfo("UTC")) # Convertir a UTC
1677
+ local_time = datetime.fromisoformat(data_update)
1678
+ return local_time.astimezone(ZoneInfo("UTC"))
1737
1679
  except ValueError:
1738
1680
  _LOGGER.error("Formato de fecha de actualización inválido: %s", data_update)
1739
1681
  return None
@@ -1752,15 +1694,11 @@ class MeteocatLightningStatusSensor(CoordinatorEntity[MeteocatLightningCoordinat
1752
1694
  data_update = self._get_data_update()
1753
1695
  if not data_update:
1754
1696
  return "unknown"
1755
-
1756
1697
  now = datetime.now(timezone.utc).astimezone(TIMEZONE)
1757
- current_time = now.time() # Extraer solo la parte de la hora
1758
- offset = now.utcoffset().total_seconds() / 3600 # Obtener el offset en horas
1759
-
1760
- # Determinar la hora de validez considerando el offset horario, el horario de verano (+02:00) o invierno (+01:00)
1698
+ current_time = now.time()
1699
+ offset = now.utcoffset().total_seconds() / 3600
1761
1700
  validity_start_time = time(int(DEFAULT_LIGHTNING_VALIDITY_HOURS + offset), DEFAULT_LIGHTNING_VALIDITY_MINUTES)
1762
1701
  validity_duration = timedelta(minutes=DEFAULT_LIGHTNING_VALIDITY_TIME)
1763
-
1764
1702
  return self._determine_status(now, data_update, current_time, validity_start_time, validity_duration)
1765
1703
 
1766
1704
  @property
@@ -1784,8 +1722,7 @@ class MeteocatLightningStatusSensor(CoordinatorEntity[MeteocatLightningCoordinat
1784
1722
 
1785
1723
  class MeteocatLightningSensor(CoordinatorEntity[MeteocatLightningFileCoordinator], SensorEntity):
1786
1724
  """Representation of Meteocat Lightning sensors."""
1787
-
1788
- _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1725
+ _attr_has_entity_name = True
1789
1726
 
1790
1727
  def __init__(self, lightning_file_coordinator, description, entry_data):
1791
1728
  super().__init__(lightning_file_coordinator)
@@ -1794,11 +1731,7 @@ class MeteocatLightningSensor(CoordinatorEntity[MeteocatLightningFileCoordinator
1794
1731
  self._town_id = entry_data["town_id"]
1795
1732
  self._station_id = entry_data["station_id"]
1796
1733
  self._region_id = entry_data["region_id"]
1797
-
1798
- # Unique ID for the entity
1799
1734
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
1800
-
1801
- # Assign entity_category if defined in the description
1802
1735
  self._attr_entity_category = getattr(description, "entity_category", None)
1803
1736
 
1804
1737
  @property
@@ -1820,8 +1753,6 @@ class MeteocatLightningSensor(CoordinatorEntity[MeteocatLightningFileCoordinator
1820
1753
  data = self.coordinator.data.get("town", {})
1821
1754
  else:
1822
1755
  return attributes
1823
-
1824
- # Agregar atributos específicos
1825
1756
  attributes.update({
1826
1757
  "cloud_cloud": data.get("cc", 0),
1827
1758
  "cloud_ground_neg": data.get("cg-", 0),
@@ -1837,4 +1768,124 @@ class MeteocatLightningSensor(CoordinatorEntity[MeteocatLightningFileCoordinator
1837
1768
  name=f"Meteocat {self._station_id} {self._town_name}",
1838
1769
  manufacturer="Meteocat",
1839
1770
  model="Meteocat API",
1840
- )
1771
+ )
1772
+
1773
+ class MeteocatSunSensor(CoordinatorEntity[MeteocatSunFileCoordinator], SensorEntity):
1774
+ """Representation of Meteocat Sun sensors (sunrise/sunset)."""
1775
+ _attr_has_entity_name = True
1776
+
1777
+ def __init__(self, sun_file_coordinator, description, entry_data):
1778
+ """Initialize the Sun sensor."""
1779
+ super().__init__(sun_file_coordinator)
1780
+ self.entity_description = description
1781
+ self._town_name = entry_data["town_name"]
1782
+ self._town_id = entry_data["town_id"]
1783
+ self._station_id = entry_data["station_id"]
1784
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
1785
+ self._attr_entity_category = getattr(description, "entity_category", None)
1786
+
1787
+ _LOGGER.debug(
1788
+ "Inicializando sensor: %s, Unique ID: %s",
1789
+ self.entity_description.name,
1790
+ self._attr_unique_id,
1791
+ )
1792
+
1793
+ @property
1794
+ def native_value(self):
1795
+ """Return the sunrise or sunset time as a datetime."""
1796
+ if self.entity_description.key in {SUNRISE, SUNSET}:
1797
+ time_str = self.coordinator.data.get(self.entity_description.key)
1798
+ if time_str:
1799
+ try:
1800
+ return datetime.fromisoformat(time_str)
1801
+ except ValueError:
1802
+ _LOGGER.error("Formato de fecha inválido para %s: %s", self.entity_description.key, time_str)
1803
+ return None
1804
+ return None
1805
+
1806
+ @property
1807
+ def extra_state_attributes(self):
1808
+ """Return additional attributes for the sensor."""
1809
+ attributes = super().extra_state_attributes or {}
1810
+ if self.entity_description.key in {SUNRISE, SUNSET}:
1811
+ time_str = self.coordinator.data.get(self.entity_description.key)
1812
+ if time_str:
1813
+ try:
1814
+ dt = datetime.fromisoformat(time_str)
1815
+ attributes["friendly_time"] = dt.strftime("%H:%M")
1816
+ except ValueError:
1817
+ attributes["friendly_time"] = None
1818
+ else:
1819
+ attributes["friendly_time"] = None
1820
+ return attributes
1821
+
1822
+ @property
1823
+ def device_info(self) -> DeviceInfo:
1824
+ """Return the device info."""
1825
+ return DeviceInfo(
1826
+ identifiers={(DOMAIN, self._town_id)},
1827
+ name=f"Meteocat {self._station_id} {self._town_name}",
1828
+ manufacturer="Meteocat",
1829
+ model="Meteocat API",
1830
+ )
1831
+
1832
+ class MeteocatSunStatusSensor(CoordinatorEntity[MeteocatSunCoordinator], SensorEntity):
1833
+ """Representation of Meteocat Sun file status sensor."""
1834
+ _attr_has_entity_name = True
1835
+
1836
+ def __init__(self, sun_coordinator, description, entry_data):
1837
+ """Initialize the Sun status sensor."""
1838
+ super().__init__(sun_coordinator)
1839
+ self.entity_description = description
1840
+ self._town_name = entry_data["town_name"]
1841
+ self._town_id = entry_data["town_id"]
1842
+ self._station_id = entry_data["station_id"]
1843
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_sun_status"
1844
+ self._attr_entity_category = getattr(description, "entity_category", None)
1845
+
1846
+ _LOGGER.debug(
1847
+ "Inicializando sensor: %s, Unique ID: %s",
1848
+ self.entity_description.name,
1849
+ self._attr_unique_id,
1850
+ )
1851
+
1852
+ def _get_data_update(self):
1853
+ """Obtain the update date from the coordinator and convert to UTC."""
1854
+ data_update = self.coordinator.data.get("actualizado")
1855
+ if data_update:
1856
+ try:
1857
+ local_time = datetime.fromisoformat(data_update)
1858
+ return local_time.astimezone(ZoneInfo("UTC"))
1859
+ except ValueError:
1860
+ _LOGGER.error("Formato de fecha de actualización inválido: %s", data_update)
1861
+ return None
1862
+
1863
+ @property
1864
+ def native_value(self):
1865
+ """Return the status of the sun file based on the update date."""
1866
+ data_update = self._get_data_update()
1867
+ if not data_update:
1868
+ return "unknown"
1869
+ now = datetime.now(timezone.utc).astimezone(TIMEZONE)
1870
+ if (now - data_update) > timedelta(days=1):
1871
+ return "obsolete"
1872
+ return "updated"
1873
+
1874
+ @property
1875
+ def extra_state_attributes(self):
1876
+ """Return additional attributes for the sensor."""
1877
+ attributes = super().extra_state_attributes or {}
1878
+ data_update = self._get_data_update()
1879
+ if data_update:
1880
+ attributes["update_date"] = data_update.isoformat()
1881
+ return attributes
1882
+
1883
+ @property
1884
+ def device_info(self) -> DeviceInfo:
1885
+ """Return the device info."""
1886
+ return DeviceInfo(
1887
+ identifiers={(DOMAIN, self._town_id)},
1888
+ name=f"Meteocat {self._station_id} {self._town_name}",
1889
+ manufacturer="Meteocat",
1890
+ model="Meteocat API",
1891
+ )