From 0e4ce1ba3fa5bf791ad50a947e7c5b70a6b05214 Mon Sep 17 00:00:00 2001 From: Yordan Suarez Date: Tue, 31 Dec 2019 12:28:42 -0500 Subject: [PATCH] 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