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
define('PAYPAL_CLIENT_ID', 'DEINE_CLIENT_ID');
define('PAYPAL_SECRET', 'DEIN_SECRET');
define('PAYPAL_API_BASE', 'https://api-m.sandbox.paypal.com');
define('SHELLY_IP', '192.168.1.50');
define('SHELLY_RELAY', 0);
define('POWER_THRESHOLD', 10);
define('RUNTIME_IN_HOURS', 3);
define('DB_HOST', 'localhost');
define('DB_NAME', 'shelly_wama');
define('DB_USER', 'root');
define('DB_PASS', '');
Alles anzeigen
<?php
require_once 'config.php';
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;
}
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;
}
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;
}
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);
return $data['meters'][0]['power'] ?? 0.0;
}
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';
if (!isset($_GET['orderID'])) {
exit("Keine Order-ID vorhanden.");
}
$orderId = $_GET['orderID'];
$accessToken = getPayPalAccessToken();
if (!$accessToken) {
exit("Fehler beim Abrufen des PayPal Access Tokens.");
}
$orderDetails = getPayPalOrderDetails($orderId, $accessToken);
if (!$orderDetails || !isset($orderDetails['status'])) {
exit("Fehler beim Abrufen der Zahlungsdetails.");
}
if ($orderDetails['status'] !== 'COMPLETED') {
exit("Zahlung nicht abgeschlossen. Status: " . $orderDetails['status']);
}
saveTransaction($orderId, $orderDetails['status']);
if (isWashingMachineRunning()) {
echo "Die Waschmaschine ist bereits in Betrieb. Bitte warte, bis sie frei wird.";
exit;
}
if (switchShellyRelay('on')) {
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) {
exit("Datenbankverbindung fehlgeschlagen.\n");
}
$stmt = $pdo->query("
SELECT id, order_id
FROM transactions
WHERE is_active = 1
AND ends_at <= NOW()
");
$expiredTransactions = $stmt->fetchAll(PDO::FETCH_ASSOC);
foreach ($expiredTransactions as $tx) {
echo "Freischaltung abgelaufen für Order-ID: {$tx['order_id']}. Schalte Waschmaschine ab.\n";
$success = switchShellyRelay('off');
if ($success) {
$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";
}
}
} catch (Exception $e) {
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>
<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.