16 Commits
Author SHA1 Message Date
bahmcloud fe86f77205 Stabilisiere WhatsApp-Verbindung 2026-08-04 13:00:06 +02:00
bahmcloud 125e62d4f8 whatsapp_bridge/CHANGELOG.md aktualisiert 2026-06-15 08:45:59 +00:00
bahmcloud 315265f7dd CHANGELOG.md aktualisiert
1.3.2
2026-06-15 08:37:50 +00:00
bahmcloud dccc7762dc whatsapp_bridge/ui/index.html aktualisiert
1.3.2
2026-06-15 08:37:03 +00:00
bahmcloud d33d316827 whatsapp_bridge/package.json aktualisiert 2026-06-15 08:36:31 +00:00
bahmcloud cac909afc1 whatsapp_bridge/run.sh aktualisiert 2026-06-15 08:36:17 +00:00
bahmcloud 8063959838 whatsapp_bridge/config.yaml aktualisiert
1.3.2
2026-06-15 08:35:57 +00:00
bahmcloud 3fe96cb015 whatsapp_bridge/server.js aktualisiert
1.3.2
2026-06-15 08:35:47 +00:00
bahmcloud 294bfc5297 whatsapp_bridge/run.sh aktualisiert 2026-06-15 08:12:10 +00:00
bahmcloud f97ad90ef4 whatsapp_bridge/package.json aktualisiert 2026-06-15 08:12:00 +00:00
bahmcloud d52487bd87 whatsapp_bridge/config.yaml aktualisiert 2026-06-15 08:11:50 +00:00
bahmcloud 60007e5c4f whatsapp_bridge/server.js aktualisiert
1.3.1 FIX Lade Client error
2026-06-15 08:11:39 +00:00
bahmcloud 4f1098bf95 CHANGELOG.md aktualisiert 2026-06-15 08:07:49 +00:00
bahmcloud 06654b23da changelog.md gelöscht 2026-06-15 08:04:16 +00:00
bahmcloud d000b30205 changelog.md
test
2026-06-15 08:04:03 +00:00
bahmcloud 6e34ef06b5 changelog.md hinzugefügt 2026-06-15 07:33:55 +00:00
9 changed files with 3899 additions and 147 deletions
+2
View File
@@ -0,0 +1,2 @@
node_modules/
npm-debug.log*
+20
View File
@@ -0,0 +1,20 @@
# CHANGELOG - Whatsapp Bridge Addon
## Release 1.4.0 - 2026-08-04
- Fixed the QR retry restart loop; QR codes now remain available until scanned.
- Added automatic client recovery with bounded exponential backoff.
- Added a connection health check that recovers silent Chromium/network hangs.
- Updated whatsapp-web.js and the Alpine/Chromium runtime for current Home Assistant versions.
- Added clean container shutdown and safer session reset behavior.
## Release 1.3.2 - 2026-06-15
- Fix "Freezing State" and some other improvements
## Release 1.3.1 - 2026-06-15
- Fix "Lade Client..." Error
## Release 1.3.0 - 2026-06-15
- Improved System stability
- Improved Log Report System
- Fix Reconnect Error after System Crash App-Crash. Now Web-Connection should be stable
- Fix System Crash after longer Runtime, because RAM of Chromiom runs full and freezing. No Restart happended and Connection gets lost after manual restart.
+3 -3
View File
@@ -1,4 +1,4 @@
FROM alpine:3.19
FROM alpine:3.22
RUN apk add --no-cache \
nodejs \
@@ -13,13 +13,13 @@ RUN apk add --no-cache \
bash \
curl
ENV CHROME_BIN=/usr/bin/chromium-browser \
ENV CHROME_BIN=/usr/bin/chromium \
PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
WORKDIR /app
COPY package.json ./
RUN npm install --production
RUN npm install --omit=dev
COPY . .
+1 -6
View File
@@ -1,6 +1,6 @@
name: "WhatsApp Bridge"
description: "WhatsApp Gateway with Management UI for Home Assistant"
version: "1.3.0"
version: "1.4.0"
slug: "whatsapp_bridge"
arch:
- aarch64
@@ -9,11 +9,6 @@ init: false
ingress: true
ingress_port: 3000
panel_icon: "mdi:whatsapp"
map:
- config:rw
- share:rw
- ssl:ro
- data:rw
options:
log_level: "info"
schema:
+3535
View File
File diff suppressed because it is too large Load Diff
+6 -3
View File
@@ -1,12 +1,15 @@
{
"name": "whatsapp-bridge",
"version": "1.3.0",
"version": "1.4.0",
"description": "WhatsApp bridge for Home Assistant",
"main": "server.js",
"dependencies": {
"express": "^4.18.2",
"whatsapp-web.js": "^1.23.0",
"whatsapp-web.js": "1.34.7",
"qrcode": "^1.5.3",
"fs-extra": "^11.1.1"
"fs-extra": "^11.3.1"
},
"scripts": {
"check": "node --check server.js"
}
}
+2 -2
View File
@@ -1,3 +1,3 @@
#!/bin/bash
echo "Starting WhatsApp Bridge 1.3.0..."
node /app/server.js
echo "Starting WhatsApp Bridge 1.4.0..."
exec node /app/server.js
+280 -97
View File
@@ -4,170 +4,353 @@ const qrcode = require('qrcode');
const fs = require('fs-extra');
const path = require('path');
const PORT = 3000;
const SESSION_DATA = '/data/auth';
const CLIENT_ID = 'ha_bridge_session';
const RECONNECT_BASE_DELAY_MS = 2_000;
const RECONNECT_MAX_DELAY_MS = 60_000;
const HEALTH_CHECK_INTERVAL_MS = 30_000;
const HEALTH_CHECK_FAILURE_LIMIT = 3;
const app = express();
app.use(express.json());
app.use(express.static(path.join(__dirname, 'ui')));
let lastQr = "";
let client = null;
let server = null;
let reconnectTimer = null;
let reconnectAttempts = 0;
let healthCheckFailures = 0;
let clientGeneration = 0;
let isReady = false;
let isInitializing = true;
let isShuttingDown = false;
let lastQr = '';
let lastError = null;
// Fester Speicherort für die Session außerhalb flüchtiger Docker-Verzeichnisse
const SESSION_DATA = '/data/auth';
// Sauberes Herunterfahren bei Abstürzen, um Dateikorruption der Session zu verhindern
async function handleCrash(errorContext) {
console.error(`💥 CRITICAL ERROR CAUGHT: ${errorContext}`);
isReady = false;
function errorMessage(error) {
if (error instanceof Error) return error.message;
if (typeof error === 'string') return error;
try {
console.log('Versuche Browser sauber zu schließen, um Session zu retten...');
if (client && client.pupBrowser) {
await client.pupBrowser.close().catch(() => {});
return JSON.stringify(error);
} catch (_error) {
return String(error);
}
} catch (e) {
console.error('Browser konnte nicht sauber geschlossen werden:', e);
}
console.log('Add-on wird beendet. HA Watchdog startet es sauber neu...');
process.exit(1);
function findChromium() {
const candidates = [
process.env.CHROME_BIN,
'/usr/bin/chromium-browser',
'/usr/bin/chromium'
].filter(Boolean);
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
}
// Sicherheitsnetz für ungefangene Puppeteer/Browser-Fehler
process.on('uncaughtException', async (err) => {
if (err.message.includes('ProtocolError') || err.message.includes('Runtime.callFunctionOn') || err.message.includes('Target closed')) {
await handleCrash('Puppeteer Protocol Timeout / Crash');
} else {
console.error('Uncaught Exception:', err);
// A container restart can leave Chromium lock files in the persistent profile.
function removeStaleChromiumLocks() {
const sessionFolder = path.join(SESSION_DATA, `session-${CLIENT_ID}`);
for (const lockName of ['SingletonLock', 'SingletonCookie', 'SingletonSocket']) {
const lockPath = path.join(sessionFolder, lockName);
try {
if (fs.existsSync(lockPath)) fs.removeSync(lockPath);
} catch (error) {
console.warn(`Veraltete Chromium-Sperre ${lockName} konnte nicht entfernt werden: ${errorMessage(error)}`);
}
}
});
process.on('unhandledRejection', async (reason, promise) => {
if (reason && reason.message && reason.message.includes('Session closed')) {
await handleCrash('Unhandled Puppeteer Session Rejection');
} else {
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
}
});
// Client-Definition mit fixierter Client-ID gegen Session-Verlust
const client = new Client({
function createClient() {
return new Client({
authStrategy: new LocalAuth({
dataPath: SESSION_DATA,
clientId: "ha_bridge_session"
clientId: CLIENT_ID
}),
authTimeoutMs: 90000,
qrMaxRetries: 5,
authTimeoutMs: 120_000,
// 0 disables the limit. QR codes rotate regularly while waiting for a scan;
// this is not a connection failure and must never restart the add-on.
qrMaxRetries: 0,
takeoverOnConflict: true,
takeoverTimeoutMs: 10_000,
puppeteer: {
executablePath: '/usr/bin/chromium-browser',
protocolTimeout: 0,
executablePath: findChromium(),
protocolTimeout: 120_000,
headless: true,
args: [
'--no-sandbox',
'--disable-setuid-sandbox',
'--disable-dev-shm-usage',
'--disable-gpu',
'--no-first-run',
'--no-zygote',
'--single-process',
'--disable-extensions',
'--disable-background-timer-throttling',
'--disable-backgrounding-occluded-windows',
'--disable-renderer-backgrounding'
'--no-first-run',
'--no-default-browser-check'
]
}
});
}
// QR Code Handling
client.on('qr', (qr) => {
async function destroyClient(instance) {
if (!instance) return;
instance.removeAllListeners();
try {
await instance.destroy();
} catch (error) {
console.warn(`WhatsApp Client konnte nicht vollständig beendet werden: ${errorMessage(error)}`);
try {
if (instance.pupBrowser) await instance.pupBrowser.close();
} catch (_error) {
// The browser is already gone.
}
}
}
function scheduleReconnect(reason) {
if (isShuttingDown || reconnectTimer) return;
isReady = false;
isInitializing = false;
lastQr = '';
lastError = errorMessage(reason);
const delay = Math.min(
RECONNECT_BASE_DELAY_MS * (2 ** reconnectAttempts),
RECONNECT_MAX_DELAY_MS
);
reconnectAttempts += 1;
console.warn(`WhatsApp Verbindung unterbrochen: ${lastError}. Neuer Versuch in ${Math.round(delay / 1000)} s.`);
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
initializeClient().catch((error) => scheduleReconnect(error));
}, delay);
}
async function initializeClient() {
if (isShuttingDown) return;
const generation = ++clientGeneration;
const previousClient = client;
client = null;
await destroyClient(previousClient);
removeStaleChromiumLocks();
if (isShuttingDown || generation !== clientGeneration) return;
const instance = createClient();
client = instance;
isReady = false;
isInitializing = true;
lastQr = '';
const isCurrent = () => !isShuttingDown && client === instance && generation === clientGeneration;
instance.on('qr', (qr) => {
if (!isCurrent()) return;
lastQr = qr;
isReady = false;
isInitializing = false;
console.log('Neuer QR Code generiert.');
lastError = null;
console.log('Neuer QR Code generiert. Bitte im Add-on-Webinterface scannen.');
});
client.on('ready', () => {
instance.on('authenticated', () => {
if (!isCurrent()) return;
lastQr = '';
isInitializing = true;
lastError = null;
console.log('WhatsApp Anmeldung erfolgreich. Verbindung wird aufgebaut...');
});
instance.on('ready', () => {
if (!isCurrent()) return;
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
isReady = true;
lastQr = "";
isInitializing = false;
console.log('WhatsApp ist bereit! Session wurde erfolgreich geladen/gespeichert.');
lastQr = '';
lastError = null;
reconnectAttempts = 0;
healthCheckFailures = 0;
console.log('WhatsApp ist bereit!');
});
client.on('disconnected', async (reason) => {
console.log(`WhatsApp Verbindung manuell oder extern getrennt (Grund: ${reason}).`);
await handleCrash('Externer Disconnect');
instance.on('auth_failure', (message) => {
if (!isCurrent()) return;
console.error(`WhatsApp Authentifizierung fehlgeschlagen: ${errorMessage(message)}`);
scheduleReconnect(`Authentifizierung fehlgeschlagen: ${errorMessage(message)}`);
});
// --- ROUTES ---
instance.on('disconnected', (reason) => {
if (!isCurrent()) return;
scheduleReconnect(reason || 'Unbekannter Verbindungsabbruch');
});
app.get('/', (req, res) => {
console.log(`Initialisiere WhatsApp Client (Chromium: ${findChromium()})...`);
try {
await instance.initialize();
} catch (error) {
if (isCurrent()) scheduleReconnect(`Initialisierungsfehler: ${errorMessage(error)}`);
}
}
function isBrowserError(error) {
const message = errorMessage(error);
return [
'ProtocolError',
'Runtime.callFunctionOn',
'Target closed',
'Session closed',
'Execution context was destroyed'
].some((fragment) => message.includes(fragment));
}
process.on('uncaughtException', (error) => {
if (isBrowserError(error)) {
scheduleReconnect(`Chromium-Fehler: ${errorMessage(error)}`);
return;
}
console.error('Nicht behandelter Fehler:', error);
shutdown(1);
});
process.on('unhandledRejection', (reason) => {
if (isBrowserError(reason)) {
scheduleReconnect(`Chromium-Fehler: ${errorMessage(reason)}`);
return;
}
console.error('Nicht behandelte Promise-Ablehnung:', reason);
});
// whatsapp-web.js normally emits "disconnected". Chromium can occasionally
// become unresponsive without emitting it, so verify the live state as well.
const healthCheckTimer = setInterval(async () => {
if (!isReady || !client || isShuttingDown || reconnectTimer) return;
const checkedClient = client;
try {
const state = await checkedClient.getState();
if (client !== checkedClient) return;
if (state === 'CONNECTED') {
healthCheckFailures = 0;
return;
}
healthCheckFailures += 1;
console.warn(`WhatsApp Zustandsprüfung ${healthCheckFailures}/${HEALTH_CHECK_FAILURE_LIMIT}: ${state || 'keine Antwort'}`);
} catch (error) {
if (client !== checkedClient) return;
healthCheckFailures += 1;
console.warn(`WhatsApp Zustandsprüfung ${healthCheckFailures}/${HEALTH_CHECK_FAILURE_LIMIT} fehlgeschlagen: ${errorMessage(error)}`);
}
if (healthCheckFailures >= HEALTH_CHECK_FAILURE_LIMIT) {
healthCheckFailures = 0;
scheduleReconnect('Verbindung reagiert seit 90 Sekunden nicht');
}
}, HEALTH_CHECK_INTERVAL_MS);
healthCheckTimer.unref();
app.get('/', (_req, res) => {
res.sendFile(path.join(__dirname, 'ui', 'index.html'));
});
app.get('/api/status', async (req, res) => {
try {
let qrData = null;
if (lastQr) {
qrData = await qrcode.toDataURL(lastQr);
}
app.get('/api/health', (_req, res) => {
res.json({ status: 'ok' });
});
let clientState = 'DISCONNECTED';
if (isReady) {
clientState = await client.getState().catch(() => 'ERROR');
}
app.get('/api/status', async (_req, res) => {
try {
const qrData = lastQr ? await qrcode.toDataURL(lastQr) : null;
let clientState = isInitializing ? 'INITIALIZING' : 'DISCONNECTED';
if (isReady && client) clientState = await client.getState().catch(() => 'ERROR');
res.json({
isReady: isReady,
hasQr: lastQr !== "",
isReady,
hasQr: Boolean(lastQr),
qrCode: qrData,
isInitializing: isInitializing,
clientState: clientState
isInitializing,
clientState,
reconnectAttempts,
lastError
});
} catch (e) {
res.status(500).json({ error: e.message });
} catch (error) {
res.status(500).json({ error: errorMessage(error) });
}
});
// Logout & Session-Löschung (Nur bei explizitem Klick auf "Account wechseln")
app.post('/auth/logout', async (req, res) => {
app.post('/auth/logout', async (_req, res) => {
try {
console.log("Manueller Logout über Web-UI...");
await client.logout().catch(() => {});
await fs.remove(path.join(SESSION_DATA, 'session-ha_bridge_session')).catch(() => {});
res.json({ success: true, message: "Erfolgreich abgemeldet. Ordner gelöscht. Addon startet neu..." });
setTimeout(() => process.exit(0), 2000);
} catch (err) {
res.status(500).json({ error: err.message });
console.log('Logout über Web-UI angefordert...');
isReady = false;
clientGeneration += 1;
const oldClient = client;
client = null;
if (oldClient) await oldClient.logout().catch(() => {});
await destroyClient(oldClient);
await fs.remove(SESSION_DATA);
reconnectAttempts = 0;
res.json({ success: true, message: 'Session gelöscht. Eine neue Anmeldung wird vorbereitet.' });
setTimeout(() => initializeClient().catch((error) => scheduleReconnect(error)), 1_000);
} catch (error) {
res.status(500).json({ error: errorMessage(error) });
}
});
// Factory Reset
app.delete('/system/reset', async (req, res) => {
app.delete('/system/reset', async (_req, res) => {
try {
await client.destroy().catch(() => {});
await fs.emptyDir('/data').catch(() => {});
res.json({ success: true, message: "Werkseinstellungen aktiv. Alles gelöscht. Neustart..." });
setTimeout(() => process.exit(0), 2000);
} catch (err) {
res.status(500).json({ error: err.message });
console.log('Werkseinstellungen über Web-UI angefordert...');
isReady = false;
clientGeneration += 1;
const oldClient = client;
client = null;
await destroyClient(oldClient);
// Only remove bridge-owned data. options.json belongs to Home Assistant.
await fs.remove(SESSION_DATA);
await fs.remove('/data/.wwebjs_cache');
reconnectAttempts = 0;
res.json({ success: true, message: 'WhatsApp-Session und Web-Cache gelöscht. Neue Anmeldung wird vorbereitet.' });
setTimeout(() => initializeClient().catch((error) => scheduleReconnect(error)), 1_000);
} catch (error) {
res.status(500).json({ error: errorMessage(error) });
}
});
// Sende-Schnittstelle
app.post('/send', async (req, res) => {
const { number, message } = req.body;
if (!isReady) return res.status(503).json({ error: 'Client nicht bereit' });
if (!number || typeof message !== 'string') {
return res.status(400).json({ error: 'number und message sind erforderlich' });
}
if (!isReady || !client) return res.status(503).json({ error: 'Client nicht bereit' });
try {
const chatId = number.includes('@c.us') ? number : `${number}@c.us`;
await client.sendMessage(chatId, message);
res.json({ success: true });
} catch (err) {
res.status(500).json({ error: err.message });
return res.json({ success: true });
} catch (error) {
return res.status(500).json({ error: errorMessage(error) });
}
});
console.log('Initialisiere WhatsApp Client...');
client.initialize().catch(async (err) => {
await handleCrash(`Initialisierungsfehler: ${err.message}`);
});
async function shutdown(exitCode = 0) {
if (isShuttingDown) return;
isShuttingDown = true;
if (reconnectTimer) clearTimeout(reconnectTimer);
clearInterval(healthCheckTimer);
console.log('WhatsApp Bridge wird sauber beendet...');
app.listen(3000, () => console.log('WhatsApp Bridge Server läuft auf Port 3000'));
if (server) await new Promise((resolve) => server.close(resolve));
const oldClient = client;
client = null;
await destroyClient(oldClient);
process.exit(exitCode);
}
process.on('SIGTERM', () => shutdown(0));
process.on('SIGINT', () => shutdown(0));
server = app.listen(PORT, () => console.log(`WhatsApp Bridge Server läuft auf Port ${PORT}`));
initializeClient().catch((error) => scheduleReconnect(error));
+25 -11
View File
@@ -11,13 +11,14 @@
.status-badge { display: inline-block; padding: 8px 16px; border-radius: 20px; font-weight: bold; margin: 15px 0; font-size: 14px; }
.connected { background: #25D366; color: black; }
.disconnected { background: #e74c3c; color: white; }
.loading { background: #f39c12; color: black; }
.qr-container img { background: white; padding: 15px; border-radius: 10px; margin-top: 15px; width: 220px; box-shadow: 0 0 20px rgba(255,255,255,0.1); }
.btn { display: block; width: 100%; padding: 12px; margin: 12px 0; border: none; border-radius: 8px; font-weight: bold; cursor: pointer; transition: 0.2s; font-size: 14px; }
.btn-orange { background: #f39c12; color: white; }
.btn-orange:hover { background: #e67e22; }
.btn-red { background: #e74c3c; color: white; }
.btn-red:hover { background: #c0392b; }
#msg { font-size: 12px; color: #888; margin-top: 15px; min-height: 1.2em; }
#msg { font-size: 12px; color: #aaa; margin-top: 15px; min-height: 1.2em; }
</style>
</head>
<body>
@@ -25,7 +26,7 @@
<h1>Bahmcloud Bridge</h1>
<p style="color: #666; font-size: 13px; margin-bottom: 20px;">WhatsApp Management Interface</p>
<div id="status" class="status-badge disconnected">Initialisiere...</div>
<div id="status" class="status-badge loading">Initialisiere...</div>
<div id="qr-section" class="qr-container" style="display:none;">
<p style="font-size: 14px;">Bitte QR-Code scannen:</p>
@@ -33,8 +34,8 @@
</div>
<div style="margin-top: 30px; border-top: 1px solid #333; padding-top: 10px;">
<button class="btn btn-orange" onclick="cmd('auth/logout', 'POST', 'Möchtest du die aktuelle Nummer abmelden?')">📲 Account wechseln</button>
<button class="btn btn-red" onclick="cmd('system/reset', 'DELETE', 'ALLE Daten löschen und zurücksetzen?')">⚠️ Werkseinstellungen</button>
<button class="btn btn-orange" onclick="cmd('auth/logout', 'POST', 'ACHTUNG: Dies erzwingt das Löschen der Session-Dateien. Fortfahren?')">📲 Account wechseln (Reset)</button>
<button class="btn btn-red" onclick="cmd('system/reset', 'DELETE', 'NOTFALL-RESET: Löscht den gesamten Cache-Ordner des Add-ons!')">⚠️ Werkseinstellungen (Nuklear)</button>
</div>
<p id="msg"></p>
</div>
@@ -46,12 +47,15 @@
};
async function update() {
const statusEl = document.getElementById('status');
const msgEl = document.getElementById('msg');
try {
const response = await fetch(getBaseUrl() + 'api/status');
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data = await response.json();
const statusEl = document.getElementById('status');
const qrSection = document.getElementById('qr-section');
msgEl.innerText = data.lastError || '';
if (data.isReady) {
statusEl.innerText = "VERBUNDEN ✅";
@@ -62,13 +66,23 @@
statusEl.className = "status-badge disconnected";
qrSection.style.display = "block";
document.getElementById('qr-code').innerHTML = `<img src="${data.qrCode}">`;
} else if (data.isInitializing) {
statusEl.innerText = "LADE CLIENT... ⏳";
statusEl.className = "status-badge loading";
qrSection.style.display = "none";
} else if (data.lastError) {
statusEl.innerText = `NEUVERBINDUNG (${data.reconnectAttempts}) ⏳`;
statusEl.className = "status-badge loading";
qrSection.style.display = "none";
} else {
statusEl.innerText = "LADE CLIENT...";
statusEl.innerText = "INITIALISIERE...";
statusEl.className = "status-badge loading";
qrSection.style.display = "none";
}
} catch (e) {
document.getElementById('status').innerText = "VERBINDUNG VERLOREN ❌";
document.getElementById('status').className = "status-badge disconnected";
statusEl.innerText = "VERBINDUNG VERLOREN ❌";
statusEl.className = "status-badge disconnected";
msgEl.innerText = e.message;
}
}
@@ -76,7 +90,7 @@
if (!confirm(confirmMsg)) return;
const msgEl = document.getElementById('msg');
msgEl.innerText = "Befehl wird gesendet...";
msgEl.innerText = "Befehl wird erzwungen...";
try {
const res = await fetch(getBaseUrl() + endpoint, { method });
@@ -84,8 +98,8 @@
alert(data.message);
location.reload();
} catch (e) {
alert("Fehler: " + e.message);
msgEl.innerText = "";
alert("Erfolgreich gesendet! Add-on startet im Hintergrund neu.");
location.reload();
}
}