Skip to content

Commit 4644c83

Browse files
authored
feat: Add diagnostic sensors (#259)
1 parent 3a52e42 commit 4644c83

File tree

7 files changed

+105
-34
lines changed

7 files changed

+105
-34
lines changed

custom_components/iec/binary_sensor.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
DOMAIN,
2121
STATICS_DICT_NAME,
2222
INVOICE_DICT_NAME,
23+
JWT_DICT_NAME,
2324
EMPTY_INVOICE,
2425
ATTRIBUTES_DICT_NAME,
2526
METER_ID_ATTR_NAME,
@@ -67,7 +68,8 @@ async def async_setup_entry(
6768
len(
6869
list(
6970
filter(
70-
lambda key: key != STATICS_DICT_NAME, list(coordinator.data.keys())
71+
lambda key: key not in (STATICS_DICT_NAME, JWT_DICT_NAME),
72+
list(coordinator.data.keys()),
7173
)
7274
)
7375
)
@@ -76,7 +78,7 @@ async def async_setup_entry(
7678

7779
entities: list[BinarySensorEntity] = []
7880
for contract_key in coordinator.data:
79-
if contract_key == STATICS_DICT_NAME:
81+
if contract_key in (STATICS_DICT_NAME, JWT_DICT_NAME):
8082
continue
8183

8284
for description in BINARY_SENSORS:

custom_components/iec/const.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
CONF_SELECTED_CONTRACTS = "selected_contracts"
4848
CONF_AVAILABLE_CONTRACTS = "contracts"
4949
CONF_MAIN_CONTRACT_ID = "main_contract_id"
50+
JWT_DICT_NAME = "jwt"
5051
STATICS_DICT_NAME = "statics"
5152
ATTRIBUTES_DICT_NAME = "entity_attributes"
5253
ESTIMATED_BILL_DICT_NAME = "estimated_bill"
@@ -68,3 +69,5 @@
6869
STATIC_KVA_TARIFF = "kva_tariff"
6970
STATIC_BP_NUMBER = "bp_number"
7071
ELECTRIC_INVOICE_DOC_ID = "1"
72+
ACCESS_TOKEN_ISSUED_AT = "iat"
73+
ACCESS_TOKEN_EXPIRATION_TIME = "exp"

custom_components/iec/coordinator.py

Lines changed: 34 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import calendar
44
import itertools
5+
import jwt
56
import logging
67
import traceback
78
import socket
@@ -41,6 +42,7 @@
4142
DOMAIN,
4243
CONF_USER_ID,
4344
STATICS_DICT_NAME,
45+
JWT_DICT_NAME,
4446
STATIC_KWH_TARIFF,
4547
INVOICE_DICT_NAME,
4648
FUTURE_CONSUMPTIONS_DICT_NAME,
@@ -65,6 +67,8 @@
6567
EST_BILL_DISTRIBUTION_PRICE_ATTR_NAME,
6668
EST_BILL_TOTAL_KVA_PRICE_ATTR_NAME,
6769
EST_BILL_KWH_CONSUMPTION_ATTR_NAME,
70+
ACCESS_TOKEN_ISSUED_AT,
71+
ACCESS_TOKEN_EXPIRATION_TIME,
6872
)
6973

7074
_LOGGER = logging.getLogger(__name__)
@@ -317,7 +321,7 @@ async def _verify_daily_readings_exist(
317321
)
318322
if not daily_reading:
319323
_LOGGER.debug(
320-
f'Daily reading for date: {desired_date.strftime("%Y-%m-%d")} is missing, calculating manually'
324+
f"Daily reading for date: {desired_date.strftime('%Y-%m-%d')} is missing, calculating manually"
321325
)
322326
readings = prefetched_reading
323327
if not readings:
@@ -330,7 +334,7 @@ async def _verify_daily_readings_exist(
330334
)
331335
else:
332336
_LOGGER.debug(
333-
f'Daily reading for date: {desired_date.strftime("%Y-%m-%d")} - using existing prefetched readings'
337+
f"Daily reading for date: {desired_date.strftime('%Y-%m-%d')} - using existing prefetched readings"
334338
)
335339

336340
if readings and readings.data:
@@ -353,16 +357,16 @@ async def _verify_daily_readings_exist(
353357
)
354358
if desired_date_reading is None or desired_date_reading.value <= 0:
355359
_LOGGER.debug(
356-
f'Couldn\'t find daily reading for: {desired_date.strftime("%Y-%m-%d")}'
360+
f"Couldn't find daily reading for: {desired_date.strftime('%Y-%m-%d')}"
357361
)
358362
else:
359363
daily_readings[device.device_number].append(
360364
RemoteReading(0, desired_date, desired_date_reading.value)
361365
)
362366
else:
363367
_LOGGER.debug(
364-
f'Daily reading for date: {daily_reading.date.strftime("%Y-%m-%d")}'
365-
f' is present: {daily_reading.value}'
368+
f"Daily reading for date: {daily_reading.date.strftime('%Y-%m-%d')}"
369+
f" is present: {daily_reading.value}"
366370
)
367371

368372
async def _update_data(
@@ -390,12 +394,21 @@ async def _update_data(
390394
kwh_tariff = await self._get_kwh_tariff()
391395
kva_tariff = await self._get_kva_tariff()
392396

397+
access_token = self.api.get_token().access_token
398+
decoded_token = jwt.decode(access_token, options={"verify_signature": False})
399+
access_token_issued_at = decoded_token["iat"]
400+
access_token_expiration_time = decoded_token["exp"]
401+
393402
data = {
403+
JWT_DICT_NAME: {
404+
ACCESS_TOKEN_ISSUED_AT: access_token_issued_at,
405+
ACCESS_TOKEN_EXPIRATION_TIME: access_token_expiration_time,
406+
},
394407
STATICS_DICT_NAME: {
395408
STATIC_KWH_TARIFF: kwh_tariff,
396409
STATIC_KVA_TARIFF: kva_tariff,
397410
STATIC_BP_NUMBER: self._bp_number,
398-
}
411+
},
399412
}
400413

401414
estimated_bill_dict = None
@@ -454,7 +467,9 @@ async def _update_data(
454467

455468
devices = await self._get_devices_by_contract_id(contract_id)
456469
if not devices:
457-
_LOGGER.debug(f"No devices for contract {contract_id}. Skipping creating devices.")
470+
_LOGGER.debug(
471+
f"No devices for contract {contract_id}. Skipping creating devices."
472+
)
458473
continue
459474

460475
for device in devices or []:
@@ -705,7 +720,14 @@ async def _insert_statistics(self, contract_id: int, is_smart_meter: bool) -> No
705720

706721
if readings and readings.meter_start_date:
707722
# Fetching the last reading from either the installation date or a month ago
708-
month_ago_time = max(month_ago_time, TIMEZONE.localize(datetime.combine(readings.meter_start_date, datetime.min.time())))
723+
month_ago_time = max(
724+
month_ago_time,
725+
TIMEZONE.localize(
726+
datetime.combine(
727+
readings.meter_start_date, datetime.min.time()
728+
)
729+
),
730+
)
709731
else:
710732
_LOGGER.debug(
711733
"[IEC Statistics] Failed to extract field `meterStartDate`, falling back to a month ago"
@@ -1016,8 +1038,10 @@ def _calculate_estimated_bill(
10161038
future_consumption_info.total_import - last_meter_read
10171039
)
10181040
else:
1019-
_LOGGER.warn(f"Failed to calculate Future Consumption, Assuming last meter read \
1020-
({last_meter_read}) as full consumption")
1041+
_LOGGER.warn(
1042+
f"Failed to calculate Future Consumption, Assuming last meter read \
1043+
({last_meter_read}) as full consumption"
1044+
)
10211045
future_consumption = last_meter_read
10221046

10231047
kva_price = power_size * kva_tariff / 365

custom_components/iec/iec_entity.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,7 @@ def __init__(
2424
self.meter_id = meter_id
2525
self.iec_entity_type = iec_entity_type
2626
self._attr_device_info = get_device_info(
27-
self.contract_id, self.meter_id, self.iec_entity_type
27+
self.contract_id if iec_entity_type != IecEntityType.GENERIC else "Generic",
28+
self.meter_id,
29+
self.iec_entity_type,
2830
)

custom_components/iec/sensor.py

Lines changed: 50 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import logging
66
from collections.abc import Callable
77
from dataclasses import dataclass
8-
from datetime import datetime, timedelta, date
8+
from datetime import date, datetime, timedelta
99

1010
from homeassistant.components.sensor import (
1111
SensorDeviceClass,
@@ -14,36 +14,39 @@
1414
SensorStateClass,
1515
)
1616
from homeassistant.config_entries import ConfigEntry
17-
from homeassistant.const import UnitOfEnergy, UnitOfTime
17+
from homeassistant.const import EntityCategory, UnitOfEnergy, UnitOfTime
1818
from homeassistant.core import HomeAssistant
1919
from homeassistant.helpers.entity_platform import AddEntitiesCallback
2020
from homeassistant.helpers.typing import StateType
2121
from iec_api.models.invoice import Invoice
2222
from iec_api.models.remote_reading import RemoteReading
2323

24-
from .commons import find_reading_by_date, IecEntityType, TIMEZONE
24+
from .commons import TIMEZONE, IecEntityType, find_reading_by_date
2525
from .const import (
26-
DOMAIN,
27-
ILS,
28-
STATICS_DICT_NAME,
29-
STATIC_KWH_TARIFF,
30-
FUTURE_CONSUMPTIONS_DICT_NAME,
31-
INVOICE_DICT_NAME,
32-
ILS_PER_KWH,
33-
DAILY_READINGS_DICT_NAME,
34-
EMPTY_REMOTE_READING,
26+
ACCESS_TOKEN_EXPIRATION_TIME,
27+
ACCESS_TOKEN_ISSUED_AT,
28+
ATTRIBUTES_DICT_NAME,
3529
CONTRACT_DICT_NAME,
30+
DAILY_READINGS_DICT_NAME,
31+
DOMAIN,
3632
EMPTY_INVOICE,
37-
ATTRIBUTES_DICT_NAME,
38-
METER_ID_ATTR_NAME,
39-
ESTIMATED_BILL_DICT_NAME,
40-
TOTAL_EST_BILL_ATTR_NAME,
41-
EST_BILL_DAYS_ATTR_NAME,
33+
EMPTY_REMOTE_READING,
4234
EST_BILL_CONSUMPTION_PRICE_ATTR_NAME,
35+
EST_BILL_DAYS_ATTR_NAME,
4336
EST_BILL_DELIVERY_PRICE_ATTR_NAME,
4437
EST_BILL_DISTRIBUTION_PRICE_ATTR_NAME,
45-
EST_BILL_TOTAL_KVA_PRICE_ATTR_NAME,
4638
EST_BILL_KWH_CONSUMPTION_ATTR_NAME,
39+
EST_BILL_TOTAL_KVA_PRICE_ATTR_NAME,
40+
ESTIMATED_BILL_DICT_NAME,
41+
FUTURE_CONSUMPTIONS_DICT_NAME,
42+
ILS,
43+
ILS_PER_KWH,
44+
INVOICE_DICT_NAME,
45+
JWT_DICT_NAME,
46+
METER_ID_ATTR_NAME,
47+
STATIC_KWH_TARIFF,
48+
STATICS_DICT_NAME,
49+
TOTAL_EST_BILL_ATTR_NAME,
4750
)
4851
from .coordinator import IecApiCoordinator
4952
from .iec_entity import IecEntity
@@ -120,6 +123,24 @@ def _get_reading_by_date(
120123
return EMPTY_REMOTE_READING
121124

122125

126+
DIAGNOSTICS_SENSORS: tuple[IecEntityDescription, ...] = (
127+
IecEntityDescription(
128+
key="access_token_expiry_time",
129+
device_class=SensorDeviceClass.TIMESTAMP,
130+
entity_category=EntityCategory.DIAGNOSTIC,
131+
value_fn=lambda data: datetime.fromtimestamp(
132+
data[ACCESS_TOKEN_EXPIRATION_TIME], tz=TIMEZONE
133+
),
134+
),
135+
IecEntityDescription(
136+
key="access_token_issued_at",
137+
device_class=SensorDeviceClass.TIMESTAMP,
138+
entity_category=EntityCategory.DIAGNOSTIC,
139+
value_fn=lambda data: datetime.fromtimestamp(
140+
data[ACCESS_TOKEN_ISSUED_AT], tz=TIMEZONE
141+
),
142+
),
143+
)
123144
SMART_ELEC_SENSORS: tuple[IecEntityDescription, ...] = (
124145
IecMeterEntityDescription(
125146
key="elec_forecasted_usage",
@@ -350,6 +371,16 @@ async def async_setup_entry(
350371
is_multi_contract=False,
351372
)
352373
)
374+
elif contract_key == JWT_DICT_NAME:
375+
for sensor_desc in DIAGNOSTICS_SENSORS:
376+
entities.append(
377+
IecSensor(
378+
coordinator,
379+
sensor_desc,
380+
JWT_DICT_NAME,
381+
is_multi_contract=False,
382+
)
383+
)
353384
else:
354385
if coordinator.data[contract_key][CONTRACT_DICT_NAME].smart_meter:
355386
sensors_desc: tuple[IecEntityDescription, ...] = (
@@ -425,7 +456,7 @@ def __init__(
425456
def native_value(self) -> StateType:
426457
"""Return the state."""
427458
if self.coordinator.data is not None:
428-
if self.contract_id == STATICS_DICT_NAME:
459+
if self.contract_id in (STATICS_DICT_NAME, JWT_DICT_NAME):
429460
return self.entity_description.value_fn(
430461
self.coordinator.data.get(self.contract_id, self.meter_id)
431462
)

custom_components/iec/translations/en.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@
66
}
77
},
88
"sensor": {
9-
"elec_forecasted_usage": {
10-
"name": "Next electric bill forecasted usage {multi_contract}"
9+
"access_token_expiry_time": {
10+
"name": "Access Token Expiry Time"
11+
},
12+
"access_token_issued_at": {
13+
"name": "Access Token Issued At"
1114
},
1215
"elec_forecasted_cost": {
1316
"name": "Next electric bill forecasted cost {multi_contract}",

custom_components/iec/translations/he.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,12 @@
66
}
77
},
88
"sensor": {
9+
"access_token_expiry_time": {
10+
"name": "תאריך פג התוקף של טוקן הגישה"
11+
},
12+
"access_token_issued_at": {
13+
"name": "תאריך יצירת טוקן הגישה"
14+
},
915
"elec_forecasted_usage": {
1016
"name": "סך צריכת החשמל בחשבונית הבאה {multi_contract}"
1117
},

0 commit comments

Comments
 (0)