1
1
"""Coordinator to handle IEC connections."""
2
+ import calendar
2
3
import itertools
3
4
import logging
4
5
import socket
5
6
from datetime import datetime , timedelta
6
7
from typing import cast , Any # noqa: UP035
8
+ from collections import Counter
7
9
8
10
import pytz
9
11
from homeassistant .components .recorder import get_instance
21
23
from homeassistant .helpers .update_coordinator import DataUpdateCoordinator
22
24
from iec_api .iec_client import IecClient
23
25
from iec_api .models .contract import Contract
24
- from iec_api .models .device import Device
26
+ from iec_api .models .device import Device , Devices
25
27
from iec_api .models .exceptions import IECError
26
28
from iec_api .models .jwt import JWT
27
29
from iec_api .models .remote_reading import ReadingResolution , RemoteReading , FutureConsumptionInfo , RemoteReadingResponse
30
32
from .const import DOMAIN , CONF_USER_ID , STATICS_DICT_NAME , STATIC_KWH_TARIFF , INVOICE_DICT_NAME , \
31
33
FUTURE_CONSUMPTIONS_DICT_NAME , DAILY_READINGS_DICT_NAME , STATIC_BP_NUMBER , ILS , CONF_BP_NUMBER , \
32
34
CONF_SELECTED_CONTRACTS , CONTRACT_DICT_NAME , EMPTY_INVOICE , ELECTRIC_INVOICE_DOC_ID , ATTRIBUTES_DICT_NAME , \
33
- CONTRACT_ID_ATTR_NAME , IS_SMART_METER_ATTR_NAME , METER_ID_ATTR_NAME
35
+ CONTRACT_ID_ATTR_NAME , IS_SMART_METER_ATTR_NAME , METER_ID_ATTR_NAME , STATIC_KVA_TARIFF , ESTIMATED_BILL_DICT_NAME , \
36
+ TOTAL_EST_BILL_ATTR_NAME , EST_BILL_DAYS_ATTR_NAME , EST_BILL_CONSUMPTION_PRICE_ATTR_NAME , \
37
+ EST_BILL_DELIVERY_PRICE_ATTR_NAME , EST_BILL_DISTRIBUTION_PRICE_ATTR_NAME , EST_BILL_TOTAL_KVA_PRICE_ATTR_NAME , \
38
+ EST_BILL_KWH_CONSUMPTION_ATTR_NAME
34
39
35
40
_LOGGER = logging .getLogger (__name__ )
36
41
TIMEZONE = pytz .timezone ("Asia/Jerusalem" )
@@ -59,7 +64,12 @@ def __init__(
59
64
self ._entry_data = config_entry .data
60
65
self ._today_readings = {}
61
66
self ._devices_by_contract_id = {}
67
+ self ._devices_by_meter_id = {}
68
+ self ._delivery_tariff_by_pahse = {}
69
+ self ._distribution_tariff_by_pahse = {}
70
+ self ._power_size_by_connection_size = {}
62
71
self ._kwh_tariff : float | None = None
72
+ self ._kva_tariff : float | None = None
63
73
self ._readings = {}
64
74
self .api = IecClient (
65
75
self ._entry_data [CONF_USER_ID ],
@@ -87,6 +97,16 @@ async def _get_devices_by_contract_id(self, contract_id) -> list[Device]:
87
97
_LOGGER .exception (f"Failed fetching devices by contract { contract_id } " , e )
88
98
return devices
89
99
100
+ async def _get_devices_by_device_id (self , meter_id ) -> Devices :
101
+ devices = self ._devices_by_meter_id .get (meter_id )
102
+ if not devices :
103
+ try :
104
+ devices = await self .api .get_device_by_device_id (str (meter_id ))
105
+ self ._devices_by_meter_id [meter_id ] = devices
106
+ except IECError as e :
107
+ _LOGGER .exception (f"Failed fetching device details by meter id { meter_id } " , e )
108
+ return devices
109
+
90
110
async def _get_kwh_tariff (self ) -> float :
91
111
if not self ._kwh_tariff :
92
112
try :
@@ -95,6 +115,44 @@ async def _get_kwh_tariff(self) -> float:
95
115
_LOGGER .exception ("Failed fetching kWh Tariff" , e )
96
116
return self ._kwh_tariff or 0.0
97
117
118
+ async def _get_kva_tariff (self ) -> float :
119
+ if not self ._kva_tariff :
120
+ try :
121
+ self ._kva_tariff = await self .api .get_kva_tariff ()
122
+ except IECError as e :
123
+ _LOGGER .exception ("Failed fetching KVA Tariff" , e )
124
+ return self ._kva_tariff or 0.0
125
+
126
+ async def _get_delivery_tariff (self , phase ) -> float :
127
+ delivery_tariff = self ._delivery_tariff_by_pahse .get (phase )
128
+ if not delivery_tariff :
129
+ try :
130
+ delivery_tariff = await self .api .get_delivery_tariff (phase )
131
+ self ._delivery_tariff_by_pahse [phase ] = delivery_tariff
132
+ except IECError as e :
133
+ _LOGGER .exception (f"Failed fetching Delivery Tariff by phase { phase } " , e )
134
+ return delivery_tariff or 0.0
135
+
136
+ async def _get_distribution_tariff (self , phase ) -> float :
137
+ distribution_tariff = self ._distribution_tariff_by_pahse .get (phase )
138
+ if not distribution_tariff :
139
+ try :
140
+ distribution_tariff = await self .api .get_distribution_tariff (phase )
141
+ self ._distribution_tariff_by_pahse [phase ] = distribution_tariff
142
+ except IECError as e :
143
+ _LOGGER .exception (f"Failed fetching Distribution Tariff by phase { phase } " , e )
144
+ return distribution_tariff or 0.0
145
+
146
+ async def _get_power_size (self , connection_size ) -> float :
147
+ power_size = self ._power_size_by_connection_size .get (connection_size )
148
+ if not power_size :
149
+ try :
150
+ power_size = await self .api .get_power_size (connection_size )
151
+ self ._power_size_by_connection_size [connection_size ] = power_size
152
+ except IECError as e :
153
+ _LOGGER .exception (f"Failed fetching Power Size by Connection Size { connection_size } " , e )
154
+ return power_size or 0.0
155
+
98
156
async def _get_readings (self , contract_id : int , device_id : str | int , device_code : str | int , date : datetime ,
99
157
resolution : ReadingResolution ):
100
158
@@ -191,13 +249,17 @@ async def _async_update_data(
191
249
contracts : dict [int , Contract ] = {int (c .contract_id ): c for c in all_contracts if c .status == 1
192
250
and int (c .contract_id ) in self ._contract_ids }
193
251
localized_today = TIMEZONE .localize (datetime .today ())
194
- tariff = await self ._get_kwh_tariff ()
252
+ kwh_tariff = await self ._get_kwh_tariff ()
253
+ kva_tariff = await self ._get_kva_tariff ()
195
254
196
255
data = {STATICS_DICT_NAME : {
197
- STATIC_KWH_TARIFF : tariff ,
256
+ STATIC_KWH_TARIFF : kwh_tariff ,
257
+ STATIC_KVA_TARIFF : kva_tariff ,
198
258
STATIC_BP_NUMBER : self ._bp_number
199
259
}}
200
260
261
+ estimated_bill_dict = None
262
+
201
263
_LOGGER .debug (f"All Contract Ids: { list (contracts .keys ())} " )
202
264
203
265
for contract_id in self ._contract_ids :
@@ -311,12 +373,41 @@ async def _async_update_data(
311
373
else :
312
374
_LOGGER .debug ("Failed fetching FutureConsumption, data in IEC API is corrupted" )
313
375
376
+ devices_by_id : Devices = await self ._get_devices_by_device_id (device .device_number )
377
+ last_meter_read = int (devices_by_id .counter_devices [0 ].last_mr )
378
+ last_meter_read_date = devices_by_id .counter_devices [0 ].last_mr_date
379
+ phase_count = devices_by_id .counter_devices [0 ].connection_size .phase
380
+ connection_size = (devices_by_id .counter_devices [0 ].
381
+ connection_size .representative_connection_size )
382
+
383
+ distribution_tariff = await self ._get_distribution_tariff (phase_count )
384
+ delivery_tariff = await self ._get_delivery_tariff (phase_count )
385
+ power_size = await self ._get_power_size (connection_size )
386
+
387
+ estimated_bill , fixed_price , consumption_price , total_days , delivery_price , distribution_price , \
388
+ total_kva_price , estimated_kwh_consumption = (
389
+ self ._calculate_estimated_bill (device .device_number , future_consumption ,
390
+ last_meter_read , last_meter_read_date ,
391
+ kwh_tariff , kva_tariff , distribution_tariff ,
392
+ delivery_tariff , power_size , last_invoice ))
393
+
394
+ estimated_bill_dict = {
395
+ TOTAL_EST_BILL_ATTR_NAME : estimated_bill ,
396
+ EST_BILL_DAYS_ATTR_NAME : total_days ,
397
+ EST_BILL_CONSUMPTION_PRICE_ATTR_NAME : consumption_price ,
398
+ EST_BILL_DELIVERY_PRICE_ATTR_NAME : delivery_price ,
399
+ EST_BILL_DISTRIBUTION_PRICE_ATTR_NAME : distribution_price ,
400
+ EST_BILL_TOTAL_KVA_PRICE_ATTR_NAME : total_kva_price ,
401
+ EST_BILL_KWH_CONSUMPTION_ATTR_NAME : estimated_kwh_consumption
402
+ }
403
+
314
404
data [str (contract_id )] = {CONTRACT_DICT_NAME : contracts .get (contract_id ),
315
405
INVOICE_DICT_NAME : last_invoice ,
316
406
FUTURE_CONSUMPTIONS_DICT_NAME : future_consumption ,
317
407
DAILY_READINGS_DICT_NAME : daily_readings ,
318
- STATICS_DICT_NAME : {STATIC_KWH_TARIFF : tariff }, # workaround,
319
- ATTRIBUTES_DICT_NAME : attributes_to_add
408
+ STATICS_DICT_NAME : {STATIC_KWH_TARIFF : kwh_tariff }, # workaround,
409
+ ATTRIBUTES_DICT_NAME : attributes_to_add ,
410
+ ESTIMATED_BILL_DICT_NAME : estimated_bill_dict
320
411
}
321
412
322
413
# Clean up for next cycle
@@ -493,3 +584,57 @@ async def _insert_statistics(self, contract_id: int, is_smart_meter: bool) -> No
493
584
async_add_external_statistics (
494
585
self .hass , cost_metadata , cost_statistics
495
586
)
587
+
588
+ @staticmethod
589
+ def _calculate_estimated_bill (meter_id , future_consumptions : dict [str , FutureConsumptionInfo | None ],
590
+ last_meter_read , last_meter_read_date , kwh_tariff ,
591
+ kva_tariff , distribution_tariff , delivery_tariff , power_size , last_invoice ):
592
+ future_consumption_info : FutureConsumptionInfo = future_consumptions [meter_id ]
593
+ future_consumption = future_consumption_info .total_import - last_meter_read
594
+
595
+ kva_price = power_size * kva_tariff / 365
596
+
597
+ total_kva_price = 0
598
+ distribution_price = 0
599
+ delivery_price = 0
600
+
601
+ consumption_price = future_consumption * kwh_tariff
602
+ total_days = 0
603
+
604
+ today = TIMEZONE .localize (datetime .today ())
605
+
606
+ if last_invoice != EMPTY_INVOICE :
607
+ current_date = last_meter_read_date + timedelta (days = 1 )
608
+ month_counter = Counter ()
609
+
610
+ while current_date <= today .date ():
611
+ # Use (year, month) as the key for counting
612
+ month_year = (current_date .year , current_date .month )
613
+ month_counter [month_year ] += 1
614
+
615
+ # Move to the next day
616
+ current_date += timedelta (days = 1 )
617
+
618
+ for (year , month ), days in month_counter .items ():
619
+ days_in_month = calendar .monthrange (year , month )[1 ]
620
+ total_kva_price += kva_price * days
621
+ distribution_price += (distribution_tariff / days_in_month ) * days
622
+ delivery_price += (delivery_tariff / days_in_month ) * days
623
+ total_days += days
624
+ else :
625
+ total_days = today .day
626
+ days_in_current_month = calendar .monthrange (today .year , today .month )[1 ]
627
+
628
+ consumption_price = future_consumption * kwh_tariff
629
+ total_kva_price = kva_price * total_days
630
+ distribution_price = (distribution_tariff / days_in_current_month ) * total_days
631
+ delivery_price = (delivery_tariff / days_in_current_month ) * total_days
632
+
633
+ _LOGGER .debug (f'Calculated estimated bill: No. of days: { total_days } , total KVA price: { total_kva_price } , '
634
+ f'total distribution price: { distribution_price } , total delivery price: { delivery_price } , '
635
+ f'consumption price: { consumption_price } ' )
636
+
637
+ fixed_price = total_kva_price + distribution_price + delivery_price
638
+ total_estimated_bill = consumption_price + fixed_price
639
+ return total_estimated_bill , fixed_price , consumption_price , total_days , \
640
+ delivery_price , distribution_price , total_kva_price , future_consumption
0 commit comments