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 """ """ FPL Component """
import logging
from datetime import timedelta
from homeassistant.core import Config, HomeAssistant 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 .config_flow import FplFlowHandler
from .const import DOMAIN 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: async def async_setup(hass: HomeAssistant, config: Config) -> bool:
"""Set up configured Fpl.""" """Set up configured Fpl."""
return True return True
async def async_setup_entry(hass, config_entry): 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.""" """Set up Fpl as config entry."""
hass.async_create_task( hass.async_create_task(
hass.config_entries.async_forward_entry_setup(config_entry, "sensor") hass.config_entries.async_forward_entry_setup(config_entry, "sensor")

View File

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

View File

@@ -16,9 +16,6 @@ REQUIRED_FILES = [
ISSUE_URL = "https://github.com/dotKrad/hass-fpl/issues" ISSUE_URL = "https://github.com/dotKrad/hass-fpl/issues"
ATTRIBUTION = "Data from this is provided by FPL." ATTRIBUTION = "Data from this is provided by FPL."
# Icons
ICON = "mdi:flash"
# Device classes # Device classes
BINARY_SENSOR_DEVICE_CLASS = "connectivity" BINARY_SENSOR_DEVICE_CLASS = "connectivity"
@@ -33,11 +30,3 @@ 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

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

View File

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