1 """Read the balance of your bank accounts via FinTS."""
3 from __future__
import annotations
5 from collections
import namedtuple
6 from datetime
import timedelta
10 from fints.client
import FinTS3PinTanClient
11 from fints.models
import SEPAAccount
12 from propcache
import cached_property
13 import voluptuous
as vol
16 PLATFORM_SCHEMA
as SENSOR_PLATFORM_SCHEMA,
25 _LOGGER = logging.getLogger(__name__)
29 ICON =
"mdi:currency-eur"
31 BankCredentials = namedtuple(
"BankCredentials",
"blz login pin url")
33 CONF_BIN =
"bank_identification_number"
34 CONF_ACCOUNTS =
"accounts"
35 CONF_HOLDINGS =
"holdings"
36 CONF_ACCOUNT =
"account"
38 ATTR_ACCOUNT = CONF_ACCOUNT
40 ATTR_ACCOUNT_TYPE =
"account_type"
42 SCHEMA_ACCOUNTS = vol.Schema(
44 vol.Required(CONF_ACCOUNT): cv.string,
45 vol.Optional(CONF_NAME, default=
None): vol.Any(
None, cv.string),
49 PLATFORM_SCHEMA = SENSOR_PLATFORM_SCHEMA.extend(
51 vol.Required(CONF_BIN): cv.string,
52 vol.Required(CONF_USERNAME): cv.string,
53 vol.Required(CONF_PIN): cv.string,
54 vol.Required(CONF_URL): cv.string,
55 vol.Optional(CONF_NAME): cv.string,
56 vol.Optional(CONF_ACCOUNTS, default=[]): cv.ensure_list(SCHEMA_ACCOUNTS),
57 vol.Optional(CONF_HOLDINGS, default=[]): cv.ensure_list(SCHEMA_ACCOUNTS),
65 add_entities: AddEntitiesCallback,
66 discovery_info: DiscoveryInfoType |
None =
None,
68 """Set up the sensors.
70 Login to the bank and get a list of existing accounts. Create a
71 sensor for each account.
74 config[CONF_BIN], config[CONF_USERNAME], config[CONF_PIN], config[CONF_URL]
76 fints_name = config.get(CONF_NAME, config[CONF_BIN])
79 acc[CONF_ACCOUNT]: acc[CONF_NAME]
for acc
in config[CONF_ACCOUNTS]
83 acc[CONF_ACCOUNT]: acc[CONF_NAME]
for acc
in config[CONF_HOLDINGS]
86 client =
FinTsClient(credentials, fints_name, account_config, holdings_config)
87 balance_accounts, holdings_accounts = client.detect_accounts()
88 accounts: list[SensorEntity] = []
90 for account
in balance_accounts:
91 if config[CONF_ACCOUNTS]
and account.iban
not in account_config:
92 _LOGGER.debug(
"Skipping account %s for bank %s", account.iban, fints_name)
95 if not (account_name := account_config.get(account.iban)):
96 account_name = f
"{fints_name} - {account.iban}"
97 accounts.append(
FinTsAccount(client, account, account_name))
98 _LOGGER.debug(
"Creating account %s for bank %s", account.iban, fints_name)
100 for account
in holdings_accounts:
101 if config[CONF_HOLDINGS]
and account.accountnumber
not in holdings_config:
103 "Skipping holdings %s for bank %s", account.accountnumber, fints_name
107 account_name = holdings_config.get(account.accountnumber)
109 account_name = f
"{fints_name} - {account.accountnumber}"
112 "Creating holdings %s for bank %s", account.accountnumber, fints_name
119 """Wrapper around the FinTS3PinTanClient.
121 Use this class as Context Manager to get the FinTS3Client object.
126 credentials: BankCredentials,
128 account_config: dict,
129 holdings_config: dict,
131 """Initialize a FinTsClient."""
141 """Get the FinTS client object.
143 The FinTS library persists the current dialog with the bank
144 and stores bank capabilities. So caching the client is beneficial.
147 return FinTS3PinTanClient(
155 """Get a dictionary of account IBANs as key and account information as value."""
159 account[
"iban"]: account
160 for account
in self.
clientclient.get_information()[
"accounts"]
167 """Determine if the given account is of type balance account."""
172 if not account_information:
175 if account_type := account_information.get(
"type"):
176 return 1 <= account_type <= 9
180 or account_information[
"account_number"]
in self.
account_configaccount_config
187 """Determine if the given account of type holdings account."""
192 if not account_information:
195 if account_type := account_information.get(
"type"):
196 return 30 <= account_type <= 39
200 or account_information[
"account_number"]
in self.
holdings_configholdings_config
207 """Identify the accounts of the bank."""
209 balance_accounts = []
210 holdings_accounts = []
212 for account
in self.
clientclient.get_sepa_accounts():
214 balance_accounts.append(account)
217 holdings_accounts.append(account)
221 "Could not determine type of account %s from %s",
223 self.
clientclient.user_id,
226 return balance_accounts, holdings_accounts
230 """Sensor for a FinTS balance account.
232 A balance account contains an amount of money (=balance). The amount may
236 def __init__(self, client: FinTsClient, account, name: str) ->
None:
237 """Initialize a FinTs balance account."""
243 ATTR_ACCOUNT: self.
_account_account.iban,
244 ATTR_ACCOUNT_TYPE:
"balance",
250 """Get the current balance and currency for the account."""
251 bank = self.
_client_client.client
252 balance = bank.get_balance(self.
_account_account)
255 _LOGGER.debug(
"updated balance of account %s", self.
namename)
259 """Sensor for a FinTS holdings account.
261 A holdings account does not contain money but rather some financial
262 instruments, e.g. stocks.
265 def __init__(self, client: FinTsClient, account, name: str) ->
None:
266 """Initialize a FinTs holdings account."""
275 """Get the current holdings for the account."""
276 bank = self.
_client_client.client
282 """Additional attributes of the sensor.
284 Lists each holding of the account with the current value.
287 ATTR_ACCOUNT: self.
_account_account.accountnumber,
288 ATTR_ACCOUNT_TYPE:
"holdings",
291 attributes[ATTR_BANK] = self.
_client_client.name
293 total_name = f
"{holding.name} total"
294 attributes[total_name] = holding.total_value
295 pieces_name = f
"{holding.name} pieces"
296 attributes[pieces_name] = holding.pieces
297 price_name = f
"{holding.name} price"
298 attributes[price_name] = holding.market_value
None __init__(self, FinTsClient client, account, str name)
_attr_native_unit_of_measurement
_attr_extra_state_attributes
bool is_balance_account(self, SEPAAccount account)
FinTS3PinTanClient client(self)
_account_information_fetched
None __init__(self, BankCredentials credentials, str name, dict account_config, dict holdings_config)
tuple[list, list] detect_accounts(self)
bool is_holdings_account(self, SEPAAccount account)
dict|None get_account_information(self, str iban)
None __init__(self, FinTsClient client, account, str name)
_attr_native_unit_of_measurement
dict[str, Any] extra_state_attributes(self)
str|UndefinedType|None name(self)
web.Response get(self, web.Request request, str config_key)
None setup_platform(HomeAssistant hass, ConfigType config, AddEntitiesCallback add_entities, DiscoveryInfoType|None discovery_info=None)
def add_entities(account, async_add_entities, tracked)