Created new sensor with specific data and group them by devices(account)

This commit is contained in:
Yordan Suarez
2020-11-01 04:57:10 +00:00
parent 4e4cd3c633
commit 8c644d35f1
9 changed files with 279 additions and 87 deletions

View File

@@ -0,0 +1,15 @@
from .FplSensor import FplSensor
class FplAverageDailySensor(FplSensor):
def __init__(self, hass, config, account):
FplSensor.__init__(self, hass, config, account, "Average Daily")
@property
def state(self):
return self.data["daily_avg"]
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self.attr

View File

@@ -0,0 +1,16 @@
from .FplSensor import FplSensor
class FplDailyUsageSensor(FplSensor):
def __init__(self, hass, config, account):
FplSensor.__init__(self, hass, config, account, "Daily Usage")
@property
def state(self):
return self.data["daily_usage"][-1]["cost"]
@property
def device_state_attributes(self):
"""Return the state attributes."""
self.attr["date"] = self.data["daily_usage"][-1]["date"]
return self.attr

View File

@@ -0,0 +1,68 @@
from homeassistant.helpers.entity import Entity
from homeassistant import util
from .const import DOMAIN, DOMAIN_DATA, ATTRIBUTION
from datetime import timedelta
MIN_TIME_BETWEEN_SCANS = timedelta(minutes=30)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=60)
class FplSensor(Entity):
def __init__(self, hass, config, account, sensorName):
self._config = config
self._state = None
self.loop = hass.loop
self._account = account
self.attr = {}
self.data = None
self.sensorName = sensorName
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_UPDATES)
async def async_update(self):
"""Update the sensor."""
# Send update "signal" to the component
await self.hass.data[DOMAIN_DATA]["client"].update_data()
# Get new data (if any)
if "data" in self.hass.data[DOMAIN_DATA]:
self.data = self.hass.data[DOMAIN_DATA]["data"][self._account]
# Set/update attributes
self.attr["attribution"] = ATTRIBUTION
async def async_added_to_hass(self):
await self.async_update()
@property
def device_info(self):
return {
"identifiers": {(DOMAIN, self._account)},
"name": f"Account {self._account}",
"manufacturer": "Florida Power & Light",
}
@property
def unique_id(self):
"""Return the ID of this device."""
id = "{}{}{}".format(
DOMAIN, self._account, self.sensorName.lower().replace(" ", "")
)
return id
@property
def name(self):
return f"{DOMAIN.upper()} {self._account} {self.sensorName}"
@property
def state(self):
return self._state
@property
def icon(self):
return "mdi:flash"
@property
def device_state_attributes(self):
"""Return the state attributes."""
return self.attr

View File

@@ -0,0 +1,31 @@
from .FplSensor import FplSensor
class FplProjectedBillSensor(FplSensor):
def __init__(self, hass, config, account):
FplSensor.__init__(self, hass, config, account, "Projected Bill")
@property
def state(self):
data = self.data
state = None
if "budget_bill" in data.keys():
if data["budget_bill"]:
if "budget_billing_projected_bill" in data.keys():
state = data["budget_billing_projected_bill"]
else:
if "projected_bill" in data.keys():
state = data["projected_bill"]
return state
@property
def device_state_attributes(self):
"""Return the state attributes."""
if "budget_bill" in self.data.keys():
self.attr["budget_bill"] = self.data["budget_bill"]
return self.attr
@property
def icon(self):
return "mdi:currency-usd"

View File

@@ -1,18 +1,62 @@
""" FPL Component """
import logging
from datetime import timedelta
from homeassistant.core import Config, HomeAssistant
from homeassistant.util import Throttle
from .fplapi import FplApi
from .const import DOMAIN_DATA, CONF_USERNAME, CONF_PASSWORD
MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)
from .config_flow import FplFlowHandler
from .const import DOMAIN
class FplData:
"""This class handle communication and stores the data."""
def __init__(self, hass, client):
"""Initialize the class."""
self.hass = hass
self.client = client
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def update_data(self):
"""Update data."""
# This is where the main logic to update platform data goes.
try:
data = await self.client.get_data()
self.hass.data[DOMAIN_DATA]["data"] = data
except Exception as error: # pylint: disable=broad-except
_LOGGER.error("Could not update data - %s", error)
async def async_setup(hass: HomeAssistant, config: Config) -> bool:
"""Set up configured Fpl."""
return True
async def async_setup_entry(hass, config_entry):
# Get "global" configuration.
username = config_entry.data.get(CONF_USERNAME)
password = config_entry.data.get(CONF_PASSWORD)
# Create DATA dict
hass.data[DOMAIN_DATA] = {}
# Configure the client.
_LOGGER.info(f"Configuring the client")
client = FplApi(username, password, hass.loop)
fplData = FplData(hass, client)
await fplData.update_data()
hass.data[DOMAIN_DATA]["client"] = fplData
"""Set up Fpl as config entry."""
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, "sensor")

View File

@@ -2,17 +2,17 @@ from collections import OrderedDict
import voluptuous as vol
from .fplapi import FplApi
from homeassistant import config_entries
import aiohttp
from .const import (
DOMAIN,
CONF_USERNAME,
CONF_PASSWORD,
CONF_NAME,
from .const import DOMAIN, CONF_USERNAME, CONF_PASSWORD, CONF_NAME
from .fplapi import (
LOGIN_RESULT_OK,
LOGIN_RESULT_INVALIDUSER,
LOGIN_RESULT_INVALIDPASSWORD,
)
from homeassistant.core import callback
@@ -34,7 +34,9 @@ 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 = {}
@@ -48,12 +50,16 @@ class FplFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
password = user_input[CONF_PASSWORD]
if username not in configured_instances(self.hass):
result = await self._test_credentials(username, password)
api = FplApi(username, password, None)
result = await api.login()
if result == LOGIN_RESULT_OK:
return self.async_create_entry(
title=user_input[CONF_NAME], data=user_input
)
fplData = await api.get_data()
accounts = fplData["accounts"]
user_input["accounts"] = accounts
return self.async_create_entry(title="", data=user_input)
if result == LOGIN_RESULT_INVALIDUSER:
self._errors[CONF_USERNAME] = "invalid_username"
@@ -77,18 +83,14 @@ class FplFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
# Defaults
username = ""
password = ""
name = "Home"
if user_input is not None:
if CONF_USERNAME in user_input:
username = user_input[CONF_USERNAME]
if CONF_PASSWORD in user_input:
password = user_input[CONF_PASSWORD]
if CONF_NAME in user_input:
name = user_input[CONF_NAME]
data_schema = OrderedDict()
data_schema[vol.Required(CONF_NAME, default=name)] = str
data_schema[vol.Required(CONF_USERNAME, default=username)] = str
data_schema[vol.Required(CONF_PASSWORD, default=password)] = str
@@ -96,14 +98,12 @@ class FplFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
step_id="user", data_schema=vol.Schema(data_schema), errors=self._errors
)
async def _test_credentials(self, username, password):
"""Return true if credentials is valid."""
session = aiohttp.ClientSession()
try:
api = FplApi(username, password, None, session)
result = await api.login()
except Exception: # pylint: disable=broad-except
pass
async def async_step_import(self, user_input): # pylint: disable=unused-argument
"""Import a config entry.
Special type of import, we're not actually going to store any data.
Instead, we're going to rely on the values that are in config file.
"""
if self._async_current_entries():
return self.async_abort(reason="single_instance_allowed")
await session.close()
return result
return self.async_create_entry(title="configuration.yaml", data={})

View File

@@ -16,9 +16,6 @@ REQUIRED_FILES = [
ISSUE_URL = "https://github.com/dotKrad/hass-fpl/issues"
ATTRIBUTION = "Data from this is provided by FPL."
# Icons
ICON = "mdi:flash"
# Device classes
BINARY_SENSOR_DEVICE_CLASS = "connectivity"
@@ -33,11 +30,3 @@ CONF_PASSWORD = "password"
# Defaults
DEFAULT_NAME = DOMAIN
# Api login result
LOGIN_RESULT_OK = "OK"
LOGIN_RESULT_INVALIDUSER = "NOTVALIDUSER"
LOGIN_RESULT_INVALIDPASSWORD = "FAILEDPASSWORD"
STATUS_CATEGORY_OPEN = "OPEN"

View File

@@ -9,7 +9,12 @@ import json
from bs4 import BeautifulSoup
from .const import STATUS_CATEGORY_OPEN, LOGIN_RESULT_OK
STATUS_CATEGORY_OPEN = "OPEN"
# Api login result
LOGIN_RESULT_OK = "OK"
LOGIN_RESULT_INVALIDUSER = "NOTVALIDUSER"
LOGIN_RESULT_INVALIDPASSWORD = "FAILEDPASSWORD"
_LOGGER = logging.getLogger(__name__)
TIMEOUT = 5
@@ -26,31 +31,59 @@ NOTENROLLED = "NOTENROLLED"
class FplApi(object):
"""A class for getting energy usage information from Florida Power & Light."""
def __init__(self, username, password, loop, session):
def __init__(self, username, password, loop):
"""Initialize the data retrieval. Session should have BasicAuth flag set."""
self._username = username
self._password = password
self._loop = loop
self._session = session
self._session = None
async def get_data(self):
self._session = aiohttp.ClientSession()
data = {}
await self.login()
accounts = await self.async_get_open_accounts()
data["accounts"] = accounts
for account in accounts:
accountData = await self.__async_get_data(account)
data[account] = accountData
await self._session.close()
return data
async def login(self):
if self._session is not None:
session = self._session
close = False
else:
session = aiohttp.ClientSession()
close = True
_LOGGER.info("Logging")
"""login and get account information"""
async with async_timeout.timeout(TIMEOUT, loop=self._loop):
response = await self._session.get(
response = await session.get(
URL_LOGIN, auth=aiohttp.BasicAuth(self._username, self._password)
)
js = json.loads(await response.text())
if response.reason == "Unauthorized":
await session.close()
raise Exception(js["messageCode"])
if js["messages"][0]["messageCode"] != "login.success":
_LOGGER.error(f"Logging Failure")
await session.close()
raise Exception("login failure")
_LOGGER.info(f"Logging Successful")
if close:
await session.close()
return LOGIN_RESULT_OK
async def async_get_open_accounts(self):
@@ -71,7 +104,7 @@ class FplApi(object):
# self._premise_number = js["data"]["selectedAccount"]["data"]["acctSecSettings"]["premiseNumber"]
return result
async def async_get_data(self, account):
async def __async_get_data(self, account):
_LOGGER.info(f"Getting Data")
data = {}
@@ -108,7 +141,7 @@ class FplApi(object):
zip_code = accountData["serviceAddress"]["zip"]
# projected bill
pbData = await self.getFromProjectedBill(account, premise, currentBillDate)
pbData = await self.__getFromProjectedBill(account, premise, currentBillDate)
data.update(pbData)
# programs
@@ -124,17 +157,17 @@ class FplApi(object):
if programs["BBL"]:
# budget billing
data["budget_bill"] = True
bblData = await self.getBBL_async(account, data)
bblData = await self.__getBBL_async(account, data)
data.update(bblData)
data.update(
await self.getDataFromEnergyService(account, premise, currentBillDate)
await self.__getDataFromEnergyService(account, premise, currentBillDate)
)
data.update(await self.getDataFromApplianceUsage(account, currentBillDate))
data.update(await self.__getDataFromApplianceUsage(account, currentBillDate))
return data
async def getFromProjectedBill(self, account, premise, currentBillDate):
async def __getFromProjectedBill(self, account, premise, currentBillDate):
async with async_timeout.timeout(TIMEOUT, loop=self._loop):
response = await self._session.get(
URL_RESOURCES_PROJECTED_BILL.format(
@@ -161,7 +194,7 @@ class FplApi(object):
return []
async def getBBL_async(self, account, projectedBillData):
async def __getBBL_async(self, account, projectedBillData):
_LOGGER.info(f"Getting budget billing data")
data = {}
@@ -206,7 +239,7 @@ class FplApi(object):
return data
async def getDataFromEnergyService(self, account, premise, lastBilledDate):
async def __getDataFromEnergyService(self, account, premise, lastBilledDate):
_LOGGER.info(f"Getting data from energy service")
URL = "https://www.fpl.com/dashboard-api/resources/account/{account}/energyService/{account}"
@@ -252,7 +285,7 @@ class FplApi(object):
return []
async def getDataFromApplianceUsage(self, account, lastBilledDate):
async def __getDataFromApplianceUsage(self, account, lastBilledDate):
_LOGGER.info(f"Getting data from applicance usage")
URL = "https://www.fpl.com/dashboard-api/resources/account/{account}/applianceUsage/{account}"
JSON = {"startDate": str(lastBilledDate.strftime("%m%d%Y"))}

View File

@@ -15,8 +15,12 @@ from homeassistant.const import (
CONF_USERNAME,
CONF_PASSWORD,
STATE_UNKNOWN,
ATTR_FRIENDLY_NAME,
)
from .const import DOMAIN, ICON, LOGIN_RESULT_OK
from .const import DOMAIN, DOMAIN_DATA, ATTRIBUTION
from .DailyUsageSensor import FplDailyUsageSensor
from .AverageDailySensor import FplAverageDailySensor
from .ProjectedBillSensor import FplProjectedBillSensor
_LOGGER = logging.getLogger(__name__)
@@ -29,38 +33,25 @@ def setup(hass, config):
async def async_setup_entry(hass, config_entry, async_add_entities):
username = config_entry.data.get(CONF_USERNAME)
password = config_entry.data.get(CONF_PASSWORD)
session = aiohttp.ClientSession()
try:
api = FplApi(username, password, hass.loop, session)
result = await api.login()
accounts = config_entry.data.get("accounts")
fpl_accounts = []
fpl_accounts = []
if result == LOGIN_RESULT_OK:
accounts = await api.async_get_open_accounts()
for account in accounts:
_LOGGER.info(f"Adding fpl account: {account}")
fpl_accounts.append(FplSensor(hass, config_entry.data, account))
for account in accounts:
_LOGGER.info(f"Adding fpl account: {account}")
fpl_accounts.append(FplSensor(hass, config_entry.data, account))
fpl_accounts.append(FplDailyUsageSensor(hass, config_entry.data, account))
fpl_accounts.append(FplAverageDailySensor(hass, config_entry.data, account))
fpl_accounts.append(FplProjectedBillSensor(hass, config_entry.data, account))
async_add_entities(fpl_accounts)
except Exception as e: # pylint: disable=broad-except
_LOGGER.error(f"Adding fpl accounts: {str(e)}")
async_call_later(
hass, 15, async_setup_entry(hass, config_entry, async_add_entities)
)
await session.close()
async_add_entities(fpl_accounts)
class FplSensor(Entity):
def __init__(self, hass, config, account):
self._config = config
self.username = config.get(CONF_USERNAME)
self.password = config.get(CONF_PASSWORD)
self._state = STATE_UNKNOWN
self._state = None
self.loop = hass.loop
self._account = account
@@ -69,6 +60,14 @@ class FplSensor(Entity):
async def async_added_to_hass(self):
await self.async_update()
@property
def device_info(self):
return {
"identifiers": {(DOMAIN, self._account)},
"name": f"Account {self._account}",
"manufacturer": "Florida Power & Light",
}
@property
def unique_id(self):
"""Return the ID of this device."""
@@ -101,7 +100,7 @@ class FplSensor(Entity):
@property
def icon(self):
return ICON
return "mdi:flash"
@property
def state_attributes(self):
@@ -109,16 +108,13 @@ class FplSensor(Entity):
@util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_UPDATES)
async def async_update(self):
try:
session = aiohttp.ClientSession()
api = FplApi(self.username, self.password, self.loop, session)
await api.login()
data = await api.async_get_data(self._account)
# Send update "signal" to the component
await self.hass.data[DOMAIN_DATA]["client"].update_data()
# Get new data (if any)
if "data" in self.hass.data[DOMAIN_DATA]:
data = self.hass.data[DOMAIN_DATA]["data"][self._account]
if data != {}:
self._data = data
except Exception as e: # pylint: disable=broad-except
_LOGGER.warning(f"Error ocurred during update: { str(e)}")
finally:
await session.close()
self._data["attribution"] = ATTRIBUTION