Hier ein bisschen Theorie, da ich kein Shelly-Gerät besitze und erst recht keine "Münzwaschmaschine".
Vorausgesetzt, du verfügst über einen Server (das kann z.B. ein kleiner Raspberry sein, oder ein 4€ Cloud-Server bei Hetzner) mit PHP und MySQL (zum Speichern von Transaktionen, wer weiß, wofür das mal nützlich sein kann). Außerdem benötigst du ein PayPal-Business-Konto, sowie API-Zugang via https://developer.paypal.com.
Dann ein paar (rudimentäre) PHP-Scripts:
<?php
// PayPal
define('PAYPAL_CLIENT_ID', 'DEINE_CLIENT_ID');
define('PAYPAL_SECRET', 'DEIN_SECRET');
// Gib hier die API-Umgebung an:
// - "api-m.sandbox.paypal.com" für Sandbox
// - "api-m.paypal.com" für Live
define('PAYPAL_API_BASE', 'https://api-m.sandbox.paypal.com');
// Shelly
define('SHELLY_IP', '192.168.1.50'); // IP deines Shelly-Geräts
define('SHELLY_RELAY', 0); // In der Regel "0" für den ersten Relais-Kanal
// Waschmaschine
define('POWER_THRESHOLD', 10); // Beispielschwelle (in Watt), ab der die Maschine als "läuft" gilt
define('RUNTIME_IN_HOURS', 3); // Laufzeit, die nach Freischaltung gewährt wird
// MySQL
define('DB_HOST', 'localhost');
define('DB_NAME', 'shelly_wama');
define('DB_USER', 'root');
define('DB_PASS', '');
Alles anzeigen
<?php
require_once 'config.php';
/**
* Gibt ein Access Token für PayPal zurück.
*/
function getPayPalAccessToken(): ?string
{
$url = PAYPAL_API_BASE . "/v1/oauth2/token";
$auth = base64_encode(PAYPAL_CLIENT_ID . ":" . PAYPAL_SECRET);
$headers = [
"Authorization: Basic $auth",
"Content-Type: application/x-www-form-urlencoded",
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_POSTFIELDS, "grant_type=client_credentials");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
if ($response === false) {
return null;
}
curl_close($ch);
$data = json_decode($response, true);
return $data['access_token'] ?? null;
}
/**
* Holt Informationen zu einem PayPal-Order (Bezahlvorgang).
*/
function getPayPalOrderDetails(string $orderId, string $accessToken): ?array
{
$url = PAYPAL_API_BASE . "/v2/checkout/orders/$orderId";
$headers = [
"Authorization: Bearer $accessToken",
"Content-Type: application/json",
];
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
if ($response === false) {
return null;
}
curl_close($ch);
$data = json_decode($response, true);
return $data;
}
/**
* Schaltet das Shelly-Relais an oder aus.
* @param string $state 'on' oder 'off'
*/
function switchShellyRelay(string $state): bool
{
$url = "http://" . SHELLY_IP . "/relay/" . SHELLY_RELAY;
$data = http_build_query(['turn' => $state]);
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_POSTFIELDS, $data);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
return $response !== false;
}
/**
* Fragt den aktuellen Stromverbrauch (Watt) des Shelly 1PM ab.
*/
function getShellyPower(): float
{
$url = "http://" . SHELLY_IP . "/status";
$ch = curl_init($url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);
if ($response === false) {
return 0.0;
}
$data = json_decode($response, true);
// Je nach Shelly-Typ kann die Struktur leicht variieren:
return $data['meters'][0]['power'] ?? 0.0;
}
/**
* Stellt fest, ob die Waschmaschine läuft.
*/
function isWashingMachineRunning(): bool
{
$power = getShellyPower();
return $power > POWER_THRESHOLD;
}
/**
* Datenbank-Funktionen
*/
function dbConnect(): ?PDO
{
try {
return new PDO('mysql:host='.DB_HOST.';dbname='.DB_NAME, DB_USER, DB_PASS, [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
]);
} catch (PDOException $e) {
// Im Fehlerfall null zurückgeben oder Exception werfen
return null;
}
}
/**
* Speicherung einer Transaktion
*/
function saveTransaction(string $orderId, string $status): void
{
$pdo = dbConnect();
if (!$pdo) {
return;
}
$stmt = $pdo->prepare("INSERT INTO transactions (order_id, status, created_at) VALUES (:oid, :status, NOW())");
$stmt->execute([
':oid' => $orderId,
':status' => $status
]);
}
Alles anzeigen
<?php
require_once 'functions.php';
// Prüfe, ob die "orderID" vorhanden ist
if (!isset($_GET['orderID'])) {
exit("Keine Order-ID vorhanden.");
}
$orderId = $_GET['orderID'];
// 1. Access Token holen
$accessToken = getPayPalAccessToken();
if (!$accessToken) {
exit("Fehler beim Abrufen des PayPal Access Tokens.");
}
// 2. Order-Details abfragen
$orderDetails = getPayPalOrderDetails($orderId, $accessToken);
if (!$orderDetails || !isset($orderDetails['status'])) {
exit("Fehler beim Abrufen der Zahlungsdetails.");
}
// 3. Bestätigen, dass Zahlung erfolgreich (COMPLETED) ist
if ($orderDetails['status'] !== 'COMPLETED') {
exit("Zahlung nicht abgeschlossen. Status: " . $orderDetails['status']);
}
// Transaktion speichern
saveTransaction($orderId, $orderDetails['status']);
// 4. Status der Waschmaschine prüfen
if (isWashingMachineRunning()) {
// Wenn die Maschine läuft, abbrechen oder Meldung ausgeben
echo "Die Waschmaschine ist bereits in Betrieb. Bitte warte, bis sie frei wird.";
exit;
}
// 5. Shelly-Relais einschalten (Waschmaschine einschalten)
if (switchShellyRelay('on')) {
// Zeitstempel merken (z. B. in einer DB, hier nur als Session-Beispiel)
session_start();
$_SESSION['washer_enabled_at'] = time();
echo "Waschmaschine wurde für " . RUNTIME_IN_HOURS . " Stunden freigeschaltet.";
} else {
echo "Fehler beim Einschalten der Waschmaschine.";
}
Alles anzeigen
<?php
require_once 'functions.php';
try {
$pdo = dbConnect();
if (!$pdo) {
// Falls keine Verbindung aufgebaut werden kann, einfach beenden
exit("Datenbankverbindung fehlgeschlagen.\n");
}
// 1) Alle aktiven Transaktionen suchen, bei denen das Ablaufdatum <= jetzt ist
$stmt = $pdo->query("
SELECT id, order_id
FROM transactions
WHERE is_active = 1
AND ends_at <= NOW()
");
$expiredTransactions = $stmt->fetchAll(PDO::FETCH_ASSOC);
// 2) Für jede abgelaufene Freischaltung: Waschmaschine ausschalten
foreach ($expiredTransactions as $tx) {
echo "Freischaltung abgelaufen für Order-ID: {$tx['order_id']}. Schalte Waschmaschine ab.\n";
// Shelly-Relais ausschalten
$success = switchShellyRelay('off');
if ($success) {
// 3) Transaktion in DB auf 'inaktiv' setzen
$updateStmt = $pdo->prepare("
UPDATE transactions
SET is_active = 0
WHERE id = :id
");
$updateStmt->execute([':id' => $tx['id']]);
echo "Waschmaschine erfolgreich deaktiviert.\n";
} else {
echo "Fehler: Konnte Shelly-Relais nicht ausschalten.\n";
}
}
// Optional: Wenn du mehrere Waschmaschinen oder mehrere Transaktionen parallel verwaltest,
// passt du diesen Vorgang entsprechend an.
// Hier haben wir nur ein Shelly-Relais (eine Waschmaschine) pro Transaktion angenommen.
} catch (Exception $e) {
// Fehlerbehandlung
echo "Fehler in cron.php: " . $e->getMessage() . "\n";
}
Alles anzeigen
Das ist der erste Streich, weitestgehend kommentiert.
Damit das funktional ist, muss dann noch eine Datenbank mitsamt Schema angelegt werden:
CREATE DATABASE IF NOT EXISTS `shelly_wama`
DEFAULT CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE `shelly_wama`;
CREATE TABLE IF NOT EXISTS `transactions` (
`id` INT AUTO_INCREMENT PRIMARY KEY,
`order_id` VARCHAR(255) NOT NULL,
`status` VARCHAR(50) NOT NULL,
`created_at` DATETIME NOT NULL,
`started_at` DATETIME DEFAULT NULL,
`ends_at` DATETIME DEFAULT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT '0',
INDEX `idx_active` (`is_active`),
INDEX `idx_order_id` (`order_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
Alles anzeigen
Damit sind die Grundvoraussetzungen geschaffen.
Was du nun benötigst, ist eine Möglichkeit für deine Kunden, dass diese die Zahlung initiieren können. Dazu könnte man einfach eine Seite erstellen, die für Kunden aus dem Internet heraus erreichbar ist:
<?php
require_once 'config.php';
?>
<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<title>Waschmaschine - Smart Payment</title>
</head>
<body>
<h1>Waschmaschine freischalten</h1>
<p>Preis pro Waschgang: 2,50 EUR</p>
<!-- PayPal Buttons integrieren (Achtung: Sandbox-Link vs. Live-Link) -->
<script src="https://www.paypal.com/sdk/js?client-id=<?php echo PAYPAL_CLIENT_ID; ?>¤cy=EUR"></script>
<div id="paypal-button-container"></div>
<script>
paypal.Buttons({
createOrder: function(data, actions) {
return actions.order.create({
purchase_units: [{
amount: {
value: '2.50' // Preis anpassen
}
}]
});
},
onApprove: function(data, actions) {
return actions.order.capture().then(function(details) {
// Nach erfolgreicher Zahlung an callback.php senden
window.location.href = 'callback.php?orderID=' + data.orderID;
});
}
}).render('#paypal-button-container');
</script>
</body>
</html>
Alles anzeigen
Was jetzt nur noch fehlt ist, dass die cron.php regelmäßig (z.B. alle 5 Minuten) ausgeführt wird:
*/5 * * * * /usr/bin/php /pfad/zur/cron.php
Das war's. Wie gesagt, ist das aus einer Vielzahl von Gründen ein Minimalbeispiel und soll vor allem veranschaulichen, wie es gehen könnte. Bevor man das produktiv nutzt, sollte es zum einen getestet- und zum anderen weiter abgesichert werden.
TL;DR: Rein über ein Shelly-Gerät wird das nicht funktionieren. Es braucht zwischen PayPal und dem Shelly noch etwas, was die Zahlungsverarbeitung durchführt, protokolliert, etc.
Tools wie Zapier oder Integromat (Make) könnten vielleicht auch dabei helfen, die PayPal-Transaktion mit einem Shelly-Switch oder einer anderen Steuerung zu verbinden, ohne dass zusätzlich etwas programmiert werden muss. Aber das weiß ich noch weniger.