Created new sensor with specific data and group them by devices(account)
This commit is contained in:
15
custom_components/fpl/AverageDailySensor.py
Normal file
15
custom_components/fpl/AverageDailySensor.py
Normal 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
|
||||
16
custom_components/fpl/DailyUsageSensor.py
Normal file
16
custom_components/fpl/DailyUsageSensor.py
Normal 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
|
||||
68
custom_components/fpl/FplSensor.py
Normal file
68
custom_components/fpl/FplSensor.py
Normal 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
|
||||
31
custom_components/fpl/ProjectedBillSensor.py
Normal file
31
custom_components/fpl/ProjectedBillSensor.py
Normal 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"
|
||||
@@ -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")
|
||||
|
||||
@@ -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={})
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"))}
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user