Manchmal habe ich eine Idee und suche nach der Lösung. Manchmal sehe ich aber auch zuerst die Lösung und habe dann die Idee. So auch als ich aus irgendeinem Grund über das Python Modul FinTS gestolpert bin.
Während ich schon seit einiger Zeit (über meinen Vater) weiß dass man eigentlich jedes deutsche EC Konto mittels einer Art API namens HBCI auslesen kann, so erfuhr ich das FinTS der Nachfolger ist. Und dass es ein sehr einfach zu bedienendes Python Modul dafür gibt…
Und als ich mir das so durchlas kam dann auch die Idee auf – warum speichere ich nicht alle meine Kontobewegungen vollautomatisch in einer MySQL Datenbank auf meinem Server? Und wenn ich da schon ein (Python) Programm laufen lasse welches meine Kontobewegungen holt – warum schreibt mir das nicht gleich eine Mail wenns was neues gibt?
Na dann: Auf ans Werk 🙂
Eine neue Datenbank ist schnell angelegt und eine Tabellenstruktur ausgedacht:
CREATE TABLE `konto`.`kontobewegungen` (
`bewegid` INT NOT NULL AUTO_INCREMENT,
`inserttime` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`hash` VARCHAR(128) NOT NULL DEFAULT '',
`onlyrequested` TINYINT NOT NULL DEFAULT 0,
`transaction_code` VARCHAR(32) NOT NULL DEFAULT '',
`transaction_details` TEXT NOT NULL DEFAULT '',
`prima_nota` VARCHAR(32) NOT NULL DEFAULT '',
`applicant_name` TEXT NOT NULL DEFAULT '',
`customer_reference` VARCHAR(32) NOT NULL DEFAULT '',
`extra_details` TEXT NOT NULL DEFAULT '',
`purpose` TEXT NOT NULL DEFAULT '',
`additional_purpose` TEXT NOT NULL DEFAULT '',
`applicant_bin` VARCHAR(32) NOT NULL DEFAULT '',
`applicant_iban` VARCHAR(32) NOT NULL DEFAULT '',
`funds_code` VARCHAR(32) NOT NULL DEFAULT '',
`date` DATE NOT NULL,
`return_debit_notes` VARCHAR(32) NOT NULL DEFAULT '',
`status` VARCHAR(32) NOT NULL DEFAULT '',
`currency` VARCHAR(32) NOT NULL DEFAULT '',
`posting_text` VARCHAR(32) NOT NULL DEFAULT '',
`recipient_name` VARCHAR(64) NOT NULL DEFAULT '',
`bank_reference` VARCHAR(32) NOT NULL DEFAULT '',
`amount` DECIMAL(15,3) NOT NULL DEFAULT '0',
`entry_date` DATE NOT NULL,
`id` VARCHAR(32) NOT NULL DEFAULT '',
`balance` DECIMAL(15,3) NOT NULL DEFAULT '0',
PRIMARY KEY (`bewegid`))
CHARACTER SET utf8 COLLATE utf8_unicode_ci,
ENGINE = InnoDB;
Das Tabellendesign ist ziemlich einfach und wahrscheinlich alles andere als effizient, aber meine privaten 500 Kontobewegungen am Tag lassen sich da noch leicht abbilden 😉
Interessant ist das Feld ‚hash‘ welches einfach einen SHA512-Hash über alle relevanten Daten einer einzelnen Kontobewegung bildet und man mit diesem dann nachsehen kann ob wir eine bestimmte Buchung schon in der Datenbank haben.
Bei meiner Bank (der Postbank) ist es so dass diese über FinTS/HBCI in etwa die letzten drei Monate an Kontobewegungen anbietet – eine Art eindeutige ID gibt es aber nicht! Deshalb müssen wir uns mit dem Hash eine Möglichkeit bauen um eine Bewegung eindeutig identifizieren zu können.
Über das Feld gibt es natürlich einen Index zum schnellen Abfragen:
ALTER TABLE `postbankkonto`.`kontobewegungen` ADD UNIQUE `hashidx` (`hash`);
Das nächste erwähnenswerte Feld ist ‚onlyrequested‘. Manchmal kommen Kontobewegungen mit Datum in der Zukunft vor. Diese sind „angefragte Buchungen“ die keinen Einfluss auf den Saldo haben. Sie verschwinden auch nach einiger Zeit von selbst wieder. Wir heben sie trotzdem auf – weil wir es können!
Achja, der Saldo: Man kriegt über HBCI zwar den Saldo, aber nur den aktuellen. Man muss also den Saldo nach jeder Buchung selber ausrechnen. Naaaaa gut…
Erwähnenswert sind übrigens auch noch die Felder ‚transaction_code‘ und ‚id‘: Diese Felder bekomme ich so von der Bank und ich hab leider keine Ahnung was „105“, „117“ oder „166“ für den transaction_code und „N005“, „N010“ oder „N051“ für die id denn heißen soll. Man kann es sich zwar zusammenreimen (Das eine ist ne Gutschrift, das andere ne Lastschrift das nächste ein vorgemerkter Umsatz…) aber eine sinnvolle Erklärung scheint es nicht zu geben im großen weiten Internet.
Ich hab gerade mal eine Erklärung für irgendeine holländische Bank gefunden. Das passt aber nicht zu meinen Daten.
Es scheint mit der Transparenz bei den Banken nicht soweit her zu sein.
Achja, Transparenz: Fürs verbinden benötigt man seine Kontonummer, seine Telefon-Banking-Pin (zumindest bei der Postbank) und die API-Adresse. Diese API Adresse zu finden ist auch nochmal etwas schwierig aber für die Postbank heißt sie: „https://hbci.postbank.de/banking/hbci.do“.
Außerdem bekommt man übrigens alle Konten (und Depots) die man bei der Bank führt in einer Liste zurück. Ich frage hier aber nur mein eigentliches EC-Konto ab. (das ist das erste Konto in der Liste!)
Viel mehr als Bewegungen holen und Salden abfragen kann man wohl scheinbar mit dem FinTS nicht machen. Überweisungen tätigen kann man jedenfalls nicht. Aber das will ich auch garnicht. Besser dass das nicht geht 🙂
Und damit sich jetzt jeder über meine Programmierkenntnisse erfreuen kann, gibts hier den vollen Programmcode zum zerpflücken und kommentieren. Quasi ein Weihnachtsgeschenk 🙂
#!/usr/bin/python3 import re import pymysql import hashlib import time import smtplib import datetime from decimal import * from email.mime.text import MIMEText from fints.client import FinTS3PinTanClient blz = '123' account = '456' pin = '789' endpoint = 'https://hbci.postbank.de/banking/hbci.do' mysqlhost = '127.0.0.1' mysqluser = 'xxx' mysqlpass = 'yyy' mysqldb = 'zzz' mailsender = 'banking@myserver.de' mailreceiver = 'xxx@yyy.de' mailserver = '127.0.0.1' mailsubject = 'Kontobewegung!' def mailtransfer(mailtext): message = '' message = message + mailtext msg = MIMEText(message, _charset="UTF-8") msg['Subject'] = mailsubject msg['From'] = mailsender msg['To'] = mailreceiver s = smtplib.SMTP(mailserver) s.send_message(msg) s.quit() # Connect to the database conn = pymysql.connect(host = mysqlhost, user = mysqluser, passwd = mysqlpass, db = mysqldb) # Connect to the bank f = FinTS3PinTanClient(blz, account, pin, endpoint) # Get all acounts konten = f.get_sepa_accounts() # Get all Statements # (We only get a few months anyway!) bewegungen = f.get_statement(konten[0], datetime.date(2010, 10, 1), datetime.date.today()) # Loop through all statements inserted = 0 for bewegung in bewegungen: # Hash this statement data myhash = '' myhash += str(bewegung.data.get('transaction_code', '')) myhash += str(bewegung.data.get('transaction_details', '')) myhash += str(bewegung.data.get('prima_nota', '')) myhash += str(bewegung.data.get('applicant_name', '')) myhash += str(bewegung.data.get('customer_reference', '')) myhash += str(bewegung.data.get('extra_details', '')) myhash += str(bewegung.data.get('purpose', '')) myhash += str(bewegung.data.get('additional_purpose', '')) myhash += str(bewegung.data.get('applicant_bin', '')) myhash += str(bewegung.data.get('applicant_iban', '')) myhash += str(bewegung.data.get('funds_code', '')) myhash += str(bewegung.data.get('date', '')) myhash += str(bewegung.data.get('return_debit_notes', '')) myhash += str(bewegung.data.get('status', '')) myhash += str(bewegung.data.get('currency', '')) myhash += str(bewegung.data.get('posting_text', '')) myhash += str(bewegung.data.get('recipient_name', '')) myhash += str(bewegung.data.get('bank_reference', '')) myhash += str(bewegung.data.get('amount', '')) myhash += str(bewegung.data.get('entry_date', '')) myhash += str(bewegung.data.get('id', '')) myhash = hashlib.sha512(myhash.encode('UTF-8')).hexdigest() # Check if we already got this hash cur = conn.cursor() query = "SELECT 1 from kontobewegungen where hash = '" + myhash + "';" cur.execute(query) rowcount = cur.rowcount if rowcount == 0: # Get the amount (some Regexp needs to be done here, because it is something like '<-1.65 EUR>') amount = '' p = re.compile('[-+]?\d*\.\d+|[-+]?\d+') m = p.search(str(bewegung.data['amount'])) amount = m.group() # Check if it's for the feature (amount is only requested, not yet really booked!) onlyrequested = 0 bookingdate = datetime.datetime.strptime(str(bewegung.data.get('date', '')), '%Y-%m-%d').date() if bookingdate > datetime.date.today(): onlyrequested = 1 # Insert it! cur = conn.cursor() query = '' query += "INSERT INTO kontobewegungen (hash, onlyrequested, transaction_code, transaction_details, prima_nota, applicant_name, customer_reference, extra_details," query += "purpose, additional_purpose, applicant_bin, applicant_iban, funds_code, date, return_debit_notes, status, currency, " query += "posting_text, recipient_name, bank_reference, amount, entry_date, id) " query += "VALUES ('" + myhash + "', " + str(onlyrequested) + ", '" + str(bewegung.data.get('transaction_code', '')) + "', '" +str(bewegung.data.get('transaction_details', '')) + "', '"+ str(bewegung.data.get('prima_nota', '')) + "', '" query += str(bewegung.data.get('applicant_name', '')) + "', '" + str(bewegung.data.get('customer_reference', '')) + "', '" + str(bewegung.data.get('extra_details', '')) + "', '" query += str(bewegung.data.get('purpose', '')) + "', '" + str(bewegung.data.get('additional_purpose', '')) + "', '" + str(bewegung.data.get('applicant_bin', '')) + "', '" query += str(bewegung.data.get('applicant_iban', '')) + "', '" + str(bewegung.data.get('funds_code', '')) + "', '" + str(bewegung.data.get('date', '')) + "', '" query += str(bewegung.data.get('return_debit_notes', '')) + "', '" + str(bewegung.data.get('status', '')) + "', '" + str(bewegung.data.get('currency', '')) + "', '" query += str(bewegung.data.get('posting_text', '')) + "', '" + str(bewegung.data.get('recipient_name', ''))+ "', '" + str(bewegung.data.get('bank_reference', '')) + "', '" query += str(amount) + "', '" + str(bewegung.data.get('entry_date', '')) + "', '" + str(bewegung.data.get('id', '')) + "');" cur.execute(query) conn.commit() inserted = inserted + 1 # Mail it mailtext = '' if onlyrequested == 1: mailtext += "ACHTUNG: VORABBUCHUNG\n\n" mailtext += str(bewegung.data) mailtransfer(mailtext) # Now get the Balance, and add it to every balance colum which is still empty from behind endbalance = f.get_balance(konten[0]) p = re.compile('[-+]?\d*\.\d+|[-+]?\d+') m = p.search(str(endbalance)) endbalance = m.group() amountbefore = Decimal(endbalance) bewegungsamountbefore = 0 cur = conn.cursor() query = 'SELECT bewegid, amount FROM kontobewegungen WHERE balance = 0 AND onlyrequested = 0 ORDER BY bewegid DESC;' cur.execute(query) for row in cur.fetchall(): bewegid = row[0] # Calculate balance balance = amountbefore - bewegungsamountbefore # Update the balance in the database cur2 = conn.cursor() query = 'UPDATE kontobewegungen SET balance = ' + str(balance) + ' WHERE bewegid = ' + str(bewegid) + ';' cur2.execute(query) conn.commit() # Update the fields bewegungsamountbefore = row[1] amountbefore = balance print(time.strftime("%d.%m.%Y %H:%M:%S") + ': Inserted: ' + str(inserted) + ' Balance: ' + str(endbalance) + ' EUR') conn.close()
PS: Wie die Banken auf die Idee kommen einen Betrag so zu schicken, ist mir vollkommen unklar:
<-1.65 EUR>
Ich bin natürlich gleich mit meinem Regexp auf die Schnauze gefallen als ich mal einen Kontosaldo ohne Nachkommastellen hatte. Denn plötzlich kam nicht ‚<1234.56 EUR>‘ sondern:
<-1234 EUR>
Die Nachkommastellen einfach weglassen wenn sie nicht relevant sind find ich auch eine sehr seltsame Design-Entscheidung!
Das Script läuft jetzt übrigens per Cronjob im 15-Minuten Takt. Somit bekomme ich spätestens 15 Minuten nach einer Kontobewegung eine Mail.
Achja, das FinTS Modul installiert man unter Ubuntu 16.04 wie gewohnt:
apt-get install python3 python3-pip
pip3 install fints
Schreibe einen Kommentar