From d11df67c1854630c950a0ce111037e03fe70daec Mon Sep 17 00:00:00 2001 From: Yordan Suarez Date: Mon, 30 Dec 2019 12:56:11 -0500 Subject: [PATCH 1/2] bunch of changes --- .gitignore | 2 + .vscode/launch.json | 16 ++ .vscode/settings.json | 5 + custom_components/fpl/.translations/en.json | 4 +- custom_components/fpl/config_flow.py | 41 ++- custom_components/fpl/const.py | 8 + custom_components/fpl/fplapi.py | 282 ++++++++++++++++---- custom_components/fpl/sensor.py | 40 ++- 8 files changed, 322 insertions(+), 76 deletions(-) create mode 100644 .gitignore create mode 100644 .vscode/launch.json create mode 100644 .vscode/settings.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b2afc79 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +custom_components/fpl/test.py diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..fba2e38 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,16 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Current File", + "type": "python", + "request": "launch", + //"program": "/workspaces/hass-fpl/custom_components/fpl/test.py", + "program": "E:/projects/hass-fpl/custom_components/fpl/test.py", + "console": "integratedTerminal" + } + ] +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..a04b218 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "files.associations": { + "*.yaml": "home-assistant" + } +} \ No newline at end of file diff --git a/custom_components/fpl/.translations/en.json b/custom_components/fpl/.translations/en.json index 800ba50..aa2923d 100644 --- a/custom_components/fpl/.translations/en.json +++ b/custom_components/fpl/.translations/en.json @@ -14,7 +14,9 @@ }, "error": { "auth": "Username/Password is wrong.", - "name_exists": "Configuration already exists." + "name_exists": "Configuration already exists.", + "invalid_username": "Invalid Username", + "invalid_password": "Invalid Password" }, "abort": { "single_instance_allowed": "Only a single configuration of Fpl is allowed." diff --git a/custom_components/fpl/config_flow.py b/custom_components/fpl/config_flow.py index c7cecd8..e248778 100644 --- a/custom_components/fpl/config_flow.py +++ b/custom_components/fpl/config_flow.py @@ -4,7 +4,15 @@ import voluptuous as vol from .fplapi import FplApi from homeassistant import config_entries import aiohttp -from .const import DOMAIN, CONF_USERNAME, CONF_PASSWORD, CONF_NAME +from .const import ( + DOMAIN, + CONF_USERNAME, + CONF_PASSWORD, + CONF_NAME, + LOGIN_RESULT_OK, + LOGIN_RESULT_INVALIDUSER, + LOGIN_RESULT_INVALIDPASSWORD, +) from homeassistant.core import callback @@ -32,24 +40,32 @@ class FplFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a flow initialized by the user.""" self._errors = {} - # if self._async_current_entries(): - # return self.async_abort(reason="single_instance_allowed") - # if self.hass.data.get(DOMAIN): - # return self.async_abort(reason="single_instance_allowed") + if self._async_current_entries(): + return self.async_abort(reason="single_instance_allowed") + if self.hass.data.get(DOMAIN): + return self.async_abort(reason="single_instance_allowed") if user_input is not None: username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] if username not in configured_instances(self.hass): - valid = await self._test_credentials(username, password) + result = await self._test_credentials(username, password) - if valid: + if result == LOGIN_RESULT_OK: return self.async_create_entry( title=user_input[CONF_NAME], data=user_input ) - else: + + if result == LOGIN_RESULT_INVALIDUSER: + self._errors[CONF_USERNAME] = "invalid_username" + + if result == LOGIN_RESULT_INVALIDPASSWORD: + self._errors[CONF_PASSWORD] = "invalid_password" + + if result == None: self._errors["base"] = "auth" + else: self._errors[CONF_NAME] = "name_exists" @@ -84,11 +100,12 @@ class FplFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): async def _test_credentials(self, username, password): """Return true if credentials is valid.""" + session = aiohttp.ClientSession() try: - session = aiohttp.ClientSession() api = FplApi(username, password, True, None, session) - await api.login() - return True + result = await api.login() except Exception: # pylint: disable=broad-except pass - return False + + await session.close() + return result diff --git a/custom_components/fpl/const.py b/custom_components/fpl/const.py index 1c00d96..dc1c6aa 100644 --- a/custom_components/fpl/const.py +++ b/custom_components/fpl/const.py @@ -33,3 +33,11 @@ CONF_PASSWORD = "password" # Defaults DEFAULT_NAME = DOMAIN + +# Api login result +LOGIN_RESULT_OK = "OK" +LOGIN_RESULT_INVALIDUSER = "NOTVALIDUSER" +LOGIN_RESULT_INVALIDPASSWORD = "FAILEDPASSWORD" + + +STATUS_CATEGORY_OPEN = "OPEN" diff --git a/custom_components/fpl/fplapi.py b/custom_components/fpl/fplapi.py index 08d649c..8a8e48e 100644 --- a/custom_components/fpl/fplapi.py +++ b/custom_components/fpl/fplapi.py @@ -1,11 +1,15 @@ import asyncio import logging import re -from datetime import timedelta, date +from datetime import timedelta, datetime, date as dt import aiohttp import async_timeout +import json + + from bs4 import BeautifulSoup +from const import STATUS_CATEGORY_OPEN, LOGIN_RESULT_OK _LOGGER = logging.getLogger(__name__) TIMEOUT = 5 @@ -14,40 +18,190 @@ TIMEOUT = 5 class FplApi(object): """A class for getting energy usage information from Florida Power & Light.""" - def __init__(self, username, password, is_tou, loop, session): + def __init__(self, username, password, loop, session): """Initialize the data retrieval. Session should have BasicAuth flag set.""" self._username = username self._password = password self._loop = loop self._session = session - self._is_tou = is_tou - self._account_number = None - self._premise_number = None - - self.yesterday_kwh = None - self.yesterday_dollars = None - self.mtd_kwh = None - self.mtd_dollars = None - self.projected_bill = None async def login(self): """login and get account information""" async with async_timeout.timeout(TIMEOUT, loop=self._loop): - response = await self._session.get("https://www.fpl.com/api/resources/login", - auth=aiohttp.BasicAuth(self._username, self._password)) + response = await self._session.get( + "https://www.fpl.com/api/resources/login", + auth=aiohttp.BasicAuth(self._username, self._password), + ) - if (await response.json())["messages"][0]["messageCode"] != "login.success": - raise Exception('login failure') + js = json.loads(await response.text()) + + if response.reason == "Unauthorized": + return js["messageCode"] + + if js["messages"][0]["messageCode"] != "login.success": + raise Exception("login failure") + + return LOGIN_RESULT_OK + + async def async_get_open_accounts(self): + async with async_timeout.timeout(TIMEOUT, loop=self._loop): + response = await self._session.get( + "https://www.fpl.com/api/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"]) + # print(account["accountNumber"]) + # print(account["premiseNumber"]) + # print(account["statusCategory"]) + + # self._account_number = js["data"]["selectedAccount"]["data"]["accountNumber"] + # self._premise_number = js["data"]["selectedAccount"]["data"]["acctSecSettings"]["premiseNumber"] + return result + + async def async_get_data(self, account): + # await self.async_get_yesterday_usage() + # await self.async_get_mtd_usage() + async with async_timeout.timeout(TIMEOUT, loop=self._loop): + response = await self._session.get( + "https://www.fpl.com/api/resources/account/" + account + ) + data = (await response.json())["data"] + + premise = data["premiseNumber"].zfill(9) + print(premise) + + # print(data["nextBillDate"].replace("-", "").split("T")[0]) + # print(data["currentBillDate"].replace("-", "").split("T")[0]) + + start_date = datetime.strptime( + data["currentBillDate"].replace("-", "").split("T")[0], "%Y%m%d").date() + end_date = dt.today() + + last_day = datetime.strptime( + data["nextBillDate"].replace("-", "").split("T")[0], "%Y%m%d").date() + + lasting_days = (last_day - dt.today()).days + zip_code = data["serviceAddress"]["zip"] + + url = ( + "https://app.fpl.com/wps/PA_ESFPortalWeb/getDailyConsumption" + f"?premiseNumber={premise}" + f"&startDate={start_date.strftime('%Y%m%d')}" + f"&endDate={end_date.strftime('%Y%m%d')}" + f"&accountNumber={account}" + # "&accountType=ELE" + f"&zipCode={zip_code}" + "&consumption=0.0" + "&usage=0.0" + "&isMultiMeter=false" + f"&lastAvailableDate={end_date}" + # "&isAmiMeter=true" + "&userType=EXT" + # "¤tReading=64359" + "&isResidential=true" + # "&isTouUser=false" + "&showGroupData=false" + # "&isNetMeter=false" + "&certifiedDate=1900/01/01" + # "&acctNetMeter=false" + "&tempType=max" + "&viewType=dollar" + "&ecDayHumType=NoHum" + # "&ecHasMoveInRate=false" + # "&ecMoveInRateVal=" + # "&lastAvailableIeeDate=20191230" + ) async with async_timeout.timeout(TIMEOUT, loop=self._loop): - response = await self._session.get("https://www.fpl.com/api/resources/header") - json = await response.json() - self._account_number = json["data"]["selectedAccount"]["data"]["accountNumber"] - self._premise_number = json["data"]["selectedAccount"]["data"]["acctSecSettings"]["premiseNumber"] + response = await self._session.get(url) + + if response.status != 200: + self.data = None + return + + malformedXML = await response.read() + + cleanerXML = ( + str(malformedXML) + .replace('', "", 1) + .split("@@", 1)[0] + ) + + total_kw = 0 + total_cost = 0 + days = 0 + + soup = BeautifulSoup(cleanerXML, "html.parser") + items = soup.find("dataset", seriesname="$").find_all("set") + + details = [] + + for item in items: + # + match = re.search( + r"Date: (\w\w\w. \d\d, \d\d\d\d).*\{br\}kWh Usage: (.*?) kWh \{br\}.*Cost:\s\$([\w|.]+).*Temp:\s(\d+)", + str(item), + ) + if match: + date = datetime.strptime(match.group(1), "%b. %d, %Y").date() + usage = int(match.group(2)) + cost = float(match.group(3)) + max_temp = int(match.group(4)) + if usage == 0: + cost = 0 + + total_kw += usage + total_cost += cost + days += 1 + + day_detail = {} + day_detail["date"] = date + day_detail["usage"] = usage + day_detail["cost"] = cost + day_detail["max_temperature"] = max_temp + + details.append(day_detail) + + print(date) + print(usage) + print(cost) + print(max_temp) + + print("TOTALS") + print(total_kw) + print(total_cost) + + print("Average") + avg_cost = round(total_cost / days, 2) + print(avg_cost) + avg_kw = round(total_kw / days, 0) + print(avg_kw) + + print("Projected") + projected_cost = round(total_cost + avg_cost * lasting_days, 2) + print(projected_cost) + + data = {} + data["start_date"] = start_date + data["end_date"] = end_date + data["service_days"] = (end_date - start_date).days + data["current_days"] = days + data["remaining_days"] = lasting_days + data["details"] = details + + return data + pass async def async_get_yesterday_usage(self): async with async_timeout.timeout(TIMEOUT, loop=self._loop): - response = await self._session.get(self._build_daily_url()) + url = self._build_daily_url() + response = await self._session.get(url) _LOGGER.debug("Response from API: %s", response.status) @@ -57,60 +211,80 @@ class FplApi(object): malformedXML = await response.read() - cleanerXML = str(malformedXML).replace( - '', '', 1 - ).split("@@", 1)[0] + cleanerXML = ( + str(malformedXML) + .replace('', "", 1) + .split("@@", 1)[0] + ) - soup = BeautifulSoup(cleanerXML, 'html.parser') + soup = BeautifulSoup(cleanerXML, "html.parser") - tool_text = soup.find("dataset", seriesname="$") \ - .find("set")["tooltext"] + tool_text = soup.find("dataset", seriesname="$").find("set")[ + "tooltext"] match = re.search(r"\{br\}kWh Usage: (.*?) kWh \{br\}", tool_text) if match: - self.yesterday_kwh = match.group(1) + self.yesterday_kwh = match.group(1).replace("$", "") match2 = re.search(r"\{br\}Approx\. Cost: (\$.*?) \{br\}", tool_text) if match2: - self.yesterday_dollars = match2.group(1) + self.yesterday_dollars = match2.group(1).replace("$", "") async def async_get_mtd_usage(self): async with async_timeout.timeout(TIMEOUT, loop=self._loop): response = await self._session.get( - "https://app.fpl.com/wps/myportal/EsfPortal") + "https://app.fpl.com/wps/myportal/EsfPortal" + ) - soup = BeautifulSoup(await response.text(), 'html.parser') + soup = BeautifulSoup(await response.text(), "html.parser") - self.mtd_kwh = soup.find(id="bpbsubcontainer") \ - .find("table", class_="bpbtab_style_bill", width=430) \ - .find_all("div", class_="bpbtabletxt")[-1].string + self.mtd_kwh = ( + soup.find(id="bpbsubcontainer") + .find("table", class_="bpbtab_style_bill", width=430) + .find_all("div", class_="bpbtabletxt")[-1] + .string + ) - self.mtd_dollars = soup \ - .find_all("div", class_="bpbusagebgnd")[1] \ - .find("div", class_="bpbusagedollartxt").getText().strip() + self.mtd_dollars = ( + soup.find_all("div", class_="bpbusagebgnd")[1] + .find("div", class_="bpbusagedollartxt") + .getText() + .strip() + .replace("$", "") + ) - self.projected_bill = soup.find(id="bpssmlsubcontainer") \ - .find("div", class_="bpsmonthbillbgnd") \ - .find("div", class_="bpsmnthbilldollartxt") \ - .getText().strip().replace("$", "") + self.projected_bill = ( + soup.find(id="bpssmlsubcontainer") + .find("div", class_="bpsmonthbillbgnd") + .find("div", class_="bpsmnthbilldollartxt") + .getText() + .strip() + .replace("$", "") + ) + + test = soup.find( + class_="bpsusagesmlmnthtxt").getText().strip().split(" - ") + self.start_period = test[0] + self.end_period = test[1] def _build_daily_url(self): - end_date = date.today() + end_date = dt.today() start_date = end_date - timedelta(days=1) - return ("https://app.fpl.com/wps/PA_ESFPortalWeb/getDailyConsumption" - "?premiseNumber={premise_number}" - "&accountNumber={account_number}" - "&isTouUser={is_tou}" - "&startDate={start_date}" - "&endDate={end_date}" - "&userType=EXT" - "&isResidential=true" - "&certifiedDate=2000/01/01" - "&viewType=dollar" - "&tempType=max" - "&ecDayHumType=NoHum" - ).format( + return ( + "https://app.fpl.com/wps/PA_ESFPortalWeb/getDailyConsumption" + "?premiseNumber={premise_number}" + "&accountNumber={account_number}" + "&isTouUser={is_tou}" + "&startDate={start_date}" + "&endDate={end_date}" + "&userType=EXT" + "&isResidential=true" + "&certifiedDate=2000/01/01" + "&viewType=dollar" + "&tempType=max" + "&ecDayHumType=NoHum" + ).format( premise_number=self._premise_number, account_number=self._account_number, is_tou=str(self._is_tou), diff --git a/custom_components/fpl/sensor.py b/custom_components/fpl/sensor.py index 1acf304..ffe4ebd 100644 --- a/custom_components/fpl/sensor.py +++ b/custom_components/fpl/sensor.py @@ -7,7 +7,7 @@ from homeassistant.helpers.entity import Entity from homeassistant import util from homeassistant.const import CONF_NAME, EVENT_CORE_CONFIG_UPDATE -from .const import DOMAIN, ICON +from .const import DOMAIN, ICON, LOGIN_RESULT_OK MIN_TIME_BETWEEN_SCANS = timedelta(minutes=1440) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=1440) @@ -24,6 +24,24 @@ def setup_platform(hass, config, add_devices, discovery_info=None): async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities([FplSensor(hass, config_entry.data)]) + print("setup entry") + + username = config_entry.data.get(const.CONF_USERNAME) + password = config_entry.data.get(const.CONF_PASSWORD) + + session = aiohttp.ClientSession() + try: + api = FplApi(username, password, True, hass.loop, session) + result = await api.login() + + if result == LOGIN_RESULT_OK: + await api.async_get_headers() + pass + + except Exception: # pylint: disable=broad-except + pass + + await session.close() class FplSensor(Entity): @@ -69,18 +87,22 @@ class FplSensor(Entity): # "yesterday_kwh": self.api.yesterday_kwh, # "yesterday_dollars": self.api.yesterday_dollars.replace("$", ""), "mtd_kwh": self.api.mtd_kwh, - "mtd_dollars": self.api.mtd_dollars.replace("$", ""), + "mtd_dollars": self.api.mtd_dollars, "projected_bill": self.api.projected_bill, } @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_UPDATES) async def async_update(self): session = aiohttp.ClientSession() - api = FplApi(self.username, self.password, True, self.loop, session) - await api.login() - # await api.async_get_yesterday_usage() - await api.async_get_mtd_usage() - await session.close() + try: + api = FplApi(self.username, self.password, True, self.loop, session) + await api.login() + # await api.async_get_yesterday_usage() + await api.async_get_mtd_usage() + self._state = api.projected_bill + self.api = api - self._state = api.projected_bill - self.api = api + except Exception: # pylint: disable=broad-except + pass + + await session.close() From 0e4ce1ba3fa5bf791ad50a947e7c5b70a6b05214 Mon Sep 17 00:00:00 2001 From: Yordan Suarez Date: Tue, 31 Dec 2019 12:28:42 -0500 Subject: [PATCH 2/2] reworked fplApi --- .gitignore | 6 + .vscode/launch.json | 16 -- custom_components/fpl/config_flow.py | 6 +- custom_components/fpl/fplapi.py | 251 +++++++++++---------------- custom_components/fpl/sensor.py | 65 ++++--- 5 files changed, 137 insertions(+), 207 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.gitignore b/.gitignore index b2afc79..e719a55 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,8 @@ custom_components/fpl/test.py + +custom_components/fpl/__pycache__/ + +.vscode/launch.json + +test.py diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index fba2e38..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Current File", - "type": "python", - "request": "launch", - //"program": "/workspaces/hass-fpl/custom_components/fpl/test.py", - "program": "E:/projects/hass-fpl/custom_components/fpl/test.py", - "console": "integratedTerminal" - } - ] -} \ No newline at end of file diff --git a/custom_components/fpl/config_flow.py b/custom_components/fpl/config_flow.py index e248778..29eb42a 100644 --- a/custom_components/fpl/config_flow.py +++ b/custom_components/fpl/config_flow.py @@ -34,9 +34,7 @@ class FplFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Initialize.""" self._errors = {} - async def async_step_user( - self, user_input={} - ): # pylint: disable=dangerous-default-value + async def async_step_user(self, user_input={}): # pylint: disable=dangerous-default-value """Handle a flow initialized by the user.""" self._errors = {} @@ -102,7 +100,7 @@ class FplFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Return true if credentials is valid.""" session = aiohttp.ClientSession() try: - api = FplApi(username, password, True, None, session) + api = FplApi(username, password, None, session) result = await api.login() except Exception: # pylint: disable=broad-except pass diff --git a/custom_components/fpl/fplapi.py b/custom_components/fpl/fplapi.py index 8a8e48e..657fa59 100644 --- a/custom_components/fpl/fplapi.py +++ b/custom_components/fpl/fplapi.py @@ -9,11 +9,16 @@ import json from bs4 import BeautifulSoup -from const import STATUS_CATEGORY_OPEN, LOGIN_RESULT_OK +from .const import STATUS_CATEGORY_OPEN, LOGIN_RESULT_OK _LOGGER = logging.getLogger(__name__) TIMEOUT = 5 +URL_LOGIN = "https://www.fpl.com/api/resources/login" +URL_RESOURCES_HEADER = "https://www.fpl.com/api/resources/header" +URL_RESOURCES_ACCOUNT = "https://www.fpl.com/api/resources/account/{account}" +URL_RESOURCES_PROJECTED_BILL = "https://www.fpl.com/api/resources/account/{account}/projectedBill?premiseNumber={premise}&lastBilledDate={lastBillDate}" + class FplApi(object): """A class for getting energy usage information from Florida Power & Light.""" @@ -25,13 +30,49 @@ class FplApi(object): self._loop = loop self._session = session + async def async_get_mtd_usage(self): + async with async_timeout.timeout(TIMEOUT, loop=self._loop): + response = await self._session.get( + "https://app.fpl.com/wps/myportal/EsfPortal" + ) + + soup = BeautifulSoup(await response.text(), "html.parser") + + self.mtd_kwh = ( + soup.find(id="bpbsubcontainer") + .find("table", class_="bpbtab_style_bill", width=430) + .find_all("div", class_="bpbtabletxt")[-1] + .string + ) + + self.mtd_dollars = ( + soup.find_all("div", class_="bpbusagebgnd")[1] + .find("div", class_="bpbusagedollartxt") + .getText() + .strip() + .replace("$", "") + ) + + self.projected_bill = ( + soup.find(id="bpssmlsubcontainer") + .find("div", class_="bpsmonthbillbgnd") + .find("div", class_="bpsmnthbilldollartxt") + .getText() + .strip() + .replace("$", "") + ) + + test = soup.find( + class_="bpsusagesmlmnthtxt").getText().strip().split(" - ") + self.start_period = test[0] + self.end_period = test[1] + + + async def login(self): """login and get account information""" async with async_timeout.timeout(TIMEOUT, loop=self._loop): - response = await self._session.get( - "https://www.fpl.com/api/resources/login", - auth=aiohttp.BasicAuth(self._username, self._password), - ) + response = await self._session.get(URL_LOGIN, auth=aiohttp.BasicAuth(self._username, self._password)) js = json.loads(await response.text()) @@ -45,9 +86,8 @@ class FplApi(object): async def async_get_open_accounts(self): async with async_timeout.timeout(TIMEOUT, loop=self._loop): - response = await self._session.get( - "https://www.fpl.com/api/resources/header" - ) + response = await self._session.get(URL_RESOURCES_HEADER) + js = await response.json() accounts = js["data"]["accounts"]["data"]["data"] @@ -56,51 +96,67 @@ class FplApi(object): for account in accounts: if account["statusCategory"] == STATUS_CATEGORY_OPEN: result.append(account["accountNumber"]) - # print(account["accountNumber"]) - # print(account["premiseNumber"]) - # print(account["statusCategory"]) + # print(account["accountNumber"]) + # print(account["premiseNumber"]) + # print(account["statusCategory"]) # self._account_number = js["data"]["selectedAccount"]["data"]["accountNumber"] # self._premise_number = js["data"]["selectedAccount"]["data"]["acctSecSettings"]["premiseNumber"] return result async def async_get_data(self, account): - # await self.async_get_yesterday_usage() - # await self.async_get_mtd_usage() + + data = {} + async with async_timeout.timeout(TIMEOUT, loop=self._loop): - response = await self._session.get( - "https://www.fpl.com/api/resources/account/" + account - ) - data = (await response.json())["data"] + response = await self._session.get(URL_RESOURCES_ACCOUNT.format(account=account)) + accountData = (await response.json())["data"] - premise = data["premiseNumber"].zfill(9) - print(premise) + premise = accountData["premiseNumber"].zfill(9) - # print(data["nextBillDate"].replace("-", "").split("T")[0]) - # print(data["currentBillDate"].replace("-", "").split("T")[0]) + # currentBillDate + currentBillDate = datetime.strptime( + accountData["currentBillDate"].replace( + "-", "").split("T")[0], "%Y%m%d" + ).date() - start_date = datetime.strptime( - data["currentBillDate"].replace("-", "").split("T")[0], "%Y%m%d").date() - end_date = dt.today() + # nextBillDate + nextBillDate = datetime.strptime( + accountData["nextBillDate"].replace( + "-", "").split("T")[0], "%Y%m%d" + ).date() - last_day = datetime.strptime( - data["nextBillDate"].replace("-", "").split("T")[0], "%Y%m%d").date() + # zip code + zip_code = accountData["serviceAddress"]["zip"] - lasting_days = (last_day - dt.today()).days - zip_code = data["serviceAddress"]["zip"] + 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") + )) + + projectedBillData = (await response.json())["data"] + + serviceDays = int(projectedBillData["serviceDays"]) + billToDate = float(projectedBillData["billToDate"]) + projectedBill = float(projectedBillData["projectedBill"]) + asOfDays = int(projectedBillData["asOfDays"]) + dailyAvg = float(projectedBillData["dailyAvg"]) + avgHighTemp = int(projectedBillData["avgHighTemp"]) url = ( "https://app.fpl.com/wps/PA_ESFPortalWeb/getDailyConsumption" f"?premiseNumber={premise}" - f"&startDate={start_date.strftime('%Y%m%d')}" - f"&endDate={end_date.strftime('%Y%m%d')}" + f"&startDate={currentBillDate.strftime('%Y%m%d')}" + f"&endDate={dt.today().strftime('%Y%m%d')}" f"&accountNumber={account}" # "&accountType=ELE" f"&zipCode={zip_code}" "&consumption=0.0" "&usage=0.0" "&isMultiMeter=false" - f"&lastAvailableDate={end_date}" + f"&lastAvailableDate={dt.today()}" # "&isAmiMeter=true" "&userType=EXT" # "¤tReading=64359" @@ -161,133 +217,26 @@ class FplApi(object): days += 1 day_detail = {} - day_detail["date"] = date + day_detail["date"] = str(date) day_detail["usage"] = usage day_detail["cost"] = cost day_detail["max_temperature"] = max_temp details.append(day_detail) - print(date) - print(usage) - print(cost) - print(max_temp) - - print("TOTALS") - print(total_kw) - print(total_cost) - - print("Average") - avg_cost = round(total_cost / days, 2) - print(avg_cost) + remaining_days = serviceDays - asOfDays avg_kw = round(total_kw / days, 0) - print(avg_kw) - print("Projected") - projected_cost = round(total_cost + avg_cost * lasting_days, 2) - print(projected_cost) - - data = {} - data["start_date"] = start_date - data["end_date"] = end_date - data["service_days"] = (end_date - start_date).days - data["current_days"] = days - data["remaining_days"] = lasting_days - data["details"] = details + data["current_bill_date"] = str(currentBillDate) + data["next_bill_date"] = str(nextBillDate) + data["service_days"] = serviceDays + data["bill_to_date"] = billToDate + data["projected_bill"] = projectedBill + data["as_of_days"] = asOfDays + data["daily_avg"] = dailyAvg + data["avg_high_temp"] = avgHighTemp + data["remaining_days"] = remaining_days + data["mtd_kwh"] = total_kw + data["average_kwh"] = avg_kw return data - pass - - async def async_get_yesterday_usage(self): - async with async_timeout.timeout(TIMEOUT, loop=self._loop): - url = self._build_daily_url() - response = await self._session.get(url) - - _LOGGER.debug("Response from API: %s", response.status) - - if response.status != 200: - self.data = None - return - - malformedXML = await response.read() - - cleanerXML = ( - str(malformedXML) - .replace('', "", 1) - .split("@@", 1)[0] - ) - - soup = BeautifulSoup(cleanerXML, "html.parser") - - tool_text = soup.find("dataset", seriesname="$").find("set")[ - "tooltext"] - - match = re.search(r"\{br\}kWh Usage: (.*?) kWh \{br\}", tool_text) - if match: - self.yesterday_kwh = match.group(1).replace("$", "") - - match2 = re.search(r"\{br\}Approx\. Cost: (\$.*?) \{br\}", tool_text) - if match2: - self.yesterday_dollars = match2.group(1).replace("$", "") - - async def async_get_mtd_usage(self): - async with async_timeout.timeout(TIMEOUT, loop=self._loop): - response = await self._session.get( - "https://app.fpl.com/wps/myportal/EsfPortal" - ) - - soup = BeautifulSoup(await response.text(), "html.parser") - - self.mtd_kwh = ( - soup.find(id="bpbsubcontainer") - .find("table", class_="bpbtab_style_bill", width=430) - .find_all("div", class_="bpbtabletxt")[-1] - .string - ) - - self.mtd_dollars = ( - soup.find_all("div", class_="bpbusagebgnd")[1] - .find("div", class_="bpbusagedollartxt") - .getText() - .strip() - .replace("$", "") - ) - - self.projected_bill = ( - soup.find(id="bpssmlsubcontainer") - .find("div", class_="bpsmonthbillbgnd") - .find("div", class_="bpsmnthbilldollartxt") - .getText() - .strip() - .replace("$", "") - ) - - test = soup.find( - class_="bpsusagesmlmnthtxt").getText().strip().split(" - ") - self.start_period = test[0] - self.end_period = test[1] - - def _build_daily_url(self): - end_date = dt.today() - start_date = end_date - timedelta(days=1) - - return ( - "https://app.fpl.com/wps/PA_ESFPortalWeb/getDailyConsumption" - "?premiseNumber={premise_number}" - "&accountNumber={account_number}" - "&isTouUser={is_tou}" - "&startDate={start_date}" - "&endDate={end_date}" - "&userType=EXT" - "&isResidential=true" - "&certifiedDate=2000/01/01" - "&viewType=dollar" - "&tempType=max" - "&ecDayHumType=NoHum" - ).format( - premise_number=self._premise_number, - account_number=self._account_number, - is_tou=str(self._is_tou), - start_date=start_date.strftime("%Y%m%d"), - end_date=end_date.strftime("%Y%m%d"), - ) diff --git a/custom_components/fpl/sensor.py b/custom_components/fpl/sensor.py index ffe4ebd..aed8584 100644 --- a/custom_components/fpl/sensor.py +++ b/custom_components/fpl/sensor.py @@ -17,25 +17,20 @@ def setup(hass, config): return True -def setup_platform(hass, config, add_devices, discovery_info=None): - setup(hass, config) - add_devices([FplSensor(hass, config)]) - - async def async_setup_entry(hass, config_entry, async_add_entities): - async_add_entities([FplSensor(hass, config_entry.data)]) - print("setup entry") - username = config_entry.data.get(const.CONF_USERNAME) password = config_entry.data.get(const.CONF_PASSWORD) session = aiohttp.ClientSession() try: - api = FplApi(username, password, True, hass.loop, session) + api = FplApi(username, password, hass.loop, session) result = await api.login() if result == LOGIN_RESULT_OK: - await api.async_get_headers() + accounts = await api.async_get_open_accounts() + for account in accounts: + async_add_entities( + [FplSensor(hass, config_entry.data, account)]) pass except Exception: # pylint: disable=broad-except @@ -45,37 +40,31 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class FplSensor(Entity): - def __init__(self, hass, config): + def __init__(self, hass, config, account): self._config = config self.username = config.get(const.CONF_USERNAME) self.password = config.get(const.CONF_PASSWORD) self._state = 0 self.loop = hass.loop - self.api = None - async def _core_config_updated(self, _event): - """Handle core config updated.""" - print("Core config updated") - # self._init_data() - # if self._unsub_fetch_data: - # self._unsub_fetch_data() - # self._unsub_fetch_data = None - # await self._fetch_data() + self._account = account + self._data = None async def async_added_to_hass(self): await self.async_update() @property - def name(self): - name = self._config.get(CONF_NAME) - if name is not None: - return f"{DOMAIN.upper()} {name}" + def unique_id(self): + """Return the ID of this device.""" + return "{}{}".format(self._account, hash(self._account)) - return DOMAIN + @property + def name(self): + return f"{DOMAIN.upper()} {self._account}" @property def state(self): - return self._state + return self._data["bill_to_date"] @property def icon(self): @@ -83,24 +72,28 @@ class FplSensor(Entity): @property def state_attributes(self): + return self._data + """ return { - # "yesterday_kwh": self.api.yesterday_kwh, - # "yesterday_dollars": self.api.yesterday_dollars.replace("$", ""), - "mtd_kwh": self.api.mtd_kwh, - "mtd_dollars": self.api.mtd_dollars, - "projected_bill": self.api.projected_bill, + "mtd_kwh": self._data["mtd_kwh"], + "bill_to_date": self._data["bill_to_date"], + "projected_bill": self._data["projected_bill"], + # "details": self._data["details"], + "start_date": self._data["start_date"], + "end_date": self._data["end_date"], + "service_days": self._data["service_days"], + "current_days": self._data["current_days"], + "remaining_days": self._data["remaining_days"], } + """ @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_UPDATES) async def async_update(self): session = aiohttp.ClientSession() try: - api = FplApi(self.username, self.password, True, self.loop, session) + api = FplApi(self.username, self.password, self.loop, session) await api.login() - # await api.async_get_yesterday_usage() - await api.async_get_mtd_usage() - self._state = api.projected_bill - self.api = api + self._data = await api.async_get_data(self._account) except Exception: # pylint: disable=broad-except pass