Check your account balance (FinTS, german banks (only?))

Hi,

I just wanted a small tool that checks my account balance and writes it into an entity. I use it to get notified on my phone once a day, so I don't have to check in my account but don't miss anything.
The old fints plugin is no longer working as it does not allow to pass the needed identifier to your bank.

What I did is to use the existing phpFinTs project:

I wrote a small script which is running as a cronjob in one of my proxmox' lxc, but I guess you can as well run it in your home assistant instance.

What it does:
Connects with your bank and passes your login credentials. Passes the identifier to legitimate itself (I used the one from the old fints project which can be found in the forums here).
Every 90 days, a TAN is required - if it is running in unattended mode, it will write a status to notify you about that.
If login is successful, it will get your account balance and publish it via MQTT.

The code - you just need to fill your Account data.

<?php
require __DIR__ . '/vendor/autoload.php';

use Fhp\FinTs;
use Fhp\Options\Credentials;
use Fhp\Options\FinTsOptions;
use Fhp\Action\GetSEPAAccounts;
use Fhp\Action\GetBalance;
use PhpMqtt\Client\MqttClient;
use PhpMqtt\Client\ConnectionSettings;

// --- Fill your data ---
$blz = 'your Bankleitzahl like 123456789';
$user = 'your login name';
$pin = 'your pin/password';
$url = 'https://banking-rl7.s-fints-pt-rl.de/fints30';
$productId = '6151256F3D4F9975B877BD4A2';

$mqttHost = 'your home assistant ip adress';
$mqttPort = 1883;
$mqttUser = 'your HA mqtt user';
$mqttPass = 'your HA mqtt user password';
$topicBalance = 'sparkasse/kontostand';
$topicStatus = 'sparkasse/status';

$stateFile = __DIR__ . '/fints_state.txt';
// -------------------

function sendMqtt($host, $port, $user, $pass, $balanceTopic, $statusTopic, $balance, $status) {
    try {
        $mqtt = new MqttClient($host, $port, 'phpfints_client');
        $settings = (new ConnectionSettings())->setUsername($user)->setPassword($pass);
        $mqtt->connect($settings, true);

        // Discovery
        $mqtt->publish("homeassistant/sensor/sparkasse_kontostand/config", json_encode([
            "name" => "Kontostand KölnBonn",
            "state_topic" => $balanceTopic,
            "unit_of_measurement" => "EUR",
            "icon" => "mdi:cash",
            "unique_id" => "sparkasse_kontostand"
        ]), 0, true);

        $mqtt->publish("homeassistant/sensor/sparkasse_status/config", json_encode([
            "name" => "Sparkasse Status",
            "state_topic" => $statusTopic,
            "unique_id" => "sparkasse_status"
        ]), 0, true);

        if ($balance !== null) {
            $mqtt->publish($balanceTopic, (string)$balance, 0, true);
        }
        $mqtt->publish($statusTopic, $status, 0, true);
        
        $mqtt->disconnect();
        echo "Erfolgreich an MQTT gesendet!\n";
    } catch (\Exception $e) {
        echo "MQTT-Fehler: " . $e->getMessage() . "\n";
    }
}

function handleTan($fints, $action, $context) {
    global $mqttHost, $mqttPort, $mqttUser, $mqttPass, $topicBalance, $topicStatus;
    
    // Prüfen, ob das Skript manuell in der Konsole gestartet wurde
    if (stream_isatty(STDIN)) {
        echo "\n--- TAN ERFORDERLICH ($context) ---\n";
        echo "Bank meldet: " . $action->getTanRequest()->getChallenge() . "\n";
        echo "Bitte chipTAN eingeben: ";
        $tan = trim(fgets(STDIN));
        $fints->submitTan($action, $tan);
    } else {
        // Skript läuft im Hintergrund (Cronjob) -> Abbruch und Meldung an HA!
        echo "TAN erforderlich ($context), aber Skript läuft unbeaufsichtigt im Cronjob!\n";
        sendMqtt($mqttHost, $mqttPort, $mqttUser, $mqttPass, $topicBalance, $topicStatus, null, "TAN_NEEDED");
        exit(1);
    }
}

try {
    echo "Verbinde mit Sparkasse KölnBonn...\n";

    $options = new FinTsOptions();
    $options->url = $url;
    $options->bankCode = $blz;
    $options->productName = $productId;
    $options->productVersion = '1.0';

    $credentials = Credentials::create($user, $pin);

    $persistedInstance = file_exists($stateFile) ? file_get_contents($stateFile) : null;
    if ($persistedInstance) {
        echo "Lade serialisierte Bank-Sitzung...\n";
    }

    $fints = FinTs::new($options, $credentials, $persistedInstance);

    // TAN-Verfahren automatisch wählen
    $tanModes = $fints->getTanModes();
    if (!empty($tanModes)) {
        $selectedMode = reset($tanModes); 
        echo "Automatisch gewähltes TAN-Verfahren: " . $selectedMode->getName() . " (" . $selectedMode->getId() . ")\n";
        $fints->selectTanMode($selectedMode->getId());
    } else {
        $fints->selectTanMode(910);
    }

    // 1. LOGIN
    $login = $fints->login();
    if ($login->needsTan()) {
        handleTan($fints, $login, "LOGIN");
    }

    // 2. KONTEN ABRUFEN
    $getAccounts = GetSEPAAccounts::create();
    $fints->execute($getAccounts);
    if ($getAccounts->needsTan()) {
        handleTan($fints, $getAccounts, "KONTENABRUF");
    }
    
    $accounts = $getAccounts->getAccounts();
    if (empty($accounts)) {
        throw new \Exception("Kein Konto gefunden!");
    }
    $account = $accounts[0];

    // 3. KONTOSTAND ABRUFEN
    $getBalance = GetBalance::create($account);
    $fints->execute($getBalance);
    if ($getBalance->needsTan()) {
        handleTan($fints, $getBalance, "KONTOSTAND");
    }

    // Soll/Haben Berechnung
    $balances = $getBalance->getBalances();
    $amount = 0.0;
    if (!empty($balances)) {
        $hisal = $balances[0];
        $sdo = $hisal->getGebuchterSaldo();
        
        $amount = (float) $sdo->betrag->wert;
        $isDebit = false;

        foreach ($sdo as $key => $val) {
            if (is_object($val) && method_exists($val, 'getValue')) {
                if ($val->getValue() === 'D') {
                    $isDebit = true;
                }
            } elseif (is_string($val) && $val === 'D') {
                $isDebit = true;
            }
        }
        
        if ($isDebit) {
            $amount = -abs($amount);
        }
    }
    echo "Kontostand: " . $amount . " EUR\n";

    // Dialog beenden und speichern
    $fints->close();
    file_put_contents($stateFile, $fints->persist());
    echo "Sitzung serialisiert gespeichert.\n";

    sendMqtt($mqttHost, $mqttPort, $mqttUser, $mqttPass, $topicBalance, $topicStatus, $amount, "OK");

} catch (\Exception $e) {
    echo "Fehler: " . $e->getMessage() . "\n";
    sendMqtt($mqttHost, $mqttPort, $mqttUser, $mqttPass, $topicBalance, $topicStatus, null, "ERROR");
}

I tested it with my Bank (Sparkasse KölnBonn) and it works perfectly. You might need to adjust to your bank, especially the FinTS server URL....

Tell me if you like it :slight_smile: