Hallo in die Runde,
da ich auch so einen Antrieb habe und die Box, hatte ich das Script auch so übernommen.
Leider hing es hin und wieder bei mir, daher habe ich mich dran gesetzt und das nun gebaut.
Hoffe es ist selbst erklärend und gefällt euch, wenn ihr Fragen habt, melde Euch einfach bei mir.
// ===== TCP-Client Garagentor (ioBroker JavaScript) =====
// Robust: Reconnect + Backoff, KeepAlive, sauberes Line-Parsing (S;..., R;...),
// AutoClose nach X Minuten mit Rest-SECONDS-Datenpunkt, DP-Autoanlage,
// Licht-Status als boolean, shutdown-sicher (keine DB-closed-Fehler)
const net = require('net');
// ==== KONFIG ====
const deviceName = "Auffahrt";
const host = "xxx.xxx.x.xxx";
const port = 2785;
const SEND_NEWLINE = true;
// ==== BASIS-PFAD ====
const base = "0_userdata.0.Eigene_Datenpunkte.Garage.";
// ==== DATENPUNKTE ====
const dp_door = base + "Garage_Status_Tor"; // "open|closed|opening|closing"
const dp_light = base + "Garage_Status_Licht"; // boolean
const dp_cmd = base + "Garage_Command"; // "open|close|stop|lighton|lightoff"
const dp_connected = base + "Garage_TcpConnected"; // boolean
const dp_lastReply = base + "Garage_LastReply"; // string
const dp_lastError = base + "Garage_LastError"; // string
const dp_ac_enabled = base + "AutoClose_Enabled"; // boolean
const dp_ac_delay = base + "AutoClose_DelayMin"; // number (1..120)
const dp_ac_left_s = base + "AutoClose_RemainingSec"; // number (Sek., read-only)
const dp_lastOpened = base + "Garage_LastOpened"; // ISO-Zeit
const dp_lastClosed = base + "Garage_LastClosed"; // ISO-Zeit
// ---- Dein vorhandener Notify-DP (nicht anlegen, nur schreiben) ----
const dp_notify = "hm-rega.0.31016"; // bestehender Homematic-ReGa Datenpunkt/Variable
// ---- Notify-Logik: Frühwarnung & Hysterese ----
const EARLY_TRIGGER_SEC = 60; // ab so vielen Sekunden vor Auto-Close -> notify = true
const NOTIFY_FALSE_DELAY_MS = 5000; // Hysterese: erst nach 5s "closed" -> notify = false
let notifyFalseTimer = null;
// ==== DP-AUTOANLAGE (nur eigene 0_userdata.*) ====
function ensureState(id, defVal, common) {
if (!existsState(id)) {
createState(id, defVal, common, () => log(`State angelegt: ${id}`, 'info'));
}
}
ensureState(dp_door, '', { name:'Torstatus', type:'string', role:'text', read:true, write:false });
ensureState(dp_light, false, { name:'Lichtstatus', type:'boolean', role:'indicator.light', read:true, write:false });
ensureState(dp_cmd, '', { name:'Befehl', type:'string', role:'text', read:true, write:true });
ensureState(dp_connected, false, { name:'TCP verbunden', type:'boolean', role:'indicator.reachable', read:true, write:false });
ensureState(dp_lastReply, '', { name:'Letzte Antwort', type:'string', role:'text', read:true, write:false });
ensureState(dp_lastError, '', { name:'Letzter Fehler', type:'string', role:'text', read:true, write:false });
ensureState(dp_ac_enabled, true, { name:'AutoClose aktiv', type:'boolean', role:'switch', read:true, write:true });
ensureState(dp_ac_delay, 5, { name:'AutoClose Minuten',type:'number', role:'level', unit:'min', read:true, write:true });
ensureState(dp_ac_left_s, 0, { name:'Rest-Sekunden', type:'number', role:'value', unit:'s', read:true, write:false });
ensureState(dp_lastOpened, '', { name:'Letzte Öffnung', type:'string', role:'date', read:true, write:false });
ensureState(dp_lastClosed, '', { name:'Letzter Verschluss',type:'string', role:'date', read:true, write:false });
// ==== SHUTDOWN-SAFE ====
let shuttingDown = false;
function safeSetState(id, val, ack = true) {
if (shuttingDown) return;
try { setState(id, val, ack); } catch (_) {}
}
// ==== TCP ====
let client = null;
let reconnectTimer = null;
let reconnectDelayMs = 2000;
const reconnectDelayMaxMs = 30000;
let recvBuffer = '';
function startClient() {
clearTimeout(reconnectTimer);
client = new net.Socket();
client.setKeepAlive(true, 10000);
client.setEncoding('utf8');
client.connect(port, host, () => {
log(`[${deviceName}] Verbunden mit ${host}:${port}`, 'info');
safeSetState(dp_connected, true);
reconnectDelayMs = 2000;
});
client.on('data', (chunk) => {
recvBuffer += chunk;
const parts = recvBuffer.split(/\r?\n|\r/);
recvBuffer = parts.pop();
for (const raw of parts) {
const line = (raw || '').trim();
if (line) handleIncoming(line);
}
});
client.on('error', (err) => {
log(`[${deviceName}] TCP Fehler: ${err.message}`, 'warn');
safeSetState(dp_connected, false);
safeSetState(dp_lastError, err.message);
scheduleReconnect();
});
client.on('close', (hadError) => {
log(`[${deviceName}] TCP geschlossen${hadError ? ' (Fehler)' : ''}`, 'warn');
safeSetState(dp_connected, false);
scheduleReconnect();
});
}
function scheduleReconnect() {
try { if (client) client.destroy(); } catch (_) {}
client = null;
reconnectTimer = setTimeout(startClient, reconnectDelayMs);
reconnectDelayMs = Math.min(Math.round(reconnectDelayMs * 1.5), reconnectDelayMaxMs);
}
// ==== AUTO-CLOSE ====
let autoCloseTimer = null;
let autoCloseDeadline = 0;
let autoCloseTicker = null;
let acInternalTrigger = false;
function getAcEnabled() {
const v = getState(dp_ac_enabled)?.val;
return v === true || v === 1 || v === 'true';
}
function getAcDelayMin() {
let m = Number(getState(dp_ac_delay)?.val);
if (!isFinite(m) || m <= 0) m = 5;
return Math.max(1, Math.min(120, Math.floor(m)));
}
function scheduleAutoClose() {
if (!getAcEnabled()) { safeSetState(dp_ac_left_s, 0); return; }
const min = getAcDelayMin();
autoCloseDeadline = Date.now() + min * 60000;
clearTimeout(autoCloseTimer);
autoCloseTimer = setTimeout(performAutoClose, min * 60000);
startAutoCloseTicker();
updateRemainingSeconds();
log(`[${deviceName}] AutoClose geplant in ${min} Min`, 'info');
}
function performAutoClose() {
if (shuttingDown) return;
autoCloseTimer = null;
const st = getState(dp_door)?.val;
if (st === 'open') {
log(`[${deviceName}] AutoClose ausgelöst -> schließe`, 'info');
acInternalTrigger = true;
try { setState(dp_cmd, { val: 'close', ack: false }); } catch (_) {}
} else {
log(`[${deviceName}] AutoClose übersprungen (Status: ${st})`, 'info');
}
stopAutoCloseTicker();
safeSetState(dp_ac_left_s, 0);
autoCloseDeadline = 0;
// dp_notify bleibt bis "closed" true (Hysterese regelt das Zurücksetzen)
}
function cancelAutoClose(reason = '', suppressWrite = false) {
if (autoCloseTimer) clearTimeout(autoCloseTimer);
if (autoCloseTicker) clearInterval(autoCloseTicker);
autoCloseTimer = null;
autoCloseTicker = null;
autoCloseDeadline = 0;
if (!suppressWrite) safeSetState(dp_ac_left_s, 0);
if (reason) log(`[${deviceName}] AutoClose abgebrochen: ${reason}`, 'info');
}
function updateRemainingSeconds() {
if (!autoCloseDeadline || shuttingDown) return;
const secLeft = Math.max(0, Math.ceil((autoCloseDeadline - Date.now()) / 1000));
safeSetState(dp_ac_left_s, secLeft);
// Frühwarnung: ≤ EARLY_TRIGGER_SEC -> notify true
if (secLeft > 0 && secLeft <= EARLY_TRIGGER_SEC) {
clearTimeout(notifyFalseTimer);
safeSetState(dp_notify, true, /*ack*/ false); // bewusst ack=false, falls Triggers benötigt
}
}
function startAutoCloseTicker() {
if (autoCloseTicker) return;
autoCloseTicker = setInterval(updateRemainingSeconds, 1000);
}
function stopAutoCloseTicker() {
if (!autoCloseTicker) return;
clearInterval(autoCloseTicker);
autoCloseTicker = null;
}
// ==== EINGEHEND ====
function handleIncoming(line) {
const parts = line.split(';');
if (parts.length < 2) {
log(`[${deviceName}] Unbekanntes Paket: "${line}"`, 'warn');
return;
}
const prefix = parts[0];
if (prefix === 'S') {
const dev = parts[1];
const value = parts.slice(2).join(';');
if (dev !== deviceName) return;
switch (value) {
case 'open':
safeSetState(dp_door, 'open');
safeSetState(dp_lastOpened, new Date().toISOString());
clearTimeout(notifyFalseTimer);
safeSetState(dp_notify, true, /*ack*/ false); // sofort true
scheduleAutoClose();
break;
case 'opening':
safeSetState(dp_door, 'opening');
clearTimeout(notifyFalseTimer);
safeSetState(dp_notify, true, /*ack*/ false); // sofort true
// kein Cancel hier
break;
case 'closing':
safeSetState(dp_door, 'closing');
clearTimeout(notifyFalseTimer);
safeSetState(dp_notify, true, /*ack*/ false); // sofort true
cancelAutoClose('Status=closing');
break;
case 'closed':
safeSetState(dp_door, 'closed');
safeSetState(dp_lastClosed, new Date().toISOString());
cancelAutoClose('Status=closed');
// Hysterese: erst nach 5s false
clearTimeout(notifyFalseTimer);
notifyFalseTimer = setTimeout(() => {
safeSetState(dp_notify, false, /*ack*/ false);
}, NOTIFY_FALSE_DELAY_MS);
break;
case 'lightOn':
safeSetState(dp_light, true);
break;
case 'lightOff':
safeSetState(dp_light, false);
break;
default:
log(`[${deviceName}] Status: ${value}`, 'info');
break;
}
return;
}
if (prefix === 'R') {
const code = parts[1] || '';
safeSetState(dp_lastReply, code);
if (/ERR|UNKNOWN/i.test(code)) safeSetState(dp_lastError, code);
else log(`[${deviceName}] Antwort: ${code}`, 'info');
return;
}
log(`[${deviceName}] Unbekanntes Paket: "${line}"`, 'warn');
}
// ==== SENDEN ====
function sendMessage(msg) {
return new Promise(resolve => {
if (!client) { log(`[${deviceName}] Kein Client aktiv`, 'warn'); return resolve('Error'); }
try { client.write(SEND_NEWLINE ? (msg + '\n') : msg); resolve('OK'); }
catch (e) { log(`[${deviceName}] Sendefehler: ${e.message}`, 'error'); safeSetState(dp_lastError, e.message); resolve('Error'); }
});
}
// ==== BEFEHL-TRIGGER ====
on({ id: dp_cmd, change: 'ne' }, async obj => {
if (obj.state.ack) return;
const cmd = String(obj.state.val || '').toLowerCase().trim();
// Manuelle Befehle canceln AutoClose (interner Trigger nicht)
if (acInternalTrigger) {
log(`[${deviceName}] DP-Command (intern): ${cmd}`, 'info');
} else {
if (cmd === 'open' || cmd === 'close' || cmd === 'stop') {
cancelAutoClose(`Befehl: ${cmd}`);
}
}
let payload = null;
switch (cmd) {
case 'open': payload = `C;${deviceName};open`; break;
case 'close': payload = `C;${deviceName};close`; break;
case 'stop': payload = `C;${deviceName};stop`; break;
case 'lighton': payload = `C;${deviceName};lightOn`; break;
case 'lightoff': payload = `C;${deviceName};lightOff`; break;
default:
log(`[${deviceName}] Unbekannter Befehl: ${cmd}`, 'warn');
safeSetState(dp_lastError, `UNKNOWN_CMD:${cmd}`);
break;
}
if (payload) await sendMessage(payload);
safeSetState(dp_cmd, '', true);
if (acInternalTrigger) acInternalTrigger = false;
});
// ==== SETTINGS-ÄNDERUNGEN ====
on({ id: dp_ac_enabled, change: 'ne' }, () => {
const st = getState(dp_door)?.val;
if (getAcEnabled() && st === 'open') scheduleAutoClose();
else cancelAutoClose('AutoClose disabled');
});
on({ id: dp_ac_delay, change: 'ne' }, () => {
const st = getState(dp_door)?.val;
if (getAcEnabled() && st === 'open') scheduleAutoClose();
});
// ==== START/STOP ====
onStop(() => {
shuttingDown = true;
clearTimeout(reconnectTimer);
clearTimeout(notifyFalseTimer);
if (client) { try { client.destroy(); } catch (_) {} }
cancelAutoClose('Script stop', true);
}, 1000);
startClient();