diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index ef8a9b4..740ea84 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.7 +FROM python:3.8 RUN apt-get update \ && apt-get install -y --no-install-recommends \ diff --git a/.devcontainer/configuration.yaml b/.devcontainer/configuration.yaml index 8a4c31e..a8fcccb 100644 --- a/.devcontainer/configuration.yaml +++ b/.devcontainer/configuration.yaml @@ -1,5 +1,5 @@ default_config: logger: - default: error + default: info logs: custom_components.fpl: debug \ No newline at end of file diff --git a/custom_components/fpl/AverageDailySensor.py b/custom_components/fpl/AverageDailySensor.py index dd2026e..d0f4778 100644 --- a/custom_components/fpl/AverageDailySensor.py +++ b/custom_components/fpl/AverageDailySensor.py @@ -7,7 +7,12 @@ class FplAverageDailySensor(FplSensor): @property def state(self): - return self.data["daily_avg"] + try: + if "daily_avg" in self.data: + self._state = self.data["daily_avg"] + except: + pass + return self._state @property def device_state_attributes(self): diff --git a/custom_components/fpl/DailyUsageSensor.py b/custom_components/fpl/DailyUsageSensor.py index 8895fee..255e0e7 100644 --- a/custom_components/fpl/DailyUsageSensor.py +++ b/custom_components/fpl/DailyUsageSensor.py @@ -7,10 +7,25 @@ class FplDailyUsageSensor(FplSensor): @property def state(self): - return self.data["daily_usage"][-1]["cost"] + try: + if "daily_usage" in self.data: + if len(self.data["daily_usage"]) > 0: + if "cost" in self.data["daily_usage"][-1]: + self._state = self.data["daily_usage"][-1]["cost"] + except: + pass + + return self._state @property def device_state_attributes(self): """Return the state attributes.""" - self.attr["date"] = self.data["daily_usage"][-1]["date"] + try: + if "daily_usage" in self.data: + if len(self.data["daily_usage"]) > 0: + if "date" in self.data["daily_usage"][-1]: + self.attr["date"] = self.data["daily_usage"][-1]["date"] + except: + pass + return self.attr diff --git a/custom_components/fpl/FplSensor.py b/custom_components/fpl/FplSensor.py index ab7b061..d6639a8 100644 --- a/custom_components/fpl/FplSensor.py +++ b/custom_components/fpl/FplSensor.py @@ -22,7 +22,7 @@ class FplSensor(Entity): async def async_update(self): """Update the sensor.""" # Send update "signal" to the component - await self.hass.data[DOMAIN_DATA]["client"].update_data() + # await self.hass.data[DOMAIN_DATA]["client"].update_data() # Get new data (if any) if "data" in self.hass.data[DOMAIN_DATA]: diff --git a/custom_components/fpl/ProjectedBillSensor.py b/custom_components/fpl/ProjectedBillSensor.py index e1d75f1..bf14a32 100644 --- a/custom_components/fpl/ProjectedBillSensor.py +++ b/custom_components/fpl/ProjectedBillSensor.py @@ -8,21 +8,28 @@ class FplProjectedBillSensor(FplSensor): @property def state(self): data = self.data - if "budget_bill" in data.keys(): - if data["budget_bill"]: - if "budget_billing_projected_bill" in data.keys(): - self._state = data["budget_billing_projected_bill"] - else: - if "projected_bill" in data.keys(): - self._state = data["projected_bill"] + try: + if "budget_bill" in data.keys(): + if data["budget_bill"]: + if "budget_billing_projected_bill" in data.keys(): + self._state = data["budget_billing_projected_bill"] + else: + if "projected_bill" in data.keys(): + self._state = data["projected_bill"] + except: + pass return self._state @property def device_state_attributes(self): """Return the state attributes.""" - if "budget_bill" in self.data.keys(): - self.attr["budget_bill"] = self.data["budget_bill"] + try: + if "budget_bill" in self.data.keys(): + self.attr["budget_bill"] = self.data["budget_bill"] + except: + pass + return self.attr @property diff --git a/custom_components/fpl/fplapi.py b/custom_components/fpl/fplapi.py index 36e3e51..2932ffe 100644 --- a/custom_components/fpl/fplapi.py +++ b/custom_components/fpl/fplapi.py @@ -6,6 +6,7 @@ from datetime import timedelta, datetime, date as dt import aiohttp import async_timeout import json +import sys from bs4 import BeautifulSoup @@ -15,9 +16,11 @@ STATUS_CATEGORY_OPEN = "OPEN" LOGIN_RESULT_OK = "OK" LOGIN_RESULT_INVALIDUSER = "NOTVALIDUSER" LOGIN_RESULT_INVALIDPASSWORD = "FAILEDPASSWORD" +LOGIN_RESULT_UNAUTHORIZED = "UNAUTHORIZED" +LOGIN_RESULT_FAILURE = "FAILURE" _LOGGER = logging.getLogger(__name__) -TIMEOUT = 5 +TIMEOUT = 30 URL_LOGIN = "https://www.fpl.com/api/resources/login" URL_RESOURCES_HEADER = "https://www.fpl.com/api/resources/header" @@ -41,13 +44,14 @@ class FplApi(object): async def get_data(self): self._session = aiohttp.ClientSession() data = {} - await self.login() - accounts = await self.async_get_open_accounts() + data["accounts"] = [] + if await self.login() == LOGIN_RESULT_OK: + accounts = await self.async_get_open_accounts() - data["accounts"] = accounts - for account in accounts: - accountData = await self.__async_get_data(account) - data[account] = accountData + data["accounts"] = accounts + for account in accounts: + accountData = await self.__async_get_data(account) + data[account] = accountData await self._session.close() @@ -63,42 +67,49 @@ class FplApi(object): _LOGGER.info("Logging") """login and get account information""" - async with async_timeout.timeout(TIMEOUT, loop=self._loop): - response = await session.get( - URL_LOGIN, auth=aiohttp.BasicAuth(self._username, self._password) - ) + result = LOGIN_RESULT_OK + try: + async with async_timeout.timeout(TIMEOUT, loop=self._loop): + response = await session.get( + URL_LOGIN, auth=aiohttp.BasicAuth(self._username, self._password) + ) - js = json.loads(await response.text()) + js = json.loads(await response.text()) - if response.reason == "Unauthorized": - await session.close() - raise Exception(js["messageCode"]) + if response.reason == "Unauthorized": + result = LOGIN_RESULT_UNAUTHORIZED - if js["messages"][0]["messageCode"] != "login.success": - _LOGGER.error(f"Logging Failure") - await session.close() - raise Exception("login failure") + if js["messages"][0]["messageCode"] != "login.success": + _LOGGER.error(f"Logging Failure") + result = LOGIN_RESULT_FAILURE - _LOGGER.info(f"Logging Successful") + _LOGGER.info(f"Logging Successful") + + except Exception as e: + _LOGGER.error(f"Error {e} : {sys.exc_info()[0]}") + result = LOGIN_RESULT_FAILURE if close: await session.close() - return LOGIN_RESULT_OK + return result async def async_get_open_accounts(self): _LOGGER.info(f"Getting accounts") - async with async_timeout.timeout(TIMEOUT, loop=self._loop): - response = await self._session.get(URL_RESOURCES_HEADER) - - js = await response.json() - accounts = js["data"]["accounts"]["data"]["data"] - result = [] - for account in accounts: - if account["statusCategory"] == STATUS_CATEGORY_OPEN: - result.append(account["accountNumber"]) + try: + async with async_timeout.timeout(TIMEOUT, loop=self._loop): + response = await self._session.get(URL_RESOURCES_HEADER) + + js = await response.json() + accounts = js["data"]["accounts"]["data"]["data"] + + for account in accounts: + if account["statusCategory"] == STATUS_CATEGORY_OPEN: + result.append(account["accountNumber"]) + except Exception as e: + _LOGGER.error(f"Getting accounts {e}") # self._account_number = js["data"]["selectedAccount"]["data"]["accountNumber"] # self._premise_number = js["data"]["selectedAccount"]["data"]["acctSecSettings"]["premiseNumber"] @@ -168,78 +179,87 @@ class FplApi(object): return data async def __getFromProjectedBill(self, account, premise, currentBillDate): - async with async_timeout.timeout(TIMEOUT, loop=self._loop): - response = await self._session.get( - URL_RESOURCES_PROJECTED_BILL.format( - account=account, - premise=premise, - lastBillDate=currentBillDate.strftime("%m%d%Y"), + try: + async with async_timeout.timeout(TIMEOUT, loop=self._loop): + response = await self._session.get( + URL_RESOURCES_PROJECTED_BILL.format( + account=account, + premise=premise, + lastBillDate=currentBillDate.strftime("%m%d%Y"), + ) ) - ) - if response.status == 200: - data = {} - projectedBillData = (await response.json())["data"] + if response.status == 200: + data = {} + projectedBillData = (await response.json())["data"] - billToDate = float(projectedBillData["billToDate"]) - projectedBill = float(projectedBillData["projectedBill"]) - dailyAvg = float(projectedBillData["dailyAvg"]) - avgHighTemp = int(projectedBillData["avgHighTemp"]) + billToDate = float(projectedBillData["billToDate"]) + projectedBill = float(projectedBillData["projectedBill"]) + dailyAvg = float(projectedBillData["dailyAvg"]) + avgHighTemp = int(projectedBillData["avgHighTemp"]) - data["bill_to_date"] = billToDate - data["projected_bill"] = projectedBill - data["daily_avg"] = dailyAvg - data["avg_high_temp"] = avgHighTemp - return data - - return [] + data["bill_to_date"] = billToDate + data["projected_bill"] = projectedBill + data["daily_avg"] = dailyAvg + data["avg_high_temp"] = avgHighTemp + return data + except: + return [] async def __getBBL_async(self, account, projectedBillData): _LOGGER.info(f"Getting budget billing data") data = {} URL = "https://www.fpl.com/api/resources/account/{account}/budgetBillingGraph/premiseDetails" - async with async_timeout.timeout(TIMEOUT, loop=self._loop): - response = await self._session.get(URL.format(account=account)) - if response.status == 200: - r = (await response.json())["data"] - dataList = r["graphData"] + try: + async with async_timeout.timeout(TIMEOUT, loop=self._loop): + response = await self._session.get(URL.format(account=account)) + if response.status == 200: + r = (await response.json())["data"] + dataList = r["graphData"] - startIndex = len(dataList) - 1 + startIndex = len(dataList) - 1 - billingCharge = 0 - budgetBillDeferBalance = r["defAmt"] + billingCharge = 0 + budgetBillDeferBalance = r["defAmt"] - projectedBill = projectedBillData["projected_bill"] - asOfDays = projectedBillData["as_of_days"] + projectedBill = projectedBillData["projected_bill"] + asOfDays = projectedBillData["as_of_days"] - for det in dataList: - billingCharge += det["actuallBillAmt"] + for det in dataList: + billingCharge += det["actuallBillAmt"] - calc1 = (projectedBill + billingCharge) / 12 - calc2 = (1 / 12) * (budgetBillDeferBalance) + calc1 = (projectedBill + billingCharge) / 12 + calc2 = (1 / 12) * (budgetBillDeferBalance) - projectedBudgetBill = round(calc1 + calc2, 2) - bbDailyAvg = round(projectedBudgetBill / 30, 2) - bbAsOfDateAmt = round(projectedBudgetBill / 30 * asOfDays, 2) + projectedBudgetBill = round(calc1 + calc2, 2) + bbDailyAvg = round(projectedBudgetBill / 30, 2) + bbAsOfDateAmt = round(projectedBudgetBill / 30 * asOfDays, 2) - data["budget_billing_daily_avg"] = bbDailyAvg - data["budget_billing_bill_to_date"] = bbAsOfDateAmt + data["budget_billing_daily_avg"] = bbDailyAvg + data["budget_billing_bill_to_date"] = bbAsOfDateAmt - data["budget_billing_projected_bill"] = float(projectedBudgetBill) + data["budget_billing_projected_bill"] = float(projectedBudgetBill) + except: + pass URL = "https://www.fpl.com/api/resources/account/{account}/budgetBillingGraph" - async with async_timeout.timeout(TIMEOUT, loop=self._loop): - response = await self._session.get(URL.format(account=account)) - if response.status == 200: - r = (await response.json())["data"] - data["bill_to_date"] = float(r["eleAmt"]) - data["defered_amount"] = float(r["defAmt"]) + try: + async with async_timeout.timeout(TIMEOUT, loop=self._loop): + response = await self._session.get(URL.format(account=account)) + if response.status == 200: + r = (await response.json())["data"] + data["bill_to_date"] = float(r["eleAmt"]) + data["defered_amount"] = float(r["defAmt"]) + except: + pass return data - async def __getDataFromEnergyService(self, account, premise, lastBilledDate): + async def __getDataFromEnergyService( + self, account, premise, lastBilledDate + ) -> dict: _LOGGER.info(f"Getting data from energy service") URL = "https://www.fpl.com/dashboard-api/resources/account/{account}/energyService/{account}" @@ -289,21 +309,23 @@ class FplApi(object): _LOGGER.info(f"Getting data from applicance usage") URL = "https://www.fpl.com/dashboard-api/resources/account/{account}/applianceUsage/{account}" JSON = {"startDate": str(lastBilledDate.strftime("%m%d%Y"))} + try: + async with async_timeout.timeout(TIMEOUT, loop=self._loop): + response = await self._session.post( + URL.format(account=account), json=JSON + ) + if response.status == 200: + electric = (await response.json())["data"]["electric"] + data = {} + full = 100 + for e in electric: + rr = round(float(e["percentageDollar"])) + if rr < full: + full = full - rr + else: + rr = full + data[e["category"].replace(" ", "_")] = rr - async with async_timeout.timeout(TIMEOUT, loop=self._loop): - response = await self._session.post(URL.format(account=account), json=JSON) - if response.status == 200: - electric = (await response.json())["data"]["electric"] - data = {} - full = 100 - for e in electric: - rr = round(float(e["percentageDollar"])) - if rr < full: - full = full - rr - else: - rr = full - data[e["category"].replace(" ", "_")] = rr - - return {"energy_percent_by_applicance": data} - - return [] + return {"energy_percent_by_applicance": data} + except: + return [] diff --git a/custom_components/fpl/sensor.py b/custom_components/fpl/sensor.py index d2e1ca1..89ee7cd 100644 --- a/custom_components/fpl/sensor.py +++ b/custom_components/fpl/sensor.py @@ -5,7 +5,7 @@ import aiohttp import asyncio from homeassistant.helpers.entity import Entity from homeassistant import util - +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.event import async_call_later from homeassistant.const import ( @@ -33,19 +33,23 @@ def setup(hass, config): async def async_setup_entry(hass, config_entry, async_add_entities): + try: + accounts = config_entry.data.get("accounts") - accounts = config_entry.data.get("accounts") + fpl_accounts = [] - fpl_accounts = [] + for account in accounts: + _LOGGER.info(f"Adding fpl account: {account}") + fpl_accounts.append(FplSensor(hass, config_entry.data, account)) + fpl_accounts.append(FplDailyUsageSensor(hass, config_entry.data, account)) + fpl_accounts.append(FplAverageDailySensor(hass, config_entry.data, account)) + fpl_accounts.append( + FplProjectedBillSensor(hass, config_entry.data, account) + ) - for account in accounts: - _LOGGER.info(f"Adding fpl account: {account}") - fpl_accounts.append(FplSensor(hass, config_entry.data, account)) - fpl_accounts.append(FplDailyUsageSensor(hass, config_entry.data, account)) - fpl_accounts.append(FplAverageDailySensor(hass, config_entry.data, account)) - fpl_accounts.append(FplProjectedBillSensor(hass, config_entry.data, account)) - - async_add_entities(fpl_accounts) + async_add_entities(fpl_accounts) + except: + raise ConfigEntryNotReady class FplSensor(Entity): @@ -108,7 +112,7 @@ class FplSensor(Entity): @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_UPDATES) async def async_update(self): # Send update "signal" to the component - await self.hass.data[DOMAIN_DATA]["client"].update_data() + # await self.hass.data[DOMAIN_DATA]["client"].update_data() # Get new data (if any) if "data" in self.hass.data[DOMAIN_DATA]: