bunch of changes

This commit is contained in:
Yordan Suarez
2019-12-30 12:56:11 -05:00
parent 48e945c6a5
commit d11df67c18
8 changed files with 322 additions and 76 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
custom_components/fpl/test.py

16
.vscode/launch.json vendored Normal file
View File

@@ -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"
}
]
}

5
.vscode/settings.json vendored Normal file
View File

@@ -0,0 +1,5 @@
{
"files.associations": {
"*.yaml": "home-assistant"
}
}

View File

@@ -14,7 +14,9 @@
}, },
"error": { "error": {
"auth": "Username/Password is wrong.", "auth": "Username/Password is wrong.",
"name_exists": "Configuration already exists." "name_exists": "Configuration already exists.",
"invalid_username": "Invalid Username",
"invalid_password": "Invalid Password"
}, },
"abort": { "abort": {
"single_instance_allowed": "Only a single configuration of Fpl is allowed." "single_instance_allowed": "Only a single configuration of Fpl is allowed."

View File

@@ -4,7 +4,15 @@ import voluptuous as vol
from .fplapi import FplApi from .fplapi import FplApi
from homeassistant import config_entries from homeassistant import config_entries
import aiohttp 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 from homeassistant.core import callback
@@ -32,24 +40,32 @@ class FplFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a flow initialized by the user.""" """Handle a flow initialized by the user."""
self._errors = {} self._errors = {}
# if self._async_current_entries(): if self._async_current_entries():
# return self.async_abort(reason="single_instance_allowed") return self.async_abort(reason="single_instance_allowed")
# if self.hass.data.get(DOMAIN): if self.hass.data.get(DOMAIN):
# return self.async_abort(reason="single_instance_allowed") return self.async_abort(reason="single_instance_allowed")
if user_input is not None: if user_input is not None:
username = user_input[CONF_USERNAME] username = user_input[CONF_USERNAME]
password = user_input[CONF_PASSWORD] password = user_input[CONF_PASSWORD]
if username not in configured_instances(self.hass): 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( return self.async_create_entry(
title=user_input[CONF_NAME], data=user_input 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" self._errors["base"] = "auth"
else: else:
self._errors[CONF_NAME] = "name_exists" self._errors[CONF_NAME] = "name_exists"
@@ -84,11 +100,12 @@ class FplFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
async def _test_credentials(self, username, password): async def _test_credentials(self, username, password):
"""Return true if credentials is valid.""" """Return true if credentials is valid."""
try:
session = aiohttp.ClientSession() session = aiohttp.ClientSession()
try:
api = FplApi(username, password, True, None, session) api = FplApi(username, password, True, None, session)
await api.login() result = await api.login()
return True
except Exception: # pylint: disable=broad-except except Exception: # pylint: disable=broad-except
pass pass
return False
await session.close()
return result

View File

@@ -33,3 +33,11 @@ CONF_PASSWORD = "password"
# Defaults # Defaults
DEFAULT_NAME = DOMAIN DEFAULT_NAME = DOMAIN
# Api login result
LOGIN_RESULT_OK = "OK"
LOGIN_RESULT_INVALIDUSER = "NOTVALIDUSER"
LOGIN_RESULT_INVALIDPASSWORD = "FAILEDPASSWORD"
STATUS_CATEGORY_OPEN = "OPEN"

View File

@@ -1,11 +1,15 @@
import asyncio import asyncio
import logging import logging
import re import re
from datetime import timedelta, date from datetime import timedelta, datetime, date as dt
import aiohttp import aiohttp
import async_timeout import async_timeout
import json
from bs4 import BeautifulSoup from bs4 import BeautifulSoup
from const import STATUS_CATEGORY_OPEN, LOGIN_RESULT_OK
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
TIMEOUT = 5 TIMEOUT = 5
@@ -14,40 +18,190 @@ TIMEOUT = 5
class FplApi(object): class FplApi(object):
"""A class for getting energy usage information from Florida Power & Light.""" """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.""" """Initialize the data retrieval. Session should have BasicAuth flag set."""
self._username = username self._username = username
self._password = password self._password = password
self._loop = loop self._loop = loop
self._session = session 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): async def login(self):
"""login and get account information""" """login and get account information"""
async with async_timeout.timeout(TIMEOUT, loop=self._loop): async with async_timeout.timeout(TIMEOUT, loop=self._loop):
response = await self._session.get("https://www.fpl.com/api/resources/login", response = await self._session.get(
auth=aiohttp.BasicAuth(self._username, self._password)) "https://www.fpl.com/api/resources/login",
auth=aiohttp.BasicAuth(self._username, self._password),
)
if (await response.json())["messages"][0]["messageCode"] != "login.success": js = json.loads(await response.text())
raise Exception('login failure')
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"
# "&currentReading=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): 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)
json = await response.json()
self._account_number = json["data"]["selectedAccount"]["data"]["accountNumber"] if response.status != 200:
self._premise_number = json["data"]["selectedAccount"]["data"]["acctSecSettings"]["premiseNumber"] self.data = None
return
malformedXML = await response.read()
cleanerXML = (
str(malformedXML)
.replace('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', "", 1)
.split("<ARG>@@", 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:
# <set color="E99356" link="j-hourlyJs-001955543,2019/12/29T00:00:00,2019/12/29T00:00:00,8142998577,residential,null,20191206,20200108" tooltext="Day/Date: Dec. 29, 2019 {br}kWh Usage: 42 kWh {br}Approx. Cost: $4.42 {br}Daily High Temp: \xc2\xb0F " value="4.42"></set>
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 def async_get_yesterday_usage(self):
async with async_timeout.timeout(TIMEOUT, loop=self._loop): 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) _LOGGER.debug("Response from API: %s", response.status)
@@ -57,48 +211,68 @@ class FplApi(object):
malformedXML = await response.read() malformedXML = await response.read()
cleanerXML = str(malformedXML).replace( cleanerXML = (
'<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', '', 1 str(malformedXML)
).split("<ARG>@@", 1)[0] .replace('<?xml version="1.0" encoding="UTF-8" standalone="yes"?>', "", 1)
.split("<ARG>@@", 1)[0]
)
soup = BeautifulSoup(cleanerXML, 'html.parser') soup = BeautifulSoup(cleanerXML, "html.parser")
tool_text = soup.find("dataset", seriesname="$") \ tool_text = soup.find("dataset", seriesname="$").find("set")[
.find("set")["tooltext"] "tooltext"]
match = re.search(r"\{br\}kWh Usage: (.*?) kWh \{br\}", tool_text) match = re.search(r"\{br\}kWh Usage: (.*?) kWh \{br\}", tool_text)
if match: 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) match2 = re.search(r"\{br\}Approx\. Cost: (\$.*?) \{br\}", tool_text)
if match2: if match2:
self.yesterday_dollars = match2.group(1) self.yesterday_dollars = match2.group(1).replace("$", "")
async def async_get_mtd_usage(self): async def async_get_mtd_usage(self):
async with async_timeout.timeout(TIMEOUT, loop=self._loop): async with async_timeout.timeout(TIMEOUT, loop=self._loop):
response = await self._session.get( 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") \ self.mtd_kwh = (
.find("table", class_="bpbtab_style_bill", width=430) \ soup.find(id="bpbsubcontainer")
.find_all("div", class_="bpbtabletxt")[-1].string .find("table", class_="bpbtab_style_bill", width=430)
.find_all("div", class_="bpbtabletxt")[-1]
.string
)
self.mtd_dollars = soup \ self.mtd_dollars = (
.find_all("div", class_="bpbusagebgnd")[1] \ soup.find_all("div", class_="bpbusagebgnd")[1]
.find("div", class_="bpbusagedollartxt").getText().strip() .find("div", class_="bpbusagedollartxt")
.getText()
.strip()
.replace("$", "")
)
self.projected_bill = soup.find(id="bpssmlsubcontainer") \ self.projected_bill = (
.find("div", class_="bpsmonthbillbgnd") \ soup.find(id="bpssmlsubcontainer")
.find("div", class_="bpsmnthbilldollartxt") \ .find("div", class_="bpsmonthbillbgnd")
.getText().strip().replace("$", "") .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): def _build_daily_url(self):
end_date = date.today() end_date = dt.today()
start_date = end_date - timedelta(days=1) start_date = end_date - timedelta(days=1)
return ("https://app.fpl.com/wps/PA_ESFPortalWeb/getDailyConsumption" return (
"https://app.fpl.com/wps/PA_ESFPortalWeb/getDailyConsumption"
"?premiseNumber={premise_number}" "?premiseNumber={premise_number}"
"&accountNumber={account_number}" "&accountNumber={account_number}"
"&isTouUser={is_tou}" "&isTouUser={is_tou}"

View File

@@ -7,7 +7,7 @@ from homeassistant.helpers.entity import Entity
from homeassistant import util from homeassistant import util
from homeassistant.const import CONF_NAME, EVENT_CORE_CONFIG_UPDATE 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_SCANS = timedelta(minutes=1440)
MIN_TIME_BETWEEN_UPDATES = 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 def async_setup_entry(hass, config_entry, async_add_entities):
async_add_entities([FplSensor(hass, config_entry.data)]) 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): class FplSensor(Entity):
@@ -69,18 +87,22 @@ class FplSensor(Entity):
# "yesterday_kwh": self.api.yesterday_kwh, # "yesterday_kwh": self.api.yesterday_kwh,
# "yesterday_dollars": self.api.yesterday_dollars.replace("$", ""), # "yesterday_dollars": self.api.yesterday_dollars.replace("$", ""),
"mtd_kwh": self.api.mtd_kwh, "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, "projected_bill": self.api.projected_bill,
} }
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_UPDATES) @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_UPDATES)
async def async_update(self): async def async_update(self):
session = aiohttp.ClientSession() session = aiohttp.ClientSession()
try:
api = FplApi(self.username, self.password, True, self.loop, session) api = FplApi(self.username, self.password, True, self.loop, session)
await api.login() await api.login()
# await api.async_get_yesterday_usage() # await api.async_get_yesterday_usage()
await api.async_get_mtd_usage() await api.async_get_mtd_usage()
await session.close()
self._state = api.projected_bill self._state = api.projected_bill
self.api = api self.api = api
except Exception: # pylint: disable=broad-except
pass
await session.close()