core/homeassistant/components/fints/sensor.py

237 lines
7.8 KiB
Python
Raw Normal View History

"""Read the balance of your bank accounts via FinTS."""
from __future__ import annotations
2018-05-17 00:45:47 +00:00
from collections import namedtuple
from datetime import timedelta
import logging
from typing import Any
2019-12-04 14:19:03 +00:00
from fints.client import FinTS3PinTanClient
2018-05-17 00:45:47 +00:00
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
2019-12-04 14:19:03 +00:00
from homeassistant.const import CONF_NAME, CONF_PIN, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant
2018-05-17 00:45:47 +00:00
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
2018-05-17 00:45:47 +00:00
_LOGGER = logging.getLogger(__name__)
SCAN_INTERVAL = timedelta(hours=4)
2019-07-31 19:25:30 +00:00
ICON = "mdi:currency-eur"
2018-05-17 00:45:47 +00:00
2019-07-31 19:25:30 +00:00
BankCredentials = namedtuple("BankCredentials", "blz login pin url")
2018-05-17 00:45:47 +00:00
2019-07-31 19:25:30 +00:00
CONF_BIN = "bank_identification_number"
CONF_ACCOUNTS = "accounts"
CONF_HOLDINGS = "holdings"
CONF_ACCOUNT = "account"
2018-05-17 00:45:47 +00:00
ATTR_ACCOUNT = CONF_ACCOUNT
2019-07-31 19:25:30 +00:00
ATTR_BANK = "bank"
ATTR_ACCOUNT_TYPE = "account_type"
SCHEMA_ACCOUNTS = vol.Schema(
{
vol.Required(CONF_ACCOUNT): cv.string,
vol.Optional(CONF_NAME, default=None): vol.Any(None, cv.string),
}
)
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_BIN): cv.string,
vol.Required(CONF_USERNAME): cv.string,
vol.Required(CONF_PIN): cv.string,
vol.Required(CONF_URL): cv.string,
vol.Optional(CONF_NAME): cv.string,
vol.Optional(CONF_ACCOUNTS, default=[]): cv.ensure_list(SCHEMA_ACCOUNTS),
vol.Optional(CONF_HOLDINGS, default=[]): cv.ensure_list(SCHEMA_ACCOUNTS),
}
)
2018-05-17 00:45:47 +00:00
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
2018-05-17 00:45:47 +00:00
"""Set up the sensors.
Login to the bank and get a list of existing accounts. Create a
sensor for each account.
"""
2019-07-31 19:25:30 +00:00
credentials = BankCredentials(
config[CONF_BIN], config[CONF_USERNAME], config[CONF_PIN], config[CONF_URL]
)
2018-05-17 00:45:47 +00:00
fints_name = config.get(CONF_NAME, config[CONF_BIN])
2019-07-31 19:25:30 +00:00
account_config = {
acc[CONF_ACCOUNT]: acc[CONF_NAME] for acc in config[CONF_ACCOUNTS]
}
2018-05-17 00:45:47 +00:00
2019-07-31 19:25:30 +00:00
holdings_config = {
acc[CONF_ACCOUNT]: acc[CONF_NAME] for acc in config[CONF_HOLDINGS]
}
2018-05-17 00:45:47 +00:00
client = FinTsClient(credentials, fints_name)
balance_accounts, holdings_accounts = client.detect_accounts()
accounts: list[SensorEntity] = []
2018-05-17 00:45:47 +00:00
for account in balance_accounts:
if config[CONF_ACCOUNTS] and account.iban not in account_config:
_LOGGER.info("Skipping account %s for bank %s", account.iban, fints_name)
2018-05-17 00:45:47 +00:00
continue
2021-10-22 09:13:05 +00:00
if not (account_name := account_config.get(account.iban)):
account_name = f"{fints_name} - {account.iban}"
2018-05-17 00:45:47 +00:00
accounts.append(FinTsAccount(client, account, account_name))
2019-07-31 19:25:30 +00:00
_LOGGER.debug("Creating account %s for bank %s", account.iban, fints_name)
2018-05-17 00:45:47 +00:00
for account in holdings_accounts:
2019-07-31 19:25:30 +00:00
if config[CONF_HOLDINGS] and account.accountnumber not in holdings_config:
_LOGGER.info(
"Skipping holdings %s for bank %s", account.accountnumber, fints_name
2019-07-31 19:25:30 +00:00
)
2018-05-17 00:45:47 +00:00
continue
account_name = holdings_config.get(account.accountnumber)
if not account_name:
account_name = f"{fints_name} - {account.accountnumber}"
2018-05-17 00:45:47 +00:00
accounts.append(FinTsHoldingsAccount(client, account, account_name))
2019-07-31 19:25:30 +00:00
_LOGGER.debug(
"Creating holdings %s for bank %s", account.accountnumber, fints_name
)
2018-05-17 00:45:47 +00:00
add_entities(accounts, True)
2018-05-17 00:45:47 +00:00
class FinTsClient:
2018-05-17 00:45:47 +00:00
"""Wrapper around the FinTS3PinTanClient.
Use this class as Context Manager to get the FinTS3Client object.
"""
def __init__(self, credentials: BankCredentials, name: str) -> None:
"""Initialize a FinTsClient."""
2018-05-17 00:45:47 +00:00
self._credentials = credentials
self.name = name
@property
def client(self):
"""Get the client object.
As the fints library is stateless, there is not benefit in caching
the client objects. If that ever changes, consider caching the client
object and also think about potential concurrency problems.
2022-06-22 23:08:51 +00:00
Note: As of version 2, the fints library is not stateless anymore.
This should be considered when reworking this integration.
2018-05-17 00:45:47 +00:00
"""
2019-07-31 19:25:30 +00:00
2018-05-17 00:45:47 +00:00
return FinTS3PinTanClient(
2019-07-31 19:25:30 +00:00
self._credentials.blz,
self._credentials.login,
self._credentials.pin,
self._credentials.url,
)
2018-05-17 00:45:47 +00:00
def detect_accounts(self):
"""Identify the accounts of the bank."""
2019-07-31 19:25:30 +00:00
2022-06-22 23:08:51 +00:00
bank = self.client
accounts = bank.get_sepa_accounts()
account_types = {
x["iban"]: x["type"]
for x in bank.get_information()["accounts"]
if x["iban"] is not None
}
2018-05-17 00:45:47 +00:00
balance_accounts = []
holdings_accounts = []
2022-06-22 23:08:51 +00:00
for account in accounts:
account_type = account_types[account.iban]
if 1 <= account_type <= 9: # 1-9 is balance account
2018-05-17 00:45:47 +00:00
balance_accounts.append(account)
2022-06-22 23:08:51 +00:00
elif 30 <= account_type <= 39: # 30-39 is holdings account
2018-05-17 00:45:47 +00:00
holdings_accounts.append(account)
return balance_accounts, holdings_accounts
class FinTsAccount(SensorEntity):
2018-08-19 20:29:08 +00:00
"""Sensor for a FinTS balance account.
2018-05-17 00:45:47 +00:00
A balance account contains an amount of money (=balance). The amount may
also be negative.
"""
def __init__(self, client: FinTsClient, account, name: str) -> None:
"""Initialize a FinTs balance account."""
self._client = client
2018-05-17 00:45:47 +00:00
self._account = account
self._attr_name = name
self._attr_icon = ICON
self._attr_extra_state_attributes = {
ATTR_ACCOUNT: self._account.iban,
ATTR_ACCOUNT_TYPE: "balance",
}
if self._client.name:
self._attr_extra_state_attributes[ATTR_BANK] = self._client.name
2018-05-17 00:45:47 +00:00
def update(self) -> None:
"""Get the current balance and currency for the account."""
bank = self._client.client
balance = bank.get_balance(self._account)
self._attr_native_value = balance.amount.amount
self._attr_native_unit_of_measurement = balance.amount.currency
2019-07-31 19:25:30 +00:00
_LOGGER.debug("updated balance of account %s", self.name)
2018-05-17 00:45:47 +00:00
class FinTsHoldingsAccount(SensorEntity):
2018-05-17 00:45:47 +00:00
"""Sensor for a FinTS holdings account.
A holdings account does not contain money but rather some financial
instruments, e.g. stocks.
"""
def __init__(self, client: FinTsClient, account, name: str) -> None:
"""Initialize a FinTs holdings account."""
self._client = client
self._attr_name = name
2018-05-17 00:45:47 +00:00
self._account = account
self._holdings: list[Any] = []
self._attr_icon = ICON
self._attr_native_unit_of_measurement = "EUR"
2018-05-17 00:45:47 +00:00
def update(self) -> None:
"""Get the current holdings for the account."""
bank = self._client.client
self._holdings = bank.get_holdings(self._account)
self._attr_native_value = sum(h.total_value for h in self._holdings)
2018-05-17 00:45:47 +00:00
@property
def extra_state_attributes(self) -> dict:
2018-05-17 00:45:47 +00:00
"""Additional attributes of the sensor.
Lists each holding of the account with the current value.
"""
attributes = {
ATTR_ACCOUNT: self._account.accountnumber,
2019-07-31 19:25:30 +00:00
ATTR_ACCOUNT_TYPE: "holdings",
2018-05-17 00:45:47 +00:00
}
if self._client.name:
attributes[ATTR_BANK] = self._client.name
for holding in self._holdings:
total_name = f"{holding.name} total"
2018-05-17 00:45:47 +00:00
attributes[total_name] = holding.total_value
pieces_name = f"{holding.name} pieces"
2018-05-17 00:45:47 +00:00
attributes[pieces_name] = holding.pieces
price_name = f"{holding.name} price"
2018-05-17 00:45:47 +00:00
attributes[price_name] = holding.market_value
return attributes