meteocat 2.1.0 → 2.2.2

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.
@@ -10,7 +10,10 @@ import voluptuous as vol
10
10
  from .const import (
11
11
  CONF_API_KEY,
12
12
  LIMIT_XEMA,
13
- LIMIT_PREDICCIO
13
+ LIMIT_PREDICCIO,
14
+ LIMIT_XDDE,
15
+ LIMIT_QUOTA,
16
+ LIMIT_BASIC
14
17
  )
15
18
  from meteocatpy.town import MeteocatTown
16
19
  from meteocatpy.exceptions import (
@@ -64,6 +67,9 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
64
67
  self.api_key = user_input.get(CONF_API_KEY)
65
68
  self.limit_xema = user_input.get(LIMIT_XEMA)
66
69
  self.limit_prediccio = user_input.get(LIMIT_PREDICCIO)
70
+ self.limit_xdde = user_input.get(LIMIT_XDDE)
71
+ self.limit_quota = user_input.get(LIMIT_QUOTA)
72
+ self.limit_basic = user_input.get(LIMIT_BASIC)
67
73
 
68
74
  # Validar la nueva API Key utilizando MeteocatTown
69
75
  if self.api_key:
@@ -84,7 +90,8 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
84
90
  errors["base"] = "unknown"
85
91
 
86
92
  # Validar que los límites sean números positivos
87
- if not cv.positive_int(self.limit_xema) or not cv.positive_int(self.limit_prediccio):
93
+ limits_to_validate = [self.limit_xema, self.limit_prediccio, self.limit_xdde, self.limit_quota, self.limit_basic]
94
+ if not all(cv.positive_int(limit) for limit in limits_to_validate if limit is not None):
88
95
  errors["base"] = "invalid_limit"
89
96
 
90
97
  if not errors:
@@ -96,6 +103,12 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
96
103
  data_update[LIMIT_XEMA] = self.limit_xema
97
104
  if self.limit_prediccio:
98
105
  data_update[LIMIT_PREDICCIO] = self.limit_prediccio
106
+ if self.limit_xdde:
107
+ data_update[LIMIT_XDDE] = self.limit_xdde
108
+ if self.limit_quota:
109
+ data_update[LIMIT_QUOTA] = self.limit_quota
110
+ if self.limit_basic:
111
+ data_update[LIMIT_BASIC] = self.limit_basic
99
112
 
100
113
  self.hass.config_entries.async_update_entry(
101
114
  self._config_entry,
@@ -110,6 +123,9 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
110
123
  vol.Required(CONF_API_KEY): str,
111
124
  vol.Required(LIMIT_XEMA, default=self._config_entry.data.get(LIMIT_XEMA)): cv.positive_int,
112
125
  vol.Required(LIMIT_PREDICCIO, default=self._config_entry.data.get(LIMIT_PREDICCIO)): cv.positive_int,
126
+ vol.Required(LIMIT_XDDE, default=self._config_entry.data.get(LIMIT_XDDE)): cv.positive_int,
127
+ vol.Required(LIMIT_QUOTA, default=self._config_entry.data.get(LIMIT_QUOTA)): cv.positive_int,
128
+ vol.Required(LIMIT_BASIC, default=self._config_entry.data.get(LIMIT_BASIC)): cv.positive_int,
113
129
  })
114
130
  return self.async_show_form(
115
131
  step_id="update_api_and_limits", data_schema=schema, errors=errors
@@ -122,9 +138,13 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
122
138
  if user_input is not None:
123
139
  self.limit_xema = user_input.get(LIMIT_XEMA)
124
140
  self.limit_prediccio = user_input.get(LIMIT_PREDICCIO)
141
+ self.limit_xdde = user_input.get(LIMIT_XDDE)
142
+ self.limit_quota = user_input.get(LIMIT_QUOTA)
143
+ self.limit_basic = user_input.get(LIMIT_BASIC)
125
144
 
126
145
  # Validar que los límites sean números positivos
127
- if not cv.positive_int(self.limit_xema) or not cv.positive_int(self.limit_prediccio):
146
+ limits_to_validate = [self.limit_xema, self.limit_prediccio, self.limit_xdde, self.limit_quota, self.limit_basic]
147
+ if not all(cv.positive_int(limit) for limit in limits_to_validate if limit is not None):
128
148
  errors["base"] = "invalid_limit"
129
149
 
130
150
  if not errors:
@@ -133,7 +153,10 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
133
153
  data={
134
154
  **self._config_entry.data,
135
155
  LIMIT_XEMA: self.limit_xema,
136
- LIMIT_PREDICCIO: self.limit_prediccio
156
+ LIMIT_PREDICCIO: self.limit_prediccio,
157
+ LIMIT_XDDE: self.limit_xdde,
158
+ LIMIT_QUOTA: self.limit_quota,
159
+ LIMIT_BASIC: self.limit_basic
137
160
  },
138
161
  )
139
162
  # Recargar la integración para aplicar los cambios dinámicamente
@@ -144,6 +167,9 @@ class MeteocatOptionsFlowHandler(OptionsFlow):
144
167
  schema = vol.Schema({
145
168
  vol.Required(LIMIT_XEMA, default=self._config_entry.data.get(LIMIT_XEMA)): cv.positive_int,
146
169
  vol.Required(LIMIT_PREDICCIO, default=self._config_entry.data.get(LIMIT_PREDICCIO)): cv.positive_int,
170
+ vol.Required(LIMIT_XDDE, default=self._config_entry.data.get(LIMIT_XDDE)): cv.positive_int,
171
+ vol.Required(LIMIT_QUOTA, default=self._config_entry.data.get(LIMIT_QUOTA)): cv.positive_int,
172
+ vol.Required(LIMIT_BASIC, default=self._config_entry.data.get(LIMIT_BASIC)): cv.positive_int,
147
173
  })
148
174
  return self.async_show_form(
149
175
  step_id="update_limits_only", data_schema=schema, errors=errors
@@ -60,6 +60,12 @@ from .const import (
60
60
  HOURLY_FORECAST_FILE_STATUS,
61
61
  DAILY_FORECAST_FILE_STATUS,
62
62
  UVI_FILE_STATUS,
63
+ QUOTA_FILE_STATUS,
64
+ QUOTA_XDDE,
65
+ QUOTA_PREDICCIO,
66
+ QUOTA_BASIC,
67
+ QUOTA_XEMA,
68
+ QUOTA_QUERIES,
63
69
  ALERTS,
64
70
  ALERT_FILE_STATUS,
65
71
  ALERT_WIND,
@@ -86,6 +92,11 @@ from .const import (
86
92
  DEFAULT_VALIDITY_HOURS,
87
93
  DEFAULT_VALIDITY_MINUTES,
88
94
  DEFAULT_ALERT_VALIDITY_TIME,
95
+ DEFAULT_QUOTES_VALIDITY_TIME,
96
+ ALERT_VALIDITY_MULTIPLIER_100,
97
+ ALERT_VALIDITY_MULTIPLIER_200,
98
+ ALERT_VALIDITY_MULTIPLIER_500,
99
+ ALERT_VALIDITY_MULTIPLIER_DEFAULT,
89
100
  )
90
101
 
91
102
  from .coordinator import (
@@ -99,6 +110,8 @@ from .coordinator import (
99
110
  MeteocatUviCoordinator,
100
111
  MeteocatAlertsCoordinator,
101
112
  MeteocatAlertsRegionCoordinator,
113
+ MeteocatQuotesCoordinator,
114
+ MeteocatQuotesFileCoordinator,
102
115
  )
103
116
 
104
117
  # Definir la zona horaria local
@@ -294,6 +307,12 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
294
307
  icon="mdi:update",
295
308
  entity_category=EntityCategory.DIAGNOSTIC,
296
309
  ),
310
+ MeteocatSensorEntityDescription(
311
+ key=QUOTA_FILE_STATUS,
312
+ translation_key="quota_file_status",
313
+ icon="mdi:update",
314
+ entity_category=EntityCategory.DIAGNOSTIC,
315
+ ),
297
316
  MeteocatSensorEntityDescription(
298
317
  key=ALERTS,
299
318
  translation_key="alerts",
@@ -344,6 +363,36 @@ SENSOR_TYPES: tuple[MeteocatSensorEntityDescription, ...] = (
344
363
  key=ALERT_SNOW,
345
364
  translation_key="alert_snow",
346
365
  icon="mdi:alert-outline",
366
+ ),
367
+ MeteocatSensorEntityDescription(
368
+ key=QUOTA_XDDE,
369
+ translation_key="quota_xdde",
370
+ icon="mdi:counter",
371
+ entity_category=EntityCategory.DIAGNOSTIC,
372
+ ),
373
+ MeteocatSensorEntityDescription(
374
+ key=QUOTA_PREDICCIO,
375
+ translation_key="quota_prediccio",
376
+ icon="mdi:counter",
377
+ entity_category=EntityCategory.DIAGNOSTIC,
378
+ ),
379
+ MeteocatSensorEntityDescription(
380
+ key=QUOTA_BASIC,
381
+ translation_key="quota_basic",
382
+ icon="mdi:counter",
383
+ entity_category=EntityCategory.DIAGNOSTIC,
384
+ ),
385
+ MeteocatSensorEntityDescription(
386
+ key=QUOTA_XEMA,
387
+ translation_key="quota_xema",
388
+ icon="mdi:counter",
389
+ entity_category=EntityCategory.DIAGNOSTIC,
390
+ ),
391
+ MeteocatSensorEntityDescription(
392
+ key=QUOTA_QUERIES,
393
+ translation_key="quota_queries",
394
+ icon="mdi:counter",
395
+ entity_category=EntityCategory.DIAGNOSTIC,
347
396
  )
348
397
  )
349
398
 
@@ -363,6 +412,8 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
363
412
  uvi_coordinator = entry_data.get("uvi_coordinator")
364
413
  alerts_coordinator = entry_data.get("alerts_coordinator")
365
414
  alerts_region_coordinator = entry_data.get("alerts_region_coordinator")
415
+ quotes_coordinator = entry_data.get("quotes_coordinator")
416
+ quotes_file_coordinator = entry_data.get("quotes_file_coordinator")
366
417
 
367
418
  # Sensores generales
368
419
  async_add_entities(
@@ -448,6 +499,20 @@ async def async_setup_entry(hass, entry, async_add_entities: AddEntitiesCallback
448
499
  if description.key in {ALERT_WIND, ALERT_RAIN_INTENSITY, ALERT_RAIN, ALERT_SEA, ALERT_COLD, ALERT_WARM, ALERT_WARM_NIGHT, ALERT_SNOW}
449
500
  )
450
501
 
502
+ # Sensores de estado de cuotas
503
+ async_add_entities(
504
+ MeteocatQuotaStatusSensor(quotes_coordinator, description, entry_data)
505
+ for description in SENSOR_TYPES
506
+ if description.key == QUOTA_FILE_STATUS
507
+ )
508
+
509
+ # Sensores cuotas
510
+ async_add_entities(
511
+ MeteocatQuotaSensor(quotes_file_coordinator, description, entry_data)
512
+ for description in SENSOR_TYPES
513
+ if description.key in {QUOTA_XDDE, QUOTA_PREDICCIO, QUOTA_BASIC, QUOTA_XEMA, QUOTA_QUERIES}
514
+ )
515
+
451
516
  # Cambiar UTC a la zona horaria local
452
517
  def convert_to_local_time(utc_time: str, local_tz: str = "Europe/Madrid") -> datetime | None:
453
518
  """
@@ -1136,6 +1201,7 @@ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], Se
1136
1201
  self._town_id = entry_data["town_id"]
1137
1202
  self._station_id = entry_data["station_id"]
1138
1203
  self._region_id = entry_data["region_id"]
1204
+ self._limit_prediccio = entry_data["limit_prediccio"]
1139
1205
 
1140
1206
  # Unique ID for the entity
1141
1207
  self._attr_unique_id = f"sensor.{DOMAIN}_{self._region_id}_alert_status"
@@ -1152,6 +1218,19 @@ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], Se
1152
1218
  except ValueError:
1153
1219
  _LOGGER.error("Formato de fecha de actualización inválido: %s", data_update)
1154
1220
  return None
1221
+
1222
+ def _get_validity_duration(self):
1223
+ """Calcula la duración de validez basada en el límite de predicción."""
1224
+ if self._limit_prediccio <= 100:
1225
+ multiplier = ALERT_VALIDITY_MULTIPLIER_100
1226
+ elif 100 < self._limit_prediccio <= 200:
1227
+ multiplier = ALERT_VALIDITY_MULTIPLIER_200
1228
+ elif 200 < self._limit_prediccio <= 500:
1229
+ multiplier = ALERT_VALIDITY_MULTIPLIER_500
1230
+ else:
1231
+ multiplier = ALERT_VALIDITY_MULTIPLIER_DEFAULT
1232
+
1233
+ return timedelta(minutes=DEFAULT_ALERT_VALIDITY_TIME * multiplier)
1155
1234
 
1156
1235
  @property
1157
1236
  def native_value(self):
@@ -1161,11 +1240,11 @@ class MeteocatAlertStatusSensor(CoordinatorEntity[MeteocatAlertsCoordinator], Se
1161
1240
  return "unknown"
1162
1241
 
1163
1242
  current_time = datetime.now(ZoneInfo("UTC"))
1243
+ validity_duration = self._get_validity_duration()
1164
1244
 
1165
1245
  # Comprobar si el archivo de alertas está obsoleto
1166
- if current_time - data_update >= timedelta(hours=DEFAULT_ALERT_VALIDITY_TIME):
1246
+ if current_time - data_update >= validity_duration:
1167
1247
  return "obsolete"
1168
-
1169
1248
  return "updated"
1170
1249
 
1171
1250
  @property
@@ -1370,3 +1449,165 @@ class MeteocatAlertMeteorSensor(CoordinatorEntity[MeteocatAlertsRegionCoordinato
1370
1449
  manufacturer="Meteocat",
1371
1450
  model="Meteocat API",
1372
1451
  )
1452
+
1453
+ class MeteocatQuotaStatusSensor(CoordinatorEntity[MeteocatQuotesCoordinator], SensorEntity):
1454
+ _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1455
+
1456
+ def __init__(self, quotes_coordinator, description, entry_data):
1457
+ super().__init__(quotes_coordinator)
1458
+ self.entity_description = description
1459
+ self._town_name = entry_data["town_name"]
1460
+ self._town_id = entry_data["town_id"]
1461
+ self._station_id = entry_data["station_id"]
1462
+
1463
+ # Unique ID for the entity
1464
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_quota_status"
1465
+
1466
+ # Assign entity_category if defined in the description
1467
+ self._attr_entity_category = getattr(description, "entity_category", None)
1468
+
1469
+ def _get_data_update(self):
1470
+ """Obtiene la fecha de actualización directamente desde el coordinador."""
1471
+ data_update = self.coordinator.data.get("actualizado")
1472
+ if data_update:
1473
+ try:
1474
+ return datetime.fromisoformat(data_update.rstrip("Z"))
1475
+ except ValueError:
1476
+ _LOGGER.error("Formato de fecha de actualización inválido: %s", data_update)
1477
+ return None
1478
+
1479
+ @property
1480
+ def native_value(self):
1481
+ """Devuelve el estado actual de las alertas basado en la fecha de actualización."""
1482
+ data_update = self._get_data_update()
1483
+ if not data_update:
1484
+ return "unknown"
1485
+
1486
+ current_time = datetime.now(ZoneInfo("UTC"))
1487
+
1488
+ # Comprobar si el archivo de alertas está obsoleto
1489
+ if current_time - data_update >= timedelta(minutes=DEFAULT_QUOTES_VALIDITY_TIME):
1490
+ return "obsolete"
1491
+
1492
+ return "updated"
1493
+
1494
+ @property
1495
+ def extra_state_attributes(self):
1496
+ """Devuelve los atributos adicionales del estado."""
1497
+ attributes = super().extra_state_attributes or {}
1498
+ data_update = self._get_data_update()
1499
+ if data_update:
1500
+ attributes["update_date"] = data_update.isoformat()
1501
+ return attributes
1502
+
1503
+ @property
1504
+ def device_info(self) -> DeviceInfo:
1505
+ """Devuelve la información del dispositivo."""
1506
+ return DeviceInfo(
1507
+ identifiers={(DOMAIN, self._town_id)},
1508
+ name=f"Meteocat {self._station_id} {self._town_name}",
1509
+ manufacturer="Meteocat",
1510
+ model="Meteocat API",
1511
+ )
1512
+
1513
+ class MeteocatQuotaSensor(CoordinatorEntity[MeteocatQuotesFileCoordinator], SensorEntity):
1514
+ """Representation of Meteocat Quota sensors."""
1515
+
1516
+ # Mapeo de claves en sensor.py a nombres en quotes.json
1517
+ QUOTA_MAPPING = {
1518
+ "quota_xdde": "XDDE",
1519
+ "quota_prediccio": "Prediccio",
1520
+ "quota_basic": "Basic",
1521
+ "quota_xema": "XEMA",
1522
+ "quota_queries": "Quota",
1523
+ }
1524
+
1525
+ # Mapeo de periodos para facilitar la traducción del estado
1526
+ PERIOD_STATE_MAPPING = {
1527
+ "Setmanal": "weekly",
1528
+ "Mensual": "monthly",
1529
+ "Anual": "annual",
1530
+ }
1531
+
1532
+ _attr_has_entity_name = True # Activa el uso de nombres basados en el dispositivo
1533
+
1534
+ def __init__(self, quotes_file_coordinator, description, entry_data):
1535
+ super().__init__(quotes_file_coordinator)
1536
+ self.entity_description = description
1537
+ self._town_name = entry_data["town_name"]
1538
+ self._town_id = entry_data["town_id"]
1539
+ self._station_id = entry_data["station_id"]
1540
+
1541
+ # Unique ID for the entity
1542
+ self._attr_unique_id = f"sensor.{DOMAIN}_{self._town_id}_{self.entity_description.key}"
1543
+
1544
+ # Assign entity_category if defined in the description
1545
+ self._attr_entity_category = getattr(description, "entity_category", None)
1546
+
1547
+ def _get_plan_data(self):
1548
+ """Encuentra los datos del plan correspondiente al sensor actual."""
1549
+ if not self.coordinator.data:
1550
+ return None
1551
+
1552
+ plan_name = self.QUOTA_MAPPING.get(self.entity_description.key)
1553
+
1554
+ if not plan_name:
1555
+ _LOGGER.error(f"No se encontró un mapeo para la clave: {self.entity_description.key}")
1556
+ return None
1557
+
1558
+ for plan in self.coordinator.data.get("plans", []):
1559
+ if plan.get("nom") == plan_name:
1560
+ return plan # Retorna el plan encontrado
1561
+
1562
+ _LOGGER.warning(f"No se encontró el plan '{plan_name}' en los datos del coordinador.")
1563
+ return None
1564
+
1565
+ @property
1566
+ def native_value(self):
1567
+ """Devuelve el estado de la cuota: 'ok' si no se ha excedido, 'exceeded' si se ha superado."""
1568
+ plan = self._get_plan_data()
1569
+
1570
+ if not plan:
1571
+ return None
1572
+
1573
+ max_consultes = plan.get("maxConsultes")
1574
+ consultes_realitzades = plan.get("consultesRealitzades")
1575
+
1576
+ if max_consultes is None or consultes_realitzades is None or \
1577
+ not isinstance(max_consultes, (int, float)) or not isinstance(consultes_realitzades, (int, float)):
1578
+ _LOGGER.warning(f"Datos inválidos para el plan '{plan.get('nom', 'unknown')}': {plan}")
1579
+ return None
1580
+
1581
+ return "ok" if consultes_realitzades <= max_consultes else "exceeded"
1582
+
1583
+ @property
1584
+ def extra_state_attributes(self):
1585
+ """Devuelve atributos adicionales del estado del sensor."""
1586
+ attributes = super().extra_state_attributes or {}
1587
+ plan = self._get_plan_data()
1588
+
1589
+ if not plan:
1590
+ return {}
1591
+
1592
+ # Aplicar el mapeo de periodos
1593
+ period = plan.get("periode", "desconocido")
1594
+ translated_period = self.PERIOD_STATE_MAPPING.get(period, period) # Si no está en el mapping, dejar el original
1595
+
1596
+ attributes.update({
1597
+ "period": translated_period,
1598
+ "max_queries": plan.get("maxConsultes"),
1599
+ "made_queries": plan.get("consultesRealitzades"),
1600
+ "remain_queries": plan.get("consultesRestants"),
1601
+ })
1602
+
1603
+ return attributes
1604
+
1605
+ @property
1606
+ def device_info(self) -> DeviceInfo:
1607
+ """Devuelve la información del dispositivo."""
1608
+ return DeviceInfo(
1609
+ identifiers={(DOMAIN, self._town_id)},
1610
+ name=f"Meteocat {self._station_id} {self._town_name}",
1611
+ manufacturer="Meteocat",
1612
+ model="Meteocat API",
1613
+ )
@@ -20,7 +20,7 @@
20
20
  }
21
21
  },
22
22
  "set_api_limits": {
23
- "description": "Set the API limits for XEMA and PREDICTIONS.",
23
+ "description": "Set the API limits.",
24
24
  "title": "API limits"
25
25
  }
26
26
  },
@@ -36,18 +36,18 @@
36
36
  "options": {
37
37
  "step":{
38
38
  "init": {
39
- "description": "Setup the API Key and the limits for XEMA and PREDICTIONS.",
39
+ "description": "Setup the API Key and the API limits.",
40
40
  "title": "Setup options",
41
41
  "data": {
42
42
  "option": "Options"
43
43
  }
44
44
  },
45
45
  "update_api_and_limits": {
46
- "description": "Setup the API Key and the limits for XEMA and PREDICTIONS.",
46
+ "description": "Setup the API Key and the API limits.",
47
47
  "title": "API Key and limits"
48
48
  },
49
49
  "update_limits_only": {
50
- "description": "Setup the limits for XEMA and PREDICTIONS.",
50
+ "description": "Setup the API limits.",
51
51
  "title": "API limits"
52
52
  }
53
53
  },
@@ -225,6 +225,153 @@
225
225
  }
226
226
  }
227
227
  },
228
+ "quota_file_status": {
229
+ "name": "Quota File",
230
+ "state": {
231
+ "updated": "Updated",
232
+ "obsolete": "Obsolete"
233
+ },
234
+ "state_attributes": {
235
+ "update_date": {
236
+ "name": "Date"
237
+ }
238
+ }
239
+ },
240
+ "quota_xdde": {
241
+ "name": "Quota XDDE",
242
+ "state": {
243
+ "ok": "Ok",
244
+ "exceeded": "Exceeded",
245
+ "unknown": "Unknown"
246
+ },
247
+ "state_attributes": {
248
+ "period": {
249
+ "name": "Period",
250
+ "state": {
251
+ "weekly": "Weekly",
252
+ "monthly": "Monthly",
253
+ "annual": "Annual"
254
+ }
255
+ },
256
+ "max_queries": {
257
+ "name": "Max Queries"
258
+ },
259
+ "remain_queries": {
260
+ "name": "Remaining"
261
+ },
262
+ "made_queries": {
263
+ "name": "Made"
264
+ }
265
+ }
266
+ },
267
+ "quota_prediccio": {
268
+ "name": "Quota Prediction",
269
+ "state": {
270
+ "ok": "Ok",
271
+ "exceeded": "Exceeded",
272
+ "unknown": "Unknown"
273
+ },
274
+ "state_attributes": {
275
+ "period": {
276
+ "name": "Period",
277
+ "state": {
278
+ "weekly": "Weekly",
279
+ "monthly": "Monthly",
280
+ "annual": "Annual"
281
+ }
282
+ },
283
+ "max_queries": {
284
+ "name": "Max Queries"
285
+ },
286
+ "remain_queries": {
287
+ "name": "Remaining"
288
+ },
289
+ "made_queries": {
290
+ "name": "Made"
291
+ }
292
+ }
293
+ },
294
+ "quota_basic": {
295
+ "name": "Quota Basic",
296
+ "state": {
297
+ "ok": "Ok",
298
+ "exceeded": "Exceeded",
299
+ "unknown": "Unknown"
300
+ },
301
+ "state_attributes": {
302
+ "period": {
303
+ "name": "Period",
304
+ "state": {
305
+ "weekly": "Weekly",
306
+ "monthly": "Monthly",
307
+ "annual": "Annual"
308
+ }
309
+ },
310
+ "max_queries": {
311
+ "name": "Max Queries"
312
+ },
313
+ "remain_queries": {
314
+ "name": "Remaining"
315
+ },
316
+ "made_queries": {
317
+ "name": "Made"
318
+ }
319
+ }
320
+ },
321
+ "quota_xema": {
322
+ "name": "Quota XEMA",
323
+ "state": {
324
+ "ok": "Ok",
325
+ "exceeded": "Exceeded",
326
+ "unknown": "Unknown"
327
+ },
328
+ "state_attributes": {
329
+ "period": {
330
+ "name": "Period",
331
+ "state": {
332
+ "weekly": "Weekly",
333
+ "monthly": "Monthly",
334
+ "annual": "Annual"
335
+ }
336
+ },
337
+ "max_queries": {
338
+ "name": "Max Queries"
339
+ },
340
+ "remain_queries": {
341
+ "name": "Remaining"
342
+ },
343
+ "made_queries": {
344
+ "name": "Made"
345
+ }
346
+ }
347
+ },
348
+ "quota_queries": {
349
+ "name": "Quota Queries",
350
+ "state": {
351
+ "ok": "Ok",
352
+ "exceeded": "Exceeded",
353
+ "unknown": "Unknown"
354
+ },
355
+ "state_attributes": {
356
+ "period": {
357
+ "name": "Period",
358
+ "state": {
359
+ "weekly": "Weekly",
360
+ "monthly": "Monthly",
361
+ "annual": "Annual"
362
+ }
363
+ },
364
+ "max_queries": {
365
+ "name": "Max Queries"
366
+ },
367
+ "remain_queries": {
368
+ "name": "Remaining"
369
+ },
370
+ "made_queries": {
371
+ "name": "Made"
372
+ }
373
+ }
374
+ },
228
375
  "alerts": {
229
376
  "name": "Alerts",
230
377
  "state_attributes": {