rework fpl api

This commit is contained in:
Yordan Suarez
2022-08-02 11:10:57 -04:00
parent a1dbe51f10
commit a9d343f1f5
16 changed files with 1129 additions and 489 deletions

View File

@@ -0,0 +1,388 @@
"""FPL Main region data collection api client"""
import json
import logging
from datetime import datetime, timedelta
import aiohttp
import async_timeout
from .const import (
API_HOST,
LOGIN_RESULT_FAILURE,
LOGIN_RESULT_INVALIDPASSWORD,
LOGIN_RESULT_INVALIDUSER,
LOGIN_RESULT_OK,
TIMEOUT,
)
STATUS_CATEGORY_OPEN = "OPEN"
URL_LOGIN = API_HOST + "/api/resources/login"
URL_BUDGET_BILLING_GRAPH = (
API_HOST + "/api/resources/account/{account}/budgetBillingGraph"
)
URL_RESOURCES_PROJECTED_BILL = (
API_HOST
+ "/api/resources/account/{account}/projectedBill"
+ "?premiseNumber={premise}&lastBilledDate={lastBillDate}"
)
URL_APPLIANCE_USAGE = (
API_HOST + "/dashboard-api/resources/account/{account}/applianceUsage/{account}"
)
URL_BUDGET_BILLING_PREMISE_DETAILS = (
API_HOST + "/api/resources/account/{account}/budgetBillingGraph/premiseDetails"
)
ENROLLED = "ENROLLED"
NOTENROLLED = "NOTENROLLED"
_LOGGER = logging.getLogger(__package__)
class FplMainRegionApiClient:
"""Fpl Main Region Api Client"""
def __init__(self, username, password, loop, session) -> None:
self.session = session
self.username = username
self.password = password
self.loop = loop
async def login(self):
"""login into fpl"""
# login and get account information
async with async_timeout.timeout(TIMEOUT):
response = await self.session.get(
URL_LOGIN,
auth=aiohttp.BasicAuth(self.username, self.password),
)
if response.status == 200:
return LOGIN_RESULT_OK
if response.status == 401:
json_data = json.loads(await response.text())
if json_data["messageCode"] == LOGIN_RESULT_INVALIDUSER:
return LOGIN_RESULT_INVALIDUSER
if json_data["messageCode"] == LOGIN_RESULT_INVALIDPASSWORD:
return LOGIN_RESULT_INVALIDPASSWORD
return LOGIN_RESULT_FAILURE
async def get_open_accounts(self):
"""
Get open accounts
Returns array with active account numbers
"""
result = []
URL = API_HOST + "/api/resources/header"
async with async_timeout.timeout(TIMEOUT):
response = await self.session.get(URL)
json_data = await response.json()
accounts = json_data["data"]["accounts"]["data"]["data"]
for account in accounts:
if account["statusCategory"] == STATUS_CATEGORY_OPEN:
result.append(account["accountNumber"])
return result
async def logout(self):
"""Logging out from fpl"""
_LOGGER.info("Logging out")
URL_LOGOUT = API_HOST + "/api/resources/logout"
try:
async with async_timeout.timeout(TIMEOUT):
await self.session.get(URL_LOGOUT)
except Exception:
pass
async def update(self, account) -> dict:
"""Get data from resources endpoint"""
data = {}
URL_RESOURCES_ACCOUNT = API_HOST + "/api/resources/account/{account}"
async with async_timeout.timeout(TIMEOUT):
response = await self.session.get(
URL_RESOURCES_ACCOUNT.format(account=account)
)
account_data = (await response.json())["data"]
premise = account_data.get("premiseNumber").zfill(9)
data["meterSerialNo"] = account_data["meterSerialNo"]
# currentBillDate
currentBillDate = datetime.strptime(
account_data["currentBillDate"].replace("-", "").split("T")[0], "%Y%m%d"
).date()
# nextBillDate
nextBillDate = datetime.strptime(
account_data["nextBillDate"].replace("-", "").split("T")[0], "%Y%m%d"
).date()
data["current_bill_date"] = str(currentBillDate)
data["next_bill_date"] = str(nextBillDate)
today = datetime.now().date()
data["service_days"] = (nextBillDate - currentBillDate).days
data["as_of_days"] = (today - currentBillDate).days
data["remaining_days"] = (nextBillDate - today).days
# zip code
# zip_code = accountData["serviceAddress"]["zip"]
# projected bill
pbData = await self.__getFromProjectedBill(account, premise, currentBillDate)
data.update(pbData)
# programs
programsData = account_data["programs"]["data"]
programs = dict()
_LOGGER.info("Getting Programs")
for program in programsData:
if "enrollmentStatus" in program.keys():
key = program["name"]
programs[key] = program["enrollmentStatus"] == ENROLLED
def hasProgram(programName) -> bool:
return programName in programs and programs[programName]
# Budget Billing program
if hasProgram("BBL"):
data["budget_bill"] = True
bbl_data = await self.__getBBL_async(account, data)
data.update(bbl_data)
else:
data["budget_bill"] = False
# Get data from energy service
data.update(
await self.__getDataFromEnergyService(account, premise, currentBillDate)
)
# Get data from energy service ( hourly )
# data.update(
# await self.__getDataFromEnergyServiceHourly(
# account, premise, currentBillDate
# )
# )
data.update(await self.__getDataFromApplianceUsage(account, currentBillDate))
return data
async def __getFromProjectedBill(self, account, premise, currentBillDate) -> dict:
"""get data from projected bill endpoint"""
data = {}
try:
async with async_timeout.timeout(TIMEOUT):
response = await self.session.get(
URL_RESOURCES_PROJECTED_BILL.format(
account=account,
premise=premise,
lastBillDate=currentBillDate.strftime("%m%d%Y"),
)
)
if response.status == 200:
projectedBillData = (await response.json())["data"]
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
except Exception:
pass
return data
async def __getBBL_async(self, account, projectedBillData) -> dict:
"""Get budget billing data"""
_LOGGER.info("Getting budget billing data")
data = {}
try:
async with async_timeout.timeout(TIMEOUT):
response = await self.session.get(
URL_BUDGET_BILLING_PREMISE_DETAILS.format(account=account)
)
if response.status == 200:
r = (await response.json())["data"]
dataList = r["graphData"]
# startIndex = len(dataList) - 1
billingCharge = 0
budgetBillDeferBalance = r["defAmt"]
projectedBill = projectedBillData["projected_bill"]
asOfDays = projectedBillData["as_of_days"]
for det in dataList:
billingCharge += det["actuallBillAmt"]
calc1 = (projectedBill + billingCharge) / 12
calc2 = (1 / 12) * (budgetBillDeferBalance)
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_projected_bill"] = float(projectedBudgetBill)
except Exception as e:
_LOGGER.error("Error getting BBL: %s", e)
try:
async with async_timeout.timeout(TIMEOUT):
response = await self.session.get(
URL_BUDGET_BILLING_GRAPH.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 Exception as e:
_LOGGER.error("Error getting BBL: %s", e)
return data
async def __getDataFromEnergyService(
self, account, premise, lastBilledDate
) -> dict:
_LOGGER.info("Getting data from energy service")
date = str(lastBilledDate.strftime("%m%d%Y"))
JSON = {
"recordCount": 24,
"status": 2,
"channel": "WEB",
"amrFlag": "Y",
"accountType": "RESIDENTIAL",
"revCode": "1",
"premiseNumber": premise,
"projectedBillFlag": True,
"billComparisionFlag": True,
"monthlyFlag": True,
"frequencyType": "Daily",
"lastBilledDate": date,
"applicationPage": "resDashBoard",
}
URL_ENERGY_SERVICE = (
API_HOST
+ "/dashboard-api/resources/account/{account}/energyService/{account}"
)
data = {}
try:
async with async_timeout.timeout(TIMEOUT):
response = await self.session.post(
URL_ENERGY_SERVICE.format(account=account), json=JSON
)
if response.status == 200:
r = (await response.json())["data"]
dailyUsage = []
# totalPowerUsage = 0
if "data" in r["DailyUsage"]:
for daily in r["DailyUsage"]["data"]:
if (
"kwhUsed" in daily.keys()
and "billingCharge" in daily.keys()
and "date" in daily.keys()
and "averageHighTemperature" in daily.keys()
):
dailyUsage.append(
{
"usage": daily["kwhUsed"],
"cost": daily["billingCharge"],
# "date": daily["date"],
"max_temperature": daily[
"averageHighTemperature"
],
"netDeliveredKwh": daily["netDeliveredKwh"]
if "netDeliveredKwh" in daily.keys()
else 0,
"netReceivedKwh": daily["netReceivedKwh"]
if "netReceivedKwh" in daily.keys()
else 0,
"readTime": datetime.fromisoformat(
daily[
"readTime"
] # 2022-02-25T00:00:00.000-05:00
),
}
)
# totalPowerUsage += int(daily["kwhUsed"])
# data["total_power_usage"] = totalPowerUsage
data["daily_usage"] = dailyUsage
data["projectedKWH"] = r["CurrentUsage"]["projectedKWH"]
data["dailyAverageKWH"] = float(
r["CurrentUsage"]["dailyAverageKWH"]
)
data["billToDateKWH"] = float(r["CurrentUsage"]["billToDateKWH"])
data["recMtrReading"] = int(r["CurrentUsage"]["recMtrReading"])
data["delMtrReading"] = int(r["CurrentUsage"]["delMtrReading"])
data["billStartDate"] = r["CurrentUsage"]["billStartDate"]
except:
pass
return data
async def __getDataFromApplianceUsage(self, account, lastBilledDate) -> dict:
"""get data from appliance usage"""
_LOGGER.info("Getting data from appliance usage")
JSON = {"startDate": str(lastBilledDate.strftime("%m%d%Y"))}
data = {}
try:
async with async_timeout.timeout(TIMEOUT):
response = await self.session.post(
URL_APPLIANCE_USAGE.format(account=account), json=JSON
)
if response.status == 200:
electric = (await response.json())["data"]["electric"]
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
except Exception:
pass
return {"energy_percent_by_applicance": data}

View File

@@ -0,0 +1,167 @@
"""FPL Northwest data collection api client"""
from datetime import datetime
import logging
import async_timeout
import boto3
from .const import TIMEOUT, API_HOST
from .aws_srp import AWSSRP
from .const import LOGIN_RESULT_OK
USER_POOL_ID = "us-east-1_w09KCowou"
CLIENT_ID = "4k78t7970hhdgtafurk158dr3a"
ACCOUNT_STATUS_ACTIVE = "ACT"
_LOGGER = logging.getLogger(__package__)
class FplNorthwestRegionApiClient:
"""FPL Northwest Api client"""
def __init__(self, username, password, loop, session) -> None:
self.session = session
self.username = username
self.password = password
self.loop = loop
self.id_token = None
self.access_token = None
self.refresh_token = None
async def login(self):
"""login using aws"""
client = await self.loop.run_in_executor(
None, boto3.client, "cognito-idp", "us-east-1"
)
aws = AWSSRP(
username=self.username,
password=self.password,
pool_id=USER_POOL_ID,
client_id=CLIENT_ID,
loop=self.loop,
client=client,
)
tokens = await aws.authenticate_user()
if "AccessToken" in tokens["AuthenticationResult"]:
self.access_token = tokens["AuthenticationResult"]["AccessToken"]
self.refresh_token = tokens["AuthenticationResult"]["RefreshToken"]
self.id_token = tokens["AuthenticationResult"]["IdToken"]
# Get User
headers = {
"Content-Type": "application/x-amz-json-1.1",
"X-Amz-Target": "AWSCognitoIdentityProviderService.GetUser",
}
JSON = {"AccessToken": self.access_token}
async with async_timeout.timeout(TIMEOUT):
response = await self.session.post(
"https://cognito-idp.us-east-1.amazonaws.com/",
headers=headers,
json=JSON,
)
if response.status == 200:
data = await response.json(content_type="application/x-amz-json-1.1")
# InitiateAuth
headers = {
"Content-Type": "application/x-amz-json-1.1",
"X-Amz-Target": "AWSCognitoIdentityProviderService.InitiateAuth",
}
payload = {
"AuthFlow": "REFRESH_TOKEN_AUTH",
"AuthParameters": {
"DEVICE_KEY": None,
"REFRESH_TOKEN": self.refresh_token,
},
"ClientId": "4k78t7970hhdgtafurk158dr3a",
}
async with async_timeout.timeout(TIMEOUT):
response = await self.session.post(
"https://cognito-idp.us-east-1.amazonaws.com/",
headers=headers,
json=payload,
)
if response.status == 200:
data = await response.json(content_type="application/x-amz-json-1.1")
self.access_token = data["AuthenticationResult"]["AccessToken"]
self.id_token = tokens["AuthenticationResult"]["IdToken"]
return LOGIN_RESULT_OK
async def get_open_accounts(self):
"""
Returns the open accounts
"""
result = []
URL = API_HOST + "/cs/gulf/ssp/v1/profile/accounts/list"
headers = {"Authorization": f"Bearer {self.id_token}"}
async with async_timeout.timeout(TIMEOUT):
response = await self.session.get(URL, headers=headers)
if response.status == 200:
data = await response.json()
for account in data["accounts"]:
if account["accountStatus"] == ACCOUNT_STATUS_ACTIVE:
result.append(account["accountNumber"])
return result
async def logout(self):
"""log out from fpl"""
async def update(self, account):
"""
Returns the data collected from fpl
"""
URL = (
API_HOST
+ f"/cs/gulf/ssp/v1/accountservices/account/{account}/accountSummary?balance=y"
)
headers = {"Authorization": f"Bearer {self.id_token}"}
async with async_timeout.timeout(TIMEOUT):
response = await self.session.get(URL, headers=headers)
result = {}
if response.status == 200:
data = await response.json()
accountSumary = data["accountSummary"]["accountSummaryData"]
billAndMetterInfo = accountSumary["billAndMeterInfo"]
programInfo = accountSumary["programInfo"]
result["budget_bill"] = False
result["bill_to_date"] = billAndMetterInfo["asOfDateAmount"]
result["projected_bill"] = billAndMetterInfo["projBillAmount"]
result["projectedKWH"] = billAndMetterInfo["projBillKWH"]
result["bill_to_date"] = billAndMetterInfo["asOfDateUsage"]
result["billToDateKWH"] = billAndMetterInfo["asOfDateUsage"]
result["daily_avg"] = billAndMetterInfo["dailyAvgAmount"]
result["dailyAverageKWH"] = billAndMetterInfo["dailyAvgKwh"]
result["billStartDate"] = programInfo["currentBillDate"]
result["next_bill_date"] = programInfo["nextBillDate"]
start = datetime.fromisoformat(result["billStartDate"])
end = datetime.fromisoformat(result["next_bill_date"])
today = datetime.fromisoformat(data["today"])
result["service_days"] = (end - start).days
result["as_of_days"] = (today - start).days
return result

View File

@@ -9,6 +9,7 @@ from homeassistant.core import Config, HomeAssistant
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.util import Throttle from homeassistant.util import Throttle
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from .fplapi import FplApi from .fplapi import FplApi
from .const import ( from .const import (
@@ -17,7 +18,7 @@ from .const import (
PLATFORMS, PLATFORMS,
STARTUP_MESSAGE, STARTUP_MESSAGE,
) )
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD
from .fplDataUpdateCoordinator import FplDataUpdateCoordinator from .fplDataUpdateCoordinator import FplDataUpdateCoordinator
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
@@ -62,7 +63,7 @@ async def async_setup_entry(hass, entry):
# Configure the client. # Configure the client.
_LOGGER.info("Configuring the client") _LOGGER.info("Configuring the client")
session = async_get_clientsession(hass) session = async_get_clientsession(hass)
client = FplApi(username, password, session) client = FplApi(username, password, session, hass.loop)
coordinator = FplDataUpdateCoordinator(hass, client=client) coordinator = FplDataUpdateCoordinator(hass, client=client)
await coordinator.async_refresh() await coordinator.async_refresh()

View File

@@ -0,0 +1,323 @@
"""AWS SRP"""
import os
import base64
import binascii
import datetime
import hashlib
import hmac
import re
import functools
import boto3
import six
from .exceptions import ForceChangePasswordException
# https://github.com/aws/amazon-cognito-identity-js/blob/master/src/AuthenticationHelper.js#L22
n_hex = (
"FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD1"
+ "29024E088A67CC74020BBEA63B139B22514A08798E3404DD"
+ "EF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245"
+ "E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7ED"
+ "EE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3D"
+ "C2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F"
+ "83655D23DCA3AD961C62F356208552BB9ED529077096966D"
+ "670C354E4ABC9804F1746C08CA18217C32905E462E36CE3B"
+ "E39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9"
+ "DE2BCBF6955817183995497CEA956AE515D2261898FA0510"
+ "15728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64"
+ "ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7"
+ "ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6B"
+ "F12FFA06D98A0864D87602733EC86A64521F2B18177B200C"
+ "BBE117577A615D6C770988C0BAD946E208E24FA074E5AB31"
+ "43DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF"
)
# https://github.com/aws/amazon-cognito-identity-js/blob/master/src/AuthenticationHelper.js#L49
g_hex = "2"
info_bits = bytearray("Caldera Derived Key", "utf-8")
def hash_sha256(buf):
"""AuthenticationHelper.hash"""
a = hashlib.sha256(buf).hexdigest()
return (64 - len(a)) * "0" + a
def hex_hash(hex_string):
return hash_sha256(bytearray.fromhex(hex_string))
def hex_to_long(hex_string):
return int(hex_string, 16)
def long_to_hex(long_num):
return "%x" % long_num
def get_random(nbytes):
random_hex = binascii.hexlify(os.urandom(nbytes))
return hex_to_long(random_hex)
def pad_hex(long_int):
"""
Converts a Long integer (or hex string) to hex format padded with zeroes for hashing
:param {Long integer|String} long_int Number or string to pad.
:return {String} Padded hex string.
"""
if not isinstance(long_int, six.string_types):
hash_str = long_to_hex(long_int)
else:
hash_str = long_int
if len(hash_str) % 2 == 1:
hash_str = "0%s" % hash_str
elif hash_str[0] in "89ABCDEFabcdef":
hash_str = "00%s" % hash_str
return hash_str
def compute_hkdf(ikm, salt):
"""
Standard hkdf algorithm
:param {Buffer} ikm Input key material.
:param {Buffer} salt Salt value.
:return {Buffer} Strong key material.
@private
"""
prk = hmac.new(salt, ikm, hashlib.sha256).digest()
info_bits_update = info_bits + bytearray(chr(1), "utf-8")
hmac_hash = hmac.new(prk, info_bits_update, hashlib.sha256).digest()
return hmac_hash[:16]
def calculate_u(big_a, big_b):
"""
Calculate the client's value U which is the hash of A and B
:param {Long integer} big_a Large A value.
:param {Long integer} big_b Server B value.
:return {Long integer} Computed U value.
"""
u_hex_hash = hex_hash(pad_hex(big_a) + pad_hex(big_b))
return hex_to_long(u_hex_hash)
class AWSSRP(object):
NEW_PASSWORD_REQUIRED_CHALLENGE = "NEW_PASSWORD_REQUIRED"
PASSWORD_VERIFIER_CHALLENGE = "PASSWORD_VERIFIER"
def __init__(
self,
username,
password,
pool_id,
client_id,
pool_region=None,
client=None,
client_secret=None,
loop=None,
):
if pool_region is not None and client is not None:
raise ValueError(
"pool_region and client should not both be specified "
"(region should be passed to the boto3 client instead)"
)
self.username = username
self.password = password
self.pool_id = pool_id
self.client_id = client_id
self.client_secret = client_secret
self.client = (
client if client else boto3.client("cognito-idp", region_name=pool_region)
)
self.big_n = hex_to_long(n_hex)
self.g = hex_to_long(g_hex)
self.k = hex_to_long(hex_hash("00" + n_hex + "0" + g_hex))
self.small_a_value = self.generate_random_small_a()
self.large_a_value = self.calculate_a()
self.loop = loop
def generate_random_small_a(self):
"""
helper function to generate a random big integer
:return {Long integer} a random value.
"""
random_long_int = get_random(128)
return random_long_int % self.big_n
def calculate_a(self):
"""
Calculate the client's public value A = g^a%N
with the generated random number a
:param {Long integer} a Randomly generated small A.
:return {Long integer} Computed large A.
"""
big_a = pow(self.g, self.small_a_value, self.big_n)
# safety check
if (big_a % self.big_n) == 0:
raise ValueError("Safety check for A failed")
return big_a
def get_password_authentication_key(self, username, password, server_b_value, salt):
"""
Calculates the final hkdf based on computed S value, and computed U value and the key
:param {String} username Username.
:param {String} password Password.
:param {Long integer} server_b_value Server B value.
:param {Long integer} salt Generated salt.
:return {Buffer} Computed HKDF value.
"""
u_value = calculate_u(self.large_a_value, server_b_value)
if u_value == 0:
raise ValueError("U cannot be zero.")
username_password = "%s%s:%s" % (self.pool_id.split("_")[1], username, password)
username_password_hash = hash_sha256(username_password.encode("utf-8"))
x_value = hex_to_long(hex_hash(pad_hex(salt) + username_password_hash))
g_mod_pow_xn = pow(self.g, x_value, self.big_n)
int_value2 = server_b_value - self.k * g_mod_pow_xn
s_value = pow(int_value2, self.small_a_value + u_value * x_value, self.big_n)
hkdf = compute_hkdf(
bytearray.fromhex(pad_hex(s_value)),
bytearray.fromhex(pad_hex(long_to_hex(u_value))),
)
return hkdf
def get_auth_params(self):
auth_params = {
"USERNAME": self.username,
"SRP_A": long_to_hex(self.large_a_value),
}
if self.client_secret is not None:
auth_params.update(
{
"SECRET_HASH": self.get_secret_hash(
self.username, self.client_id, self.client_secret
)
}
)
return auth_params
@staticmethod
def get_secret_hash(username, client_id, client_secret):
message = bytearray(username + client_id, "utf-8")
hmac_obj = hmac.new(bytearray(client_secret, "utf-8"), message, hashlib.sha256)
return base64.standard_b64encode(hmac_obj.digest()).decode("utf-8")
def process_challenge(self, challenge_parameters):
user_id_for_srp = challenge_parameters["USER_ID_FOR_SRP"]
salt_hex = challenge_parameters["SALT"]
srp_b_hex = challenge_parameters["SRP_B"]
secret_block_b64 = challenge_parameters["SECRET_BLOCK"]
# re strips leading zero from a day number (required by AWS Cognito)
timestamp = re.sub(
r" 0(\d) ",
r" \1 ",
datetime.datetime.utcnow().strftime("%a %b %d %H:%M:%S UTC %Y"),
)
hkdf = self.get_password_authentication_key(
user_id_for_srp, self.password, hex_to_long(srp_b_hex), salt_hex
)
secret_block_bytes = base64.standard_b64decode(secret_block_b64)
msg = (
bytearray(self.pool_id.split("_")[1], "utf-8")
+ bytearray(user_id_for_srp, "utf-8")
+ bytearray(secret_block_bytes)
+ bytearray(timestamp, "utf-8")
)
hmac_obj = hmac.new(hkdf, msg, digestmod=hashlib.sha256)
signature_string = base64.standard_b64encode(hmac_obj.digest())
response = {
"TIMESTAMP": timestamp,
"USERNAME": user_id_for_srp,
"PASSWORD_CLAIM_SECRET_BLOCK": secret_block_b64,
"PASSWORD_CLAIM_SIGNATURE": signature_string.decode("utf-8"),
}
if self.client_secret is not None:
response.update(
{
"SECRET_HASH": self.get_secret_hash(
self.username, self.client_id, self.client_secret
)
}
)
return response
async def authenticate_user(self, client=None):
"""authenticate user"""
boto_client = self.client or client
auth_params = self.get_auth_params()
response = await self.loop.run_in_executor(
None,
functools.partial(
boto_client.initiate_auth,
AuthFlow="USER_SRP_AUTH",
AuthParameters=auth_params,
ClientId=self.client_id,
),
)
if response["ChallengeName"] == self.PASSWORD_VERIFIER_CHALLENGE:
challenge_response = self.process_challenge(response["ChallengeParameters"])
tokens = await self.loop.run_in_executor(
None,
functools.partial(
boto_client.respond_to_auth_challenge,
ClientId=self.client_id,
ChallengeName=self.PASSWORD_VERIFIER_CHALLENGE,
ChallengeResponses=challenge_response,
),
)
# tokens = boto_client.respond_to_auth_challenge(
# ClientId=self.client_id,
# ChallengeName=self.PASSWORD_VERIFIER_CHALLENGE,
# ChallengeResponses=challenge_response,
# )
if tokens.get("ChallengeName") == self.NEW_PASSWORD_REQUIRED_CHALLENGE:
raise ForceChangePasswordException(
"Change password before authenticating"
)
return tokens
else:
raise NotImplementedError(
"The %s challenge is not supported" % response["ChallengeName"]
)
def set_new_password_challenge(self, new_password, client=None):
boto_client = self.client or client
auth_params = self.get_auth_params()
response = boto_client.initiate_auth(
AuthFlow="USER_SRP_AUTH",
AuthParameters=auth_params,
ClientId=self.client_id,
)
if response["ChallengeName"] == self.PASSWORD_VERIFIER_CHALLENGE:
challenge_response = self.process_challenge(response["ChallengeParameters"])
tokens = boto_client.respond_to_auth_challenge(
ClientId=self.client_id,
ChallengeName=self.PASSWORD_VERIFIER_CHALLENGE,
ChallengeResponses=challenge_response,
)
if tokens["ChallengeName"] == self.NEW_PASSWORD_REQUIRED_CHALLENGE:
challenge_response = {
"USERNAME": auth_params["USERNAME"],
"NEW_PASSWORD": new_password,
}
new_password_response = boto_client.respond_to_auth_challenge(
ClientId=self.client_id,
ChallengeName=self.NEW_PASSWORD_REQUIRED_CHALLENGE,
Session=tokens["Session"],
ChallengeResponses=challenge_response,
)
return new_password_response
return tokens
else:
raise NotImplementedError(
"The %s challenge is not supported" % response["ChallengeName"]
)

View File

@@ -8,16 +8,19 @@ from homeassistant.helpers.aiohttp_client import async_create_clientsession
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_NAME
from .const import DEFAULT_CONF_PASSWORD, DEFAULT_CONF_USERNAME, DOMAIN
from .fplapi import ( from .const import (
DEFAULT_CONF_PASSWORD,
DEFAULT_CONF_USERNAME,
DOMAIN,
LOGIN_RESULT_OK, LOGIN_RESULT_OK,
LOGIN_RESULT_FAILURE, LOGIN_RESULT_FAILURE,
LOGIN_RESULT_INVALIDUSER, LOGIN_RESULT_INVALIDUSER,
LOGIN_RESULT_INVALIDPASSWORD, LOGIN_RESULT_INVALIDPASSWORD,
FplApi,
) )
from .fplapi import FplApi
try: try:
from .secrets import DEFAULT_CONF_PASSWORD, DEFAULT_CONF_USERNAME from .secrets import DEFAULT_CONF_PASSWORD, DEFAULT_CONF_USERNAME
except: except:
@@ -61,12 +64,15 @@ class FplFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
if username not in configured_instances(self.hass): if username not in configured_instances(self.hass):
session = async_create_clientsession(self.hass) session = async_create_clientsession(self.hass)
api = FplApi(username, password, session) api = FplApi(username, password, session, loop=self.hass.loop)
result = await api.login() result = await api.login()
if result == LOGIN_RESULT_OK: if result == LOGIN_RESULT_OK:
info = await api.get_basic_info()
accounts = await api.async_get_open_accounts() accounts = info["accounts"]
# accounts = await api.async_get_open_accounts()
await api.logout() await api.logout()
user_input["accounts"] = accounts user_input["accounts"] = accounts

View File

@@ -1,7 +1,9 @@
"""Constants for fpl.""" """Constants for fpl."""
# #
DEBUG = True DEBUG = False
TIMEOUT = 5
API_HOST = "https://www.fpl.com"
# Base component constants # Base component constants
NAME = "FPL Integration" NAME = "FPL Integration"
@@ -45,3 +47,11 @@ If you have any issues with this you need to open an issue here:
DEFAULT_CONF_USERNAME = "" DEFAULT_CONF_USERNAME = ""
DEFAULT_CONF_PASSWORD = "" DEFAULT_CONF_PASSWORD = ""
# Api login result
LOGIN_RESULT_OK = "OK"
LOGIN_RESULT_INVALIDUSER = "NOTVALIDUSER"
LOGIN_RESULT_INVALIDPASSWORD = "FAILEDPASSWORD"
LOGIN_RESULT_UNAUTHORIZED = "UNAUTHORIZED"
LOGIN_RESULT_FAILURE = "FAILURE"

View File

@@ -0,0 +1,13 @@
"""exceptions file"""
class WarrantException(Exception):
"""Base class for all Warrant exceptions"""
class ForceChangePasswordException(WarrantException):
"""Raised when the user is forced to change their password"""
class TokenVerificationException(WarrantException):
"""Raised when token verification fails."""

View File

@@ -71,12 +71,11 @@ class FplEnergyEntity(FplEntity):
"""Represents a energy sensor""" """Represents a energy sensor"""
_attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR _attr_native_unit_of_measurement = ENERGY_KILO_WATT_HOUR
_attr_device_class = DEVICE_CLASS_ENERGY # _attr_device_class = DEVICE_CLASS_ENERGY
_attr_icon = "mdi:flash" _attr_icon = "mdi:flash"
_attr_state_class = STATE_CLASS_MEASUREMENT
@property @property
def last_reset(self) -> datetime: def last_reset_not_use(self) -> datetime:
"""Return the time when the sensor was last reset, if any.""" """Return the time when the sensor was last reset, if any."""
today = datetime.today() today = datetime.today()

View File

@@ -1,478 +1,163 @@
"""Custom FPl api client""" """Custom FPl api client"""
import logging
from datetime import datetime, timedelta
import sys import sys
import json import json
import aiohttp import logging
from datetime import datetime, timedelta
import async_timeout import async_timeout
STATUS_CATEGORY_OPEN = "OPEN"
# Api login result from .const import (
LOGIN_RESULT_OK = "OK" LOGIN_RESULT_FAILURE,
LOGIN_RESULT_INVALIDUSER = "NOTVALIDUSER" LOGIN_RESULT_OK,
LOGIN_RESULT_INVALIDPASSWORD = "FAILEDPASSWORD" TIMEOUT,
LOGIN_RESULT_UNAUTHORIZED = "UNAUTHORIZED" API_HOST,
LOGIN_RESULT_FAILURE = "FAILURE" )
from .FplMainRegionApiClient import FplMainRegionApiClient
from .FplNorthwestRegionApiClient import FplNorthwestRegionApiClient
_LOGGER = logging.getLogger(__package__) _LOGGER = logging.getLogger(__package__)
TIMEOUT = 5
API_HOST = "https://www.fpl.com"
URL_LOGIN = API_HOST + "/api/resources/login"
URL_LOGOUT = API_HOST + "/api/resources/logout"
URL_RESOURCES_HEADER = API_HOST + "/api/resources/header"
URL_RESOURCES_ACCOUNT = API_HOST + "/api/resources/account/{account}"
URL_BUDGET_BILLING_GRAPH = (
API_HOST + "/api/resources/account/{account}/budgetBillingGraph"
)
URL_RESOURCES_PROJECTED_BILL = (
API_HOST
+ "/api/resources/account/{account}/projectedBill"
+ "?premiseNumber={premise}&lastBilledDate={lastBillDate}"
)
URL_ENERGY_SERVICE = (
API_HOST + "/dashboard-api/resources/account/{account}/energyService/{account}"
)
URL_APPLIANCE_USAGE = (
API_HOST + "/dashboard-api/resources/account/{account}/applianceUsage/{account}"
)
URL_BUDGET_BILLING_PREMISE_DETAILS = (
API_HOST + "/api/resources/account/{account}/budgetBillingGraph/premiseDetails"
)
ENROLLED = "ENROLLED" URL_TERRITORY = API_HOST + "/cs/customer/v1/territoryid/public/territory"
NOTENROLLED = "NOTENROLLED"
FPL_MAINREGION = "FL01"
FPL_NORTHWEST = "FL02"
class NoTerrytoryAvailableException(Exception):
"""Thrown when not possible to determine user territory"""
class FplApi: class FplApi:
"""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, session): def __init__(self, username, password, session, loop):
"""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._session = session self._session = session
self._loop = loop
self._territory = None
self.access_token = None
self.id_token = None
self.apiClient = None
async def getTerritory(self):
"""get territory"""
if self._territory is not None:
return self._territory
headers = {"userID": f"{self._username}", "channel": "WEB"}
async with async_timeout.timeout(TIMEOUT):
response = await self._session.get(URL_TERRITORY, headers=headers)
if response.status == 200:
json_data = json.loads(await response.text())
territoryArray = json_data["data"]["territory"]
if len(territoryArray) == 0:
raise NoTerrytoryAvailableException()
self._territory = territoryArray[0]
return territoryArray[0]
def isMainRegion(self):
"""Returns true if this account belongs to the main region, not northwest"""
return self._territory == FPL_MAINREGION
async def initialize(self):
"""initialize the api client"""
self._territory = await self.getTerritory()
# set the api client based on user's territory
if self.apiClient is None:
if self.isMainRegion():
self.apiClient = FplMainRegionApiClient(
self._username, self._password, self._loop, self._session
)
else:
self.apiClient = FplNorthwestRegionApiClient(
self._username, self._password, self._loop, self._session
)
async def get_basic_info(self):
"""returns basic info for sensor initialization"""
await self.initialize()
data = {}
data["territory"] = self._territory
data["accounts"] = await self.apiClient.get_open_accounts()
return data
async def async_get_data(self) -> dict: async def async_get_data(self) -> dict:
"""Get data from fpl api""" """Get data from fpl api"""
data = {} await self.initialize()
data = {
"as_of_days": 5,
"avg_high_temp": 89,
"billStartDate": "07-27-2022",
"billToDateKWH": "196",
"bill_to_date": 160.1,
"budget_bill": True,
"budget_billing_bill_to_date": 18.61,
"budget_billing_daily_avg": 3.72,
"budget_billing_projected_bill": 111.69,
"current_bill_date": "2022-07-27",
"dailyAverageKWH": 39,
"daily_avg": 5.25,
"daily_usage": [],
"defered_amount": -6.84,
"delMtrReading": "15929",
"energy_percent_by_applicance": {},
"meterSerialNo": "20948426",
"next_bill_date": "2022-08-26",
"projectedKWH": "1176",
"projected_bill": 163.77,
"recMtrReading": "",
"remaining_days": 25,
"service_days": 30,
}
data["accounts"] = [] data["accounts"] = []
if await self.login() == LOGIN_RESULT_OK:
accounts = await self.async_get_open_accounts() data["territory"] = self._territory
print(self._territory)
login_result = await self.apiClient.login()
if login_result == LOGIN_RESULT_OK:
accounts = await self.apiClient.get_open_accounts()
data["accounts"] = accounts data["accounts"] = accounts
for account in accounts: for account in accounts:
account_data = await self.__async_get_data(account) data[account] = await self.apiClient.update(account)
data[account] = account_data
await self.logout() await self.apiClient.logout()
return data return data
async def login(self): async def login(self):
"""login into fpl""" """method to use in config flow"""
try:
await self.initialize()
_LOGGER.info("Logging in") _LOGGER.info("Logging in")
# login and get account information # login and get account information
try:
async with async_timeout.timeout(TIMEOUT):
response = await self._session.get(
URL_LOGIN, auth=aiohttp.BasicAuth(self._username, self._password)
)
if response.status == 200: return await self.apiClient.login()
_LOGGER.info("Logging Successful")
return LOGIN_RESULT_OK
if response.status == 401:
_LOGGER.error("Logging Unauthorized")
json_data = json.loads(await response.text())
if json_data["messageCode"] == LOGIN_RESULT_INVALIDUSER:
return LOGIN_RESULT_INVALIDUSER
if json_data["messageCode"] == LOGIN_RESULT_INVALIDPASSWORD:
return LOGIN_RESULT_INVALIDPASSWORD
except Exception as exception: except Exception as exception:
_LOGGER.error("Error %s : %s", exception, sys.exc_info()[0]) _LOGGER.error("Error %s : %s", exception, sys.exc_info()[0])
return LOGIN_RESULT_FAILURE return LOGIN_RESULT_FAILURE
return LOGIN_RESULT_FAILURE async def async_get_open_accounts(self):
"""return open accounts"""
self.initialize()
return await self.apiClient.get_open_accounts()
async def logout(self): async def logout(self):
"""Logging out from fpl""" """log out from fpl"""
_LOGGER.info("Logging out") return await self.apiClient.logout()
try:
async with async_timeout.timeout(TIMEOUT):
await self._session.get(URL_LOGOUT)
except Exception:
pass
async def async_get_open_accounts(self):
"""Getting open accounts"""
_LOGGER.info("Getting open accounts")
result = []
try:
async with async_timeout.timeout(TIMEOUT):
response = await self._session.get(URL_RESOURCES_HEADER)
json_data = await response.json()
accounts = json_data["data"]["accounts"]["data"]["data"]
for account in accounts:
if account["statusCategory"] == STATUS_CATEGORY_OPEN:
result.append(account["accountNumber"])
except Exception:
_LOGGER.error("Getting accounts %s", sys.exc_info())
return result
async def __async_get_data(self, account) -> dict:
"""Get data from resources endpoint"""
_LOGGER.info("Getting Data")
data = {}
async with async_timeout.timeout(TIMEOUT):
response = await self._session.get(
URL_RESOURCES_ACCOUNT.format(account=account)
)
account_data = (await response.json())["data"]
premise = account_data.get("premiseNumber").zfill(9)
data["meterSerialNo"] = account_data["meterSerialNo"]
# currentBillDate
currentBillDate = datetime.strptime(
account_data["currentBillDate"].replace("-", "").split("T")[0], "%Y%m%d"
).date()
# nextBillDate
nextBillDate = datetime.strptime(
account_data["nextBillDate"].replace("-", "").split("T")[0], "%Y%m%d"
).date()
data["current_bill_date"] = str(currentBillDate)
data["next_bill_date"] = str(nextBillDate)
today = datetime.now().date()
data["service_days"] = (nextBillDate - currentBillDate).days
data["as_of_days"] = (today - currentBillDate).days
data["remaining_days"] = (nextBillDate - today).days
# zip code
# zip_code = accountData["serviceAddress"]["zip"]
# projected bill
pbData = await self.__getFromProjectedBill(account, premise, currentBillDate)
data.update(pbData)
# programs
programsData = account_data["programs"]["data"]
programs = dict()
_LOGGER.info("Getting Programs")
for program in programsData:
if "enrollmentStatus" in program.keys():
key = program["name"]
programs[key] = program["enrollmentStatus"] == ENROLLED
def hasProgram(programName) -> bool:
return programName in programs and programs[programName]
# Budget Billing program
if hasProgram("BBL"):
data["budget_bill"] = True
bbl_data = await self.__getBBL_async(account, data)
data.update(bbl_data)
else:
data["budget_bill"] = False
# Get data from energy service
data.update(
await self.__getDataFromEnergyService(account, premise, currentBillDate)
)
# Get data from energy service ( hourly )
# data.update(
# await self.__getDataFromEnergyServiceHourly(
# account, premise, currentBillDate
# )
# )
data.update(await self.__getDataFromApplianceUsage(account, currentBillDate))
return data
async def __getFromProjectedBill(self, account, premise, currentBillDate) -> dict:
"""get data from projected bill endpoint"""
data = {}
try:
async with async_timeout.timeout(TIMEOUT):
response = await self._session.get(
URL_RESOURCES_PROJECTED_BILL.format(
account=account,
premise=premise,
lastBillDate=currentBillDate.strftime("%m%d%Y"),
)
)
if response.status == 200:
projectedBillData = (await response.json())["data"]
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
except Exception:
pass
return data
async def __getBBL_async(self, account, projectedBillData) -> dict:
"""Get budget billing data"""
_LOGGER.info("Getting budget billing data")
data = {}
try:
async with async_timeout.timeout(TIMEOUT):
response = await self._session.get(
URL_BUDGET_BILLING_PREMISE_DETAILS.format(account=account)
)
if response.status == 200:
r = (await response.json())["data"]
dataList = r["graphData"]
# startIndex = len(dataList) - 1
billingCharge = 0
budgetBillDeferBalance = r["defAmt"]
projectedBill = projectedBillData["projected_bill"]
asOfDays = projectedBillData["as_of_days"]
for det in dataList:
billingCharge += det["actuallBillAmt"]
calc1 = (projectedBill + billingCharge) / 12
calc2 = (1 / 12) * (budgetBillDeferBalance)
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_projected_bill"] = float(projectedBudgetBill)
except Exception as e:
_LOGGER.error("Error getting BBL: %s", e)
try:
async with async_timeout.timeout(TIMEOUT):
response = await self._session.get(
URL_BUDGET_BILLING_GRAPH.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 Exception as e:
_LOGGER.error("Error getting BBL: %s", e)
return data
async def __getDataFromEnergyService(
self, account, premise, lastBilledDate
) -> dict:
_LOGGER.info("Getting data from energy service")
date = str(lastBilledDate.strftime("%m%d%Y"))
JSON = {
"recordCount": 24,
"status": 2,
"channel": "WEB",
"amrFlag": "Y",
"accountType": "RESIDENTIAL",
"revCode": "1",
"premiseNumber": premise,
"projectedBillFlag": True,
"billComparisionFlag": True,
"monthlyFlag": True,
"frequencyType": "Daily",
"lastBilledDate": date,
"applicationPage": "resDashBoard",
}
data = {}
async with async_timeout.timeout(TIMEOUT):
response = await self._session.post(
URL_ENERGY_SERVICE.format(account=account), json=JSON
)
if response.status == 200:
r = (await response.json())["data"]
dailyUsage = []
# totalPowerUsage = 0
if "data" in r["DailyUsage"]:
for daily in r["DailyUsage"]["data"]:
if (
"kwhUsed" in daily.keys()
and "billingCharge" in daily.keys()
and "date" in daily.keys()
and "averageHighTemperature" in daily.keys()
):
dailyUsage.append(
{
"usage": daily["kwhUsed"],
"cost": daily["billingCharge"],
# "date": daily["date"],
"max_temperature": daily["averageHighTemperature"],
"netDeliveredKwh": daily["netDeliveredKwh"]
if "netDeliveredKwh" in daily.keys()
else 0,
"netReceivedKwh": daily["netReceivedKwh"]
if "netReceivedKwh" in daily.keys()
else 0,
"readTime": datetime.fromisoformat(
daily[
"readTime"
] # 2022-02-25T00:00:00.000-05:00
),
}
)
# totalPowerUsage += int(daily["kwhUsed"])
# data["total_power_usage"] = totalPowerUsage
data["daily_usage"] = dailyUsage
data["projectedKWH"] = r["CurrentUsage"]["projectedKWH"]
data["dailyAverageKWH"] = r["CurrentUsage"]["dailyAverageKWH"]
data["billToDateKWH"] = r["CurrentUsage"]["billToDateKWH"]
data["recMtrReading"] = r["CurrentUsage"]["recMtrReading"]
data["delMtrReading"] = r["CurrentUsage"]["delMtrReading"]
data["billStartDate"] = r["CurrentUsage"]["billStartDate"]
return data
async def __getDataFromEnergyServiceHourly(
self, account, premise, lastBilledDate
) -> dict:
_LOGGER.info("Getting data from energy service Hourly")
# date = str(lastBilledDate.strftime("%m%d%Y"))
date = str((datetime.now() - timedelta(days=1)).strftime("%m%d%Y"))
JSON = {
"status": 2,
"channel": "WEB",
"amrFlag": "Y",
"accountType": "RESIDENTIAL",
"revCode": "1",
"premiseNumber": premise,
"projectedBillFlag": False,
"billComparisionFlag": False,
"monthlyFlag": False,
"frequencyType": "Hourly",
"applicationPage": "resDashBoard",
"startDate": date,
}
data = {}
# now = homeassistant.util.dt.utcnow()
# now = datetime.now().astimezone()
# hour = now.hour
async with async_timeout.timeout(TIMEOUT):
response = await self._session.post(
URL_ENERGY_SERVICE.format(account=account), json=JSON
)
if response.status == 200:
r = (await response.json())["data"]
dailyUsage = []
# totalPowerUsage = 0
if "data" in r["HourlyUsage"]:
for daily in r["HourlyUsage"]["data"]:
if (
"kwhUsed" in daily.keys()
and "billingCharge" in daily.keys()
and "date" in daily.keys()
and "averageHighTemperature" in daily.keys()
):
dailyUsage.append(
{
"usage": daily["kwhUsed"],
"cost": daily["billingCharge"],
# "date": daily["date"],
"max_temperature": daily["averageHighTemperature"],
"netDeliveredKwh": daily["netDeliveredKwh"]
if "netDeliveredKwh" in daily.keys()
else 0,
"netReceivedKwh": daily["netReceivedKwh"]
if "netReceivedKwh" in daily.keys()
else 0,
"readTime": datetime.fromisoformat(
daily[
"readTime"
] # 2022-02-25T00:00:00.000-05:00
),
}
)
# totalPowerUsage += int(daily["kwhUsed"])
# data["total_power_usage"] = totalPowerUsage
data["daily_usage"] = dailyUsage
data["projectedKWH"] = r["HourlyUsage"]["projectedKWH"]
data["dailyAverageKWH"] = r["HourlyUsage"]["dailyAverageKWH"]
data["billToDateKWH"] = r["HourlyUsage"]["billToDateKWH"]
data["recMtrReading"] = r["HourlyUsage"]["recMtrReading"]
data["delMtrReading"] = r["HourlyUsage"]["delMtrReading"]
data["billStartDate"] = r["HourlyUsage"]["billStartDate"]
return data
async def __getDataFromApplianceUsage(self, account, lastBilledDate) -> dict:
"""get data from appliance usage"""
_LOGGER.info("Getting data from appliance usage")
JSON = {"startDate": str(lastBilledDate.strftime("%m%d%Y"))}
data = {}
try:
async with async_timeout.timeout(TIMEOUT):
response = await self._session.post(
URL_APPLIANCE_USAGE.format(account=account), json=JSON
)
if response.status == 200:
electric = (await response.json())["data"]["electric"]
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
except Exception:
pass
return {"energy_percent_by_applicance": data}

View File

@@ -43,6 +43,9 @@ async def async_setup_entry(hass, entry, async_add_devices):
"""Setup sensor platform.""" """Setup sensor platform."""
accounts = entry.data.get("accounts") accounts = entry.data.get("accounts")
territory = entry.data.get("territory")
print(f"setting sensor for {territory}")
coordinator = hass.data[DOMAIN][entry.entry_id] coordinator = hass.data[DOMAIN][entry.entry_id]
fpl_accounts = [] fpl_accounts = []
@@ -50,6 +53,8 @@ async def async_setup_entry(hass, entry, async_add_devices):
if DEBUG: if DEBUG:
for account in accounts: for account in accounts:
fpl_accounts.append(TestSensor(coordinator, entry, account)) fpl_accounts.append(TestSensor(coordinator, entry, account))
fpl_accounts.append(FplProjectedBillSensor(coordinator, entry, account))
else: else:
for account in accounts: for account in accounts:
# Test Sensor # Test Sensor

View File

@@ -10,7 +10,7 @@ class DailyAverageSensor(FplMoneyEntity):
super().__init__(coordinator, config, account, "Daily Average") super().__init__(coordinator, config, account, "Daily Average")
@property @property
def state(self): def native_value(self):
budget = self.getData("budget_bill") budget = self.getData("budget_bill")
budget_billing_projected_bill = self.getData("budget_billing_daily_avg") budget_billing_projected_bill = self.getData("budget_billing_daily_avg")
@@ -22,7 +22,7 @@ class DailyAverageSensor(FplMoneyEntity):
def customAttributes(self): def customAttributes(self):
"""Return the state attributes.""" """Return the state attributes."""
attributes = {} attributes = {}
attributes["state_class"] = STATE_CLASS_TOTAL # attributes["state_class"] = STATE_CLASS_TOTAL
return attributes return attributes
@@ -33,13 +33,13 @@ class BudgetDailyAverageSensor(FplMoneyEntity):
super().__init__(coordinator, config, account, "Budget Daily Average") super().__init__(coordinator, config, account, "Budget Daily Average")
@property @property
def state(self): def native_value(self):
return self.getData("budget_billing_daily_avg") return self.getData("budget_billing_daily_avg")
def customAttributes(self): def customAttributes(self):
"""Return the state attributes.""" """Return the state attributes."""
attributes = {} attributes = {}
attributes["state_class"] = STATE_CLASS_TOTAL # attributes["state_class"] = STATE_CLASS_TOTAL
return attributes return attributes
@@ -50,11 +50,11 @@ class ActualDailyAverageSensor(FplMoneyEntity):
super().__init__(coordinator, config, account, "Actual Daily Average") super().__init__(coordinator, config, account, "Actual Daily Average")
@property @property
def state(self): def native_value(self):
return self.getData("daily_avg") return self.getData("daily_avg")
def customAttributes(self): def customAttributes(self):
"""Return the state attributes.""" """Return the state attributes."""
attributes = {} attributes = {}
attributes["state_class"] = STATE_CLASS_TOTAL # attributes["state_class"] = STATE_CLASS_TOTAL
return attributes return attributes

View File

@@ -1,6 +1,10 @@
"""Daily Usage Sensors""" """Daily Usage Sensors"""
from datetime import timedelta from datetime import timedelta, datetime
from homeassistant.components.sensor import STATE_CLASS_TOTAL_INCREASING from homeassistant.components.sensor import (
STATE_CLASS_TOTAL_INCREASING,
STATE_CLASS_TOTAL,
DEVICE_CLASS_ENERGY,
)
from .fplEntity import FplEnergyEntity, FplMoneyEntity from .fplEntity import FplEnergyEntity, FplMoneyEntity
@@ -11,7 +15,7 @@ class FplDailyUsageSensor(FplMoneyEntity):
super().__init__(coordinator, config, account, "Daily Usage") super().__init__(coordinator, config, account, "Daily Usage")
@property @property
def state(self): def native_value(self):
data = self.getData("daily_usage") data = self.getData("daily_usage")
if data is not None and len(data) > 0 and "cost" in data[-1].keys(): if data is not None and len(data) > 0 and "cost" in data[-1].keys():
@@ -36,8 +40,11 @@ class FplDailyUsageKWHSensor(FplEnergyEntity):
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Daily Usage KWH") super().__init__(coordinator, config, account, "Daily Usage KWH")
_attr_state_class = STATE_CLASS_TOTAL_INCREASING
_attr_device_class = DEVICE_CLASS_ENERGY
@property @property
def state(self): def native_value(self):
data = self.getData("daily_usage") data = self.getData("daily_usage")
if data is not None and len(data) > 0 and "usage" in data[-1].keys(): if data is not None and len(data) > 0 and "usage" in data[-1].keys():
@@ -45,6 +52,13 @@ class FplDailyUsageKWHSensor(FplEnergyEntity):
return None return None
@property
def last_reset(self) -> datetime | None:
data = self.getData("daily_usage")
date = data[-1]["readTime"]
last_reset = date - timedelta(days=1)
return last_reset
def customAttributes(self): def customAttributes(self):
"""Return the state attributes.""" """Return the state attributes."""
data = self.getData("daily_usage") data = self.getData("daily_usage")
@@ -64,8 +78,10 @@ class FplDailyReceivedKWHSensor(FplEnergyEntity):
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Daily Received KWH") super().__init__(coordinator, config, account, "Daily Received KWH")
# _attr_state_class = STATE_CLASS_TOTAL_INCREASING
@property @property
def state(self): def native_value(self):
data = self.getData("daily_usage") data = self.getData("daily_usage")
if data is not None and len(data) > 0 and "netReceivedKwh" in data[-1].keys(): if data is not None and len(data) > 0 and "netReceivedKwh" in data[-1].keys():
return data[-1]["netReceivedKwh"] return data[-1]["netReceivedKwh"]
@@ -78,20 +94,21 @@ class FplDailyReceivedKWHSensor(FplEnergyEntity):
last_reset = date - timedelta(days=1) last_reset = date - timedelta(days=1)
attributes = {} attributes = {}
attributes["state_class"] = STATE_CLASS_TOTAL_INCREASING
attributes["date"] = date attributes["date"] = date
attributes["last_reset"] = last_reset # attributes["last_reset"] = last_reset
return attributes return attributes
class FplDailyDeliveredKWHSensor(FplEnergyEntity): class FplDailyDeliveredKWHSensor(FplEnergyEntity):
"""daily delivered Kwh sensor""" """daily delivered Kwh sensor"""
# _attr_state_class = STATE_CLASS_TOTAL_INCREASING
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Daily Delivered KWH") super().__init__(coordinator, config, account, "Daily Delivered KWH")
@property @property
def state(self): def native_value(self):
data = self.getData("daily_usage") data = self.getData("daily_usage")
if data is not None and len(data) > 0 and "netDeliveredKwh" in data[-1].keys(): if data is not None and len(data) > 0 and "netDeliveredKwh" in data[-1].keys():
return data[-1]["netDeliveredKwh"] return data[-1]["netDeliveredKwh"]
@@ -104,7 +121,6 @@ class FplDailyDeliveredKWHSensor(FplEnergyEntity):
last_reset = date - timedelta(days=1) last_reset = date - timedelta(days=1)
attributes = {} attributes = {}
attributes["state_class"] = STATE_CLASS_TOTAL_INCREASING
attributes["date"] = date attributes["date"] = date
attributes["last_reset"] = last_reset # attributes["last_reset"] = last_reset
return attributes return attributes

View File

@@ -4,45 +4,55 @@ from .fplEntity import FplDateEntity, FplDayEntity
class CurrentBillDateSensor(FplDateEntity): class CurrentBillDateSensor(FplDateEntity):
"""Current bill date sensor"""
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Current Bill Date") super().__init__(coordinator, config, account, "Current Bill Date")
@property @property
def state(self): def native_value(self):
return datetime.date.fromisoformat(self.getData("current_bill_date")) return datetime.date.fromisoformat(self.getData("current_bill_date"))
class NextBillDateSensor(FplDateEntity): class NextBillDateSensor(FplDateEntity):
"""Next bill date sensor"""
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Next Bill Date") super().__init__(coordinator, config, account, "Next Bill Date")
@property @property
def state(self): def native_value(self):
return datetime.date.fromisoformat(self.getData("next_bill_date")) return datetime.date.fromisoformat(self.getData("next_bill_date"))
class ServiceDaysSensor(FplDayEntity): class ServiceDaysSensor(FplDayEntity):
"""Service days sensor"""
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Service Days") super().__init__(coordinator, config, account, "Service Days")
@property @property
def state(self): def native_value(self):
return self.getData("service_days") return self.getData("service_days")
class AsOfDaysSensor(FplDayEntity): class AsOfDaysSensor(FplDayEntity):
"""As of days sensor"""
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "As Of Days") super().__init__(coordinator, config, account, "As Of Days")
@property @property
def state(self): def native_value(self):
return self.getData("as_of_days") return self.getData("as_of_days")
class RemainingDaysSensor(FplDayEntity): class RemainingDaysSensor(FplDayEntity):
"""Remaining days sensor"""
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Remaining Days") super().__init__(coordinator, config, account, "Remaining Days")
@property @property
def state(self): def native_value(self):
return self.getData("remaining_days") return self.getData("remaining_days")

View File

@@ -8,41 +8,47 @@ from .fplEntity import FplEnergyEntity
class ProjectedKWHSensor(FplEnergyEntity): class ProjectedKWHSensor(FplEnergyEntity):
"""Projected KWH sensor"""
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Projected KWH") super().__init__(coordinator, config, account, "Projected KWH")
@property @property
def state(self): def native_value(self):
return self.getData("projectedKWH") return self.getData("projectedKWH")
def customAttributes(self): def customAttributes(self):
"""Return the state attributes.""" """Return the state attributes."""
attributes = {} attributes = {}
attributes["state_class"] = STATE_CLASS_TOTAL # attributes["state_class"] = STATE_CLASS_TOTAL
return attributes return attributes
class DailyAverageKWHSensor(FplEnergyEntity): class DailyAverageKWHSensor(FplEnergyEntity):
"""Daily Average KWH sensor"""
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Daily Average KWH") super().__init__(coordinator, config, account, "Daily Average KWH")
@property @property
def state(self): def native_value(self):
return self.getData("dailyAverageKWH") return self.getData("dailyAverageKWH")
def customAttributes(self): def customAttributes(self):
"""Return the state attributes.""" """Return the state attributes."""
attributes = {} attributes = {}
attributes["state_class"] = STATE_CLASS_TOTAL # attributes["state_class"] = STATE_CLASS_TOTAL
return attributes return attributes
class BillToDateKWHSensor(FplEnergyEntity): class BillToDateKWHSensor(FplEnergyEntity):
"""Bill To Date KWH sensor"""
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Bill To Date KWH") super().__init__(coordinator, config, account, "Bill To Date KWH")
@property @property
def state(self): def native_value(self):
return self.getData("billToDateKWH") return self.getData("billToDateKWH")
def customAttributes(self): def customAttributes(self):
@@ -54,36 +60,40 @@ class BillToDateKWHSensor(FplEnergyEntity):
last_reset = date.today() - timedelta(days=asOfDays) last_reset = date.today() - timedelta(days=asOfDays)
attributes = {} attributes = {}
attributes["state_class"] = STATE_CLASS_TOTAL_INCREASING # attributes["state_class"] = STATE_CLASS_TOTAL_INCREASING
attributes["last_reset"] = last_reset # attributes["last_reset"] = last_reset
return attributes return attributes
class NetReceivedKWHSensor(FplEnergyEntity): class NetReceivedKWHSensor(FplEnergyEntity):
"""Received Meter Reading KWH sensor"""
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Received Meter Reading KWH") super().__init__(coordinator, config, account, "Received Meter Reading KWH")
@property @property
def state(self): def native_value(self):
return self.getData("recMtrReading") return self.getData("recMtrReading")
def customAttributes(self): def customAttributes(self):
"""Return the state attributes.""" """Return the state attributes."""
attributes = {} attributes = {}
attributes["state_class"] = STATE_CLASS_TOTAL_INCREASING # attributes["state_class"] = STATE_CLASS_TOTAL_INCREASING
return attributes return attributes
class NetDeliveredKWHSensor(FplEnergyEntity): class NetDeliveredKWHSensor(FplEnergyEntity):
"""Delivered Meter Reading KWH sensor"""
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Delivered Meter Reading KWH") super().__init__(coordinator, config, account, "Delivered Meter Reading KWH")
@property @property
def state(self): def native_value(self):
return self.getData("delMtrReading") return self.getData("delMtrReading")
def customAttributes(self): def customAttributes(self):
"""Return the state attributes.""" """Return the state attributes."""
attributes = {} attributes = {}
attributes["state_class"] = STATE_CLASS_TOTAL_INCREASING # attributes["state_class"] = STATE_CLASS_TOTAL_INCREASING
return attributes return attributes

View File

@@ -9,7 +9,7 @@ from .fplEntity import FplMoneyEntity
class FplProjectedBillSensor(FplMoneyEntity): class FplProjectedBillSensor(FplMoneyEntity):
"""Projected bill sensor""" """Projected bill sensor"""
_attr_state_class = STATE_CLASS_TOTAL # _attr_state_class = STATE_CLASS_TOTAL
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Projected Bill") super().__init__(coordinator, config, account, "Projected Bill")
@@ -35,7 +35,7 @@ class FplProjectedBillSensor(FplMoneyEntity):
class DeferedAmountSensor(FplMoneyEntity): class DeferedAmountSensor(FplMoneyEntity):
"""Defered amount sensor""" """Defered amount sensor"""
_attr_state_class = STATE_CLASS_TOTAL # _attr_state_class = STATE_CLASS_TOTAL
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Defered Amount") super().__init__(coordinator, config, account, "Defered Amount")
@@ -50,7 +50,7 @@ class DeferedAmountSensor(FplMoneyEntity):
class ProjectedBudgetBillSensor(FplMoneyEntity): class ProjectedBudgetBillSensor(FplMoneyEntity):
"""projected budget bill sensor""" """projected budget bill sensor"""
_attr_state_class = STATE_CLASS_TOTAL # _attr_state_class = STATE_CLASS_TOTAL
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Projected Budget Bill") super().__init__(coordinator, config, account, "Projected Budget Bill")
@@ -63,7 +63,7 @@ class ProjectedBudgetBillSensor(FplMoneyEntity):
class ProjectedActualBillSensor(FplMoneyEntity): class ProjectedActualBillSensor(FplMoneyEntity):
"""projeted actual bill sensor""" """projeted actual bill sensor"""
_attr_state_class = STATE_CLASS_TOTAL # _attr_state_class = STATE_CLASS_TOTAL
def __init__(self, coordinator, config, account): def __init__(self, coordinator, config, account):
super().__init__(coordinator, config, account, "Projected Actual Bill") super().__init__(coordinator, config, account, "Projected Actual Bill")

View File

@@ -5,6 +5,7 @@ from homeassistant.components.sensor import (
DEVICE_CLASS_ENERGY, DEVICE_CLASS_ENERGY,
) )
from homeassistant.core import callback from homeassistant.core import callback
from homeassistant.const import STATE_UNKNOWN
from .fplEntity import FplEnergyEntity from .fplEntity import FplEnergyEntity
@@ -24,20 +25,26 @@ class TestSensor(FplEnergyEntity):
if data is not None and len(data) > 0 and "usage" in data[-1].keys(): if data is not None and len(data) > 0 and "usage" in data[-1].keys():
return data[-1]["usage"] return data[-1]["usage"]
return None return STATE_UNKNOWN
@property @property
def last_reset(self) -> datetime | None: def last_reset(self) -> datetime | None:
last_reset = None
data = self.getData("daily_usage") data = self.getData("daily_usage")
if len(data) > 0 and "readTime" in data[-1]:
date = data[-1]["readTime"] date = data[-1]["readTime"]
last_reset = date - timedelta(days=1) last_reset = datetime.combine(date, datetime.min.time())
print(f"setting last reset {last_reset}")
return last_reset return last_reset
def customAttributes(self): def customAttributes(self):
"""Return the state attributes.""" """Return the state attributes."""
print("setting custom attributes")
data = self.getData("daily_usage") data = self.getData("daily_usage")
date = data[-1]["readTime"] date = data[-1]["readTime"]
attributes = {} attributes = {}
attributes["date"] = date attributes["date"] = date
last_reset = date - timedelta(days=1)
# attributes["last_reset"] = last_reset
return attributes return attributes