From e80df7f0abbed939894d5da543f8c466eea06243 Mon Sep 17 00:00:00 2001 From: Chau Truong Thinh Date: Sun, 1 Dec 2024 19:00:52 +0700 Subject: [PATCH 1/3] EVNSPC: Fix Error fetching data --- custom_components/nestup_evn/config_flow.py | 1 + .../nestup_evn/evn_branches.json | 1 + custom_components/nestup_evn/nestup_evn.py | 91 +++++++------------ custom_components/nestup_evn/types.py | 2 +- 4 files changed, 36 insertions(+), 59 deletions(-) diff --git a/custom_components/nestup_evn/config_flow.py b/custom_components/nestup_evn/config_flow.py index 3a340d6..4f97666 100644 --- a/custom_components/nestup_evn/config_flow.py +++ b/custom_components/nestup_evn/config_flow.py @@ -165,6 +165,7 @@ async def _try_auth(self): self._user_data.get(CONF_AREA), self._user_data.get(CONF_USERNAME), self._user_data.get(CONF_PASSWORD), + self._user_data.get(CONF_CUSTOMER_ID), ) except Exception as e: _LOGGER.exception(f"Unexpected exception: {e}") diff --git a/custom_components/nestup_evn/evn_branches.json b/custom_components/nestup_evn/evn_branches.json index 923c4e5..457fadb 100644 --- a/custom_components/nestup_evn/evn_branches.json +++ b/custom_components/nestup_evn/evn_branches.json @@ -60,6 +60,7 @@ "PB0804":"Điện Lực Gò Công Tây", "PB0805":"Điện Lực Cai Lậy", "PB0901":"Điện Lực Bến Tre", + "PB0905":"Điện Lực Bến Tre", "PB0906":"Điện Lực Châu Thành", "PB1001":"Điện Lực Vĩnh Long", "PB1002":"Điện Lực Trà Ôn", diff --git a/custom_components/nestup_evn/nestup_evn.py b/custom_components/nestup_evn/nestup_evn.py index 88f905d..d43abb0 100644 --- a/custom_components/nestup_evn/nestup_evn.py +++ b/custom_components/nestup_evn/nestup_evn.py @@ -8,7 +8,6 @@ import os import ssl from typing import Any -import uuid from dateutil import parser @@ -62,7 +61,7 @@ def __init__(self, hass: HomeAssistant, is_new_session=False): self._evn_area = {} - async def login(self, evn_area, username, password) -> str: + async def login(self, evn_area, username, password, customer_id) -> str: """Try login into EVN corresponding with different EVN areas""" self._evn_area = evn_area @@ -80,7 +79,7 @@ async def login(self, evn_area, username, password) -> str: return await self.login_evncpc(username, password) elif evn_area.get("name") == EVN_NAME.SPC: - return await self.login_evnspc(username, password) + return await self.login_evnspc(username, password, customer_id) elif evn_area.get("name") == EVN_NAME.NPC: return await self.login_evnnpc(username, password) @@ -278,13 +277,13 @@ async def login_evncpc(self, username, password) -> str: _LOGGER.error(f"Error while logging in EVN Endpoints: {resp_json}") return CONF_ERR_UNKNOWN - async def login_evnspc(self, username, password) -> str: + async def login_evnspc(self, username, password, customer_id) -> str: """Create EVN login session corresponding with EVNSPC Endpoint""" payload = { "strUsername": username, "strPassword": password, - "strDeviceID": str(uuid.uuid4), + "strDeviceID": customer_id, } headers = { @@ -773,7 +772,9 @@ async def request_update_evnspc( self, customer_id, from_date, to_date, last_index="001" ): """Request new update from EVNSPC Server""" - + from_date_str = (parser.parse(from_date, dayfirst=True) - timedelta(days=1)).strftime("%Y%m%d") + to_date_str = parser.parse(to_date, dayfirst=True).strftime("%Y%m%d") + headers = { "User-Agent": "evnapp/59 CFNetwork/1240.0.4 Darwin/20.6.0", "Authorization": f"Bearer {self._evn_area.get('access_token')}", @@ -788,8 +789,8 @@ async def request_update_evnspc( headers=headers, params={ "strMaDiemDo": f"{customer_id}{last_index}", - "strFromDate": from_date, - "strToDate": to_date, + "strFromDate": from_date_str, + "strToDate": to_date_str, }, ssl=False, ) @@ -799,53 +800,24 @@ async def request_update_evnspc( if status != CONF_SUCCESS: return resp_json - from_date = parser.parse(resp_json[0]["strTime"], dayfirst=True) + from_date = parser.parse(resp_json[0]["strTime"], dayfirst=True) + timedelta(days=1) to_date = parser.parse( resp_json[(-1 if len(resp_json) > 1 else 0)]["strTime"], dayfirst=True - ) - timedelta(days=1) + ) previous_date = parser.parse( resp_json[(-2 if len(resp_json) > 2 else 0)]["strTime"], dayfirst=True - ) - timedelta(days=1) + ) fetched_data = { "status": CONF_SUCCESS, - ID_ECON_TOTAL_OLD: round( - float(str(resp_json[0]["dGiaoTong"]).replace(",", "")), 2 - ), - ID_ECON_TOTAL_NEW: round( - float( - str( - resp_json[(-1 if len(resp_json) > 1 else 0)]["dGiaoTong"] - ).replace(",", "") - ), - 2, - ), - ID_ECON_DAILY_NEW: round( - float( - str( - resp_json[(-1 if len(resp_json) > 1 else 0)]["dSanLuongBT"] - ).replace(",", "") - ), - 2, - ), + ID_ECON_TOTAL_OLD: round(safe_float(resp_json[0].get("dGiaoBT")), 2), + ID_ECON_TOTAL_NEW: round(safe_float(resp_json[-1].get("dGiaoBT")), 2), + ID_ECON_DAILY_NEW: round(safe_float(resp_json[-1].get("dSanLuongBT")), 2), ID_ECON_DAILY_OLD: round( - float( - str( - resp_json[(-2 if len(resp_json) > 2 else 0)]["dSanLuongBT"] - ).replace(",", "") - ), - 2, + safe_float(resp_json[-2].get("dSanLuongBT")) if len(resp_json) > 1 else 0.0, 2 ), ID_ECON_MONTHLY_NEW: round( - float( - float( - str( - resp_json[(-1 if len(resp_json) > 1 else 0)]["dGiaoTong"] - ).replace(",", "") - ) - - float(str(resp_json[0]["dGiaoTong"]).replace(",", "")) - ), - 2, + safe_float(resp_json[-1].get("dGiaoBT")) - safe_float(resp_json[0].get("dGiaoBT")) ), "to_date": to_date.date(), "from_date": from_date.date(), @@ -863,20 +835,17 @@ async def request_update_evnspc( status, resp_json = await json_processing(resp) - if status == CONF_EMPTY: - payment_status = STATUS_N_PAYMENT_NEEDED - m_payment_status = 0 - elif status == CONF_SUCCESS: - payment_status = STATUS_PAYMENT_NEEDED - - if len(resp_json) and "lThanhTien" in resp_json[0]: - m_payment_status = int(resp_json[0].get("lThanhTien")) + if status == CONF_SUCCESS and resp_json and isinstance(resp_json, list) and resp_json: + m_payment_status = int(resp_json[0].get("lTongTien", 0)) + fetched_data.update({ + ID_PAYMENT_NEEDED: STATUS_PAYMENT_NEEDED, + ID_M_PAYMENT_NEEDED: m_payment_status + }) else: - payment_status = CONF_ERR_UNKNOWN - - fetched_data.update( - {ID_PAYMENT_NEEDED: payment_status, ID_M_PAYMENT_NEEDED: m_payment_status} - ) + fetched_data.update({ + ID_PAYMENT_NEEDED: STATUS_N_PAYMENT_NEEDED if status == CONF_EMPTY else CONF_ERR_UNKNOWN, + ID_M_PAYMENT_NEEDED: 0 + }) return fetched_data @@ -1118,3 +1087,9 @@ def calc_ecost(kwh: float) -> str: total_price = int(round((total_price / 100) * (100 + VIETNAM_ECOST_VAT))) return str(total_price) + +def safe_float(value, default=0.0): + try: + return float(str(value).replace(",", "")) if value is not None else default + except ValueError: + return default \ No newline at end of file diff --git a/custom_components/nestup_evn/types.py b/custom_components/nestup_evn/types.py index 5364e2b..e9658de 100644 --- a/custom_components/nestup_evn/types.py +++ b/custom_components/nestup_evn/types.py @@ -117,7 +117,7 @@ class EVNSensorEntityDescription(SensorEntityDescription, EVNRequiredKeysMixin): name=EVN_NAME.SPC, location="Khu vực miền Nam", evn_login_url="https://api.cskh.evnspc.vn/api/user/authenticate", - evn_data_url="https://api.cskh.evnspc.vn/api/NghiepVu/LayThongTinSanLuongTheoNgay", + evn_data_url="https://api.cskh.evnspc.vn/api/NghiepVu/LayThongTinSanLuongTheoNgay_v1", evn_payment_url="https://api.cskh.evnspc.vn/api/NghiepVu/TraCuuNoHoaDon", pattern=["PB", "PK"], ), From b806fc8a439da613b8ce6086e32d7fc7ece5f29e Mon Sep 17 00:00:00 2001 From: Chau Truong Thinh Date: Sun, 1 Dec 2024 19:28:44 +0700 Subject: [PATCH 2/3] =?UTF-8?q?EVNSPC:=20Th=C3=AAm=20l=E1=BB=8Bch=20c?= =?UTF-8?q?=E1=BA=AFt=20=C4=91i=E1=BB=87n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- custom_components/nestup_evn/const.py | 2 + custom_components/nestup_evn/nestup_evn.py | 51 ++++++++++++++++++++-- custom_components/nestup_evn/types.py | 10 +++++ 3 files changed, 60 insertions(+), 3 deletions(-) diff --git a/custom_components/nestup_evn/const.py b/custom_components/nestup_evn/const.py index f916a1e..e37a10e 100644 --- a/custom_components/nestup_evn/const.py +++ b/custom_components/nestup_evn/const.py @@ -37,12 +37,14 @@ ID_ECOST_MONTHLY_NEW = "ecost_monthly_new" ID_PAYMENT_NEEDED = "payment_needed" ID_M_PAYMENT_NEEDED = "m_payment_needed" +ID_LOADSHEDDING = "loadshedding" ID_FROM_DATE = "from_date" ID_TO_DATE = "to_date" ID_LATEST_UPDATE = "latest_update" STATUS_N_PAYMENT_NEEDED = "Đã thanh toán" STATUS_PAYMENT_NEEDED = "Chưa thanh toán" +STATUS_LOADSHEDDING = "Không có lịch cắt điện" VIETNAM_ECOST_VAT = 8 # in % VIETNAM_ECOST_STAGES = { diff --git a/custom_components/nestup_evn/nestup_evn.py b/custom_components/nestup_evn/nestup_evn.py index d43abb0..5ee2169 100644 --- a/custom_components/nestup_evn/nestup_evn.py +++ b/custom_components/nestup_evn/nestup_evn.py @@ -38,9 +38,11 @@ ID_LATEST_UPDATE, ID_M_PAYMENT_NEEDED, ID_PAYMENT_NEEDED, + ID_LOADSHEDDING, ID_TO_DATE, STATUS_N_PAYMENT_NEEDED, STATUS_PAYMENT_NEEDED, + STATUS_LOADSHEDDING, VIETNAM_ECOST_STAGES, VIETNAM_ECOST_VAT, ) @@ -772,9 +774,10 @@ async def request_update_evnspc( self, customer_id, from_date, to_date, last_index="001" ): """Request new update from EVNSPC Server""" + from_date_str = (parser.parse(from_date, dayfirst=True) - timedelta(days=1)).strftime("%Y%m%d") to_date_str = parser.parse(to_date, dayfirst=True).strftime("%Y%m%d") - + headers = { "User-Agent": "evnapp/59 CFNetwork/1240.0.4 Darwin/20.6.0", "Authorization": f"Bearer {self._evn_area.get('access_token')}", @@ -847,8 +850,29 @@ async def request_update_evnspc( ID_M_PAYMENT_NEEDED: 0 }) - return fetched_data + resp = await self._session.get( + url=self._evn_area.get("evn_loadshedding_url"), + headers=headers, + params={ + "strMaKH": f"{customer_id}", + }, + ssl=False, + ) + status, resp_json = await json_processing(resp) + loadshedding_status = STATUS_LOADSHEDDING + if status == CONF_EMPTY: + loadshedding_status = STATUS_LOADSHEDDING + elif status == CONF_SUCCESS: + if len(resp_json) and "strThoiGianMatDien" in resp_json[0]: + loadshedding_status = str(resp_json[0].get("strThoiGianMatDien")) + else: + loadshedding_status = CONF_ERR_UNKNOWN + fetched_data.update( + {ID_LOADSHEDDING: loadshedding_status} + ) + + return fetched_data async def json_processing(resp): resp_json: dict = {} @@ -973,6 +997,13 @@ def formatted_result(raw_data: dict) -> dict: ), } + original_content = str(raw_data.get(ID_LOADSHEDDING, "Unknown")) + formatted_content = format_loadshedding(original_content) + res[ID_LOADSHEDDING] = { + "value": formatted_content, + "info": "mdi:transmission-tower-off", + } + if ID_FROM_DATE in raw_data: res[ID_FROM_DATE] = {"value": raw_data.get("from_date").strftime("%d/%m/%Y")} else: @@ -1092,4 +1123,18 @@ def safe_float(value, default=0.0): try: return float(str(value).replace(",", "")) if value is not None else default except ValueError: - return default \ No newline at end of file + return default + +def format_loadshedding(raw_value: str) -> str: + try: + start, end = raw_value.replace('từ ', '').replace(' ngày', '').split('đến') + start_time, start_date = start.strip().split() + end_time, end_date = end.strip().split() + start_time = start_time[:-3] + end_time = end_time[:-3] + start_date = start_date[:-5] + end_date = end_date[:-5] + return f"{start_time} {start_date} - {end_time} {end_date}" + + except Exception as e: + return f"Error: {str(e)}" \ No newline at end of file diff --git a/custom_components/nestup_evn/types.py b/custom_components/nestup_evn/types.py index e9658de..4e094d0 100644 --- a/custom_components/nestup_evn/types.py +++ b/custom_components/nestup_evn/types.py @@ -22,6 +22,7 @@ ID_LATEST_UPDATE, ID_M_PAYMENT_NEEDED, ID_PAYMENT_NEEDED, + ID_LOADSHEDDING, ID_TO_DATE, ) @@ -48,6 +49,7 @@ class Area: evn_login_url: str | None = None evn_data_url: str | None = None evn_payment_url: str | None = None + evn_loadshedding_url: str | None = None supported: bool = True date_needed: bool = True pattern: ArrayType | None = None @@ -119,6 +121,7 @@ class EVNSensorEntityDescription(SensorEntityDescription, EVNRequiredKeysMixin): evn_login_url="https://api.cskh.evnspc.vn/api/user/authenticate", evn_data_url="https://api.cskh.evnspc.vn/api/NghiepVu/LayThongTinSanLuongTheoNgay_v1", evn_payment_url="https://api.cskh.evnspc.vn/api/NghiepVu/TraCuuNoHoaDon", + evn_loadshedding_url="https://api.cskh.evnspc.vn/api/NghiepVu/TraCuuLichNgungGiamCungCapDien", pattern=["PB", "PK"], ), ] @@ -230,4 +233,11 @@ class EVNSensorEntityDescription(SensorEntityDescription, EVNRequiredKeysMixin): value_fn=lambda data: data[ID_M_PAYMENT_NEEDED], dynamic_icon=True, ), + EVNSensorEntityDescription( + key=ID_LOADSHEDDING, + name="Lịch cắt điện", + icon="mdi:transmission-tower-off", + value_fn=lambda data: data[ID_LOADSHEDDING], + dynamic_icon=True, + ), ) From 371f7fe524e367289b813b8329d23ce907d1b7f9 Mon Sep 17 00:00:00 2001 From: Chau Truong Thinh Date: Sun, 1 Dec 2024 23:06:10 +0700 Subject: [PATCH 3/3] EVNSPC: Retry fetch 3 times when failed --- custom_components/nestup_evn/nestup_evn.py | 61 +++++++++++++--------- 1 file changed, 37 insertions(+), 24 deletions(-) diff --git a/custom_components/nestup_evn/nestup_evn.py b/custom_components/nestup_evn/nestup_evn.py index 5ee2169..e8dc311 100644 --- a/custom_components/nestup_evn/nestup_evn.py +++ b/custom_components/nestup_evn/nestup_evn.py @@ -787,7 +787,7 @@ async def request_update_evnspc( "Connection": "keep-alive", } - resp = await self._session.get( + status, resp_json = await fetch_with_retries( url=self._evn_area.get("evn_data_url"), headers=headers, params={ @@ -795,13 +795,12 @@ async def request_update_evnspc( "strFromDate": from_date_str, "strToDate": to_date_str, }, - ssl=False, + session=self._session, + api_name="Fetch EVN data" ) - status, resp_json = await json_processing(resp) - - if status != CONF_SUCCESS: - return resp_json + if not resp_json: + raise ValueError("Received empty response from EVN data API.") from_date = parser.parse(resp_json[0]["strTime"], dayfirst=True) + timedelta(days=1) to_date = parser.parse( @@ -827,17 +826,17 @@ async def request_update_evnspc( "previous_date": previous_date.date(), } - resp = await self._session.get( + status, resp_json = await fetch_with_retries( url=self._evn_area.get("evn_payment_url"), headers=headers, params={ "strMaKH": f"{customer_id}", }, - ssl=False, + session=self._session, + allow_empty=True, + api_name="Payment data" ) - status, resp_json = await json_processing(resp) - if status == CONF_SUCCESS and resp_json and isinstance(resp_json, list) and resp_json: m_payment_status = int(resp_json[0].get("lTongTien", 0)) fetched_data.update({ @@ -850,26 +849,18 @@ async def request_update_evnspc( ID_M_PAYMENT_NEEDED: 0 }) - resp = await self._session.get( + status, resp_json = await fetch_with_retries( url=self._evn_area.get("evn_loadshedding_url"), headers=headers, params={ "strMaKH": f"{customer_id}", }, - ssl=False, + session=self._session, + api_name="EVN loadshedding data" ) - status, resp_json = await json_processing(resp) - loadshedding_status = STATUS_LOADSHEDDING - if status == CONF_EMPTY: - loadshedding_status = STATUS_LOADSHEDDING - elif status == CONF_SUCCESS: - if len(resp_json) and "strThoiGianMatDien" in resp_json[0]: - loadshedding_status = str(resp_json[0].get("strThoiGianMatDien")) - else: - loadshedding_status = CONF_ERR_UNKNOWN - fetched_data.update( - {ID_LOADSHEDDING: loadshedding_status} + fetched_data[ID_LOADSHEDDING] = ( + resp_json[0].get("strThoiGianMatDien") if resp_json else STATUS_LOADSHEDDING if status == CONF_EMPTY else CONF_ERR_UNKNOWN ) return fetched_data @@ -1137,4 +1128,26 @@ def format_loadshedding(raw_value: str) -> str: return f"{start_time} {start_date} - {end_time} {end_date}" except Exception as e: - return f"Error: {str(e)}" \ No newline at end of file + return f"Error: {str(e)}" + +async def fetch_with_retries( + url, headers, params, max_retries=3, session=None, allow_empty=False, api_name="API" +): + """Fetch data with retry mechanism.""" + for attempt in range(max_retries): + try: + resp = await session.get(url=url, headers=headers, params=params, ssl=False) + status, resp_json = await json_processing(resp) + + if status == CONF_EMPTY: + return CONF_EMPTY, [] + + if status == CONF_SUCCESS or (allow_empty and status == CONF_EMPTY): + return status, resp_json + + _LOGGER.error(f"Attempt {attempt + 1}/{max_retries} failed for {api_name}: {resp_json}") + + except Exception as e: + _LOGGER.error(f"Attempt {attempt + 1}/{max_retries} encountered an error: {str(e)}") + + raise Exception(f"Failed to fetch data of {api_name} after {max_retries} attempts.") \ No newline at end of file