From 7720ae1d585afb59a43d96144ddf8ee67c5e511a Mon Sep 17 00:00:00 2001 From: c1328 <112856305+c1328@users.noreply.github.com> Date: Thu, 29 Jan 2026 22:53:42 +0100 Subject: [PATCH] Refactor Meshtastic ioBroker integration script --- meshcli_iobroker.js | 675 +++++++++++++++++++------------------------- 1 file changed, 293 insertions(+), 382 deletions(-) diff --git a/meshcli_iobroker.js b/meshcli_iobroker.js index cf440af..75575c4 100644 --- a/meshcli_iobroker.js +++ b/meshcli_iobroker.js @@ -1,202 +1,279 @@ -// this script/idea is based on https://forum.iobroker.net/topic/73326/meshtastic/2 +// Meshtastic ioBroker Integration Kit +// Idea based on https://forum.iobroker.net/topic/73326/meshtastic/2 -// Meshtastic ioBroker Integration Kit (Full Script) +// ====================================================== +// CONFIG +// ====================================================== -// Configuration of IP adress of meshtastic node that is connected via TCP -var deviceIp = ''; -// Configuration of MQTT instance that is used (in my case 3rd instance) -var mqttPath = 'mqtt.3.msh.*.json.*'; - -var chats = [ - { name: 'Default', id: 0 }, - { name: '', id: 1 }, - { name: '', id: 2 } +// IP of your Meshtastic node +const deviceIp = "192.168.1.x"; + +// MQTT path variable (note the 3 because I use the 3rd instance of mqtt at IoBroker) +const mqttPath = /^mqtt\.3\.msh\..*\.json\..*$/; + +// configure the channels of your node +const chats = [ + { name: "Default", id: 0 }, + { name: "", id: 1 }, + { name: "", id: 2 } ]; +// Base paths (usualy no adjustment needed) +const BASE = "0_userdata.0.Meshtastic"; +const NODES = BASE + ".Nodes"; +const CHATS = BASE + ".Chats"; + +// ====================================================== +// HELPERS +// ====================================================== +function safeCreateObject(id, obj) { + if (!existsObject(id)) setObject(id, obj); +} + +function safeCreateState(id, common) { + if (!existsObject(id)) { + setObject(id, { type: "state", common: common, native: {} }); + } +} + +function safeSetState(id, val) { + if (existsObject(id)) setState(id, val, true); +} + +function shellSafe(msg) { + if (!msg) return ""; + return msg.replace(/'/g, "").replace(/"/g, '\\"'); +} + +function parseNum(val) { + if (!val || val === "N/A" || val === "Powered") return 0; + return parseFloat(String(val).replace(/[^\d.-]/g, "")) || 0; +} // ====================================================== // 1. MQTT TRIGGER (Realtime Chat + Name Resolution) // ====================================================== -on({id: /^mqtt\.3\.msh\..*\.json\..*$/, change: "any"}, function (obj) { +on({ id: mqttPath, change: "any" }, function (obj) { try { if (!obj.state.val) return; const msg = JSON.parse(obj.state.val); - // Basis-Daten extrahieren const channelIdx = parseInt(msg.channel) || 0; - - // ID generieren: Dezimal zu Hex, Kleinbuchstaben, ohne !, aufgefüllt auf 8 Stellen - let senderHex = parseInt(msg.from).toString(16).toLowerCase().replace('!', '').padStart(8, '0'); - const nodeBasePath = '0_userdata.0.Meshtastic.Nodes.' + senderHex; - // --- Namensauflösung --- + // sender hex + let senderHex = parseInt(msg.from) + .toString(16) + .toLowerCase() + .replace("!", "") + .padStart(8, "0"); + + const nodeBasePath = `${NODES}.${senderHex}`; + + // Name resolution let displayName = senderHex; - if (existsState(nodeBasePath + '.info.alias')) { - let aliasVal = getState(nodeBasePath + '.info.alias').val; - if (aliasVal && aliasVal !== 'N/A' && aliasVal !== '') displayName = aliasVal; - } else if (existsState(nodeBasePath + '.info.user')) { - let userVal = getState(nodeBasePath + '.info.user').val; - if (userVal && userVal !== 'N/A' && userVal !== '') displayName = userVal; + + if (existsState(nodeBasePath + ".info.alias")) { + let aliasVal = getState(nodeBasePath + ".info.alias").val; + if (aliasVal && aliasVal !== "N/A") displayName = aliasVal; + } else if (existsState(nodeBasePath + ".info.user")) { + let userVal = getState(nodeBasePath + ".info.user").val; + if (userVal && userVal !== "N/A") displayName = userVal; } - // --- FALL 1: TEXTNACHRICHTEN --- - if (msg.type === "text" && msg.payload && msg.payload.text) { + // --- TEXT MESSAGE --- + if (msg.type === "text" && msg.payload?.text) { const text = msg.payload.text; - const nodeMsgPath = nodeBasePath + '.info.lastMessage'; - // In Node Info speichern - if (existsObject(nodeBasePath)) { - if (!existsState(nodeMsgPath)) { - setObject(nodeMsgPath, { - type: 'state', - common: { name: 'Letzte Nachricht', type: 'string', role: 'text', read: true, write: false }, - native: {} - }); - } - setState(nodeMsgPath, text, true); - } + // Node lastMessage + safeCreateState(nodeBasePath + ".info.lastMessage", { + name: "Letzte Nachricht", + type: "string", + role: "text", + read: true, + write: false + }); + safeSetState(nodeBasePath + ".info.lastMessage", text); - // In Chat & Historie speichern - const chatPath = '0_userdata.0.Meshtastic.Chats.' + channelIdx; + // Chat lastMessage + history + const chatPath = `${CHATS}.${channelIdx}`; if (existsObject(chatPath)) { - setState(chatPath + '.lastMessage', `${displayName}: ${text}`, true); + safeSetState(chatPath + ".lastMessage", `${displayName}: ${text}`); addToHistory(channelIdx, displayName, text); - } else { - log(`Chat-Kanal ${channelIdx} nicht gefunden unter ${chatPath}`, "warn"); } - log(`Meshtastic Text: [Kanal ${channelIdx}] ${displayName}: ${text}`); + + log(`Meshtastic Text: [${channelIdx}] ${displayName}: ${text}`); + } - // --- FALL 2: POSITIONSDATEN --- + // --- POSITION --- else if (msg.type === "position" && msg.payload) { const p = msg.payload; - const infoPath = nodeBasePath + '.info.'; if (p.latitude_i && p.longitude_i) { - // Umrechnung von Meshtastic Integer zu Float const lat = p.latitude_i / 10000000; const lon = p.longitude_i / 10000000; const alt = p.altitude || 0; - if (existsObject(nodeBasePath)) { - // Einzelne Datenpunkte setzen - setState(infoPath + 'latitude', lat, true); - setState(infoPath + 'longitude', lon, true); - setState(infoPath + 'altitude', alt, true); - - // Kombiniertes Location-Feld für Jarvis/Maps (lat,lon) - setState(infoPath + 'location', `${lat},${lon}`, true); + safeSetState(nodeBasePath + ".info.latitude", lat); + safeSetState(nodeBasePath + ".info.longitude", lon); + safeSetState(nodeBasePath + ".info.altitude", alt); + safeSetState(nodeBasePath + ".info.location", `${lat},${lon}`); - log(`Meshtastic Position: ${displayName} ist bei ${lat}, ${lon}`); - } + log(`Meshtastic Position: ${displayName} @ ${lat},${lon}`); } } + } catch (e) { log("Fehler im MQTT Trigger: " + e, "error"); } }); +// ====================================================== +// 2. SEND MESSAGE TO CHAT CHANNELS +// ====================================================== +on({ id: new RegExp("^" + CHATS.replace(/\./g, "\\.") + "\\.\\d+\\.sendMessage$"), change: "any" }, +function (obj) { -// Trigger für Nachrichten an Kanäle (Chats) -on({id: /^0_userdata\.0\.Meshtastic\.Chats\.\d+\.sendMessage$/, change: "any"}, function (obj) { + if (obj.state.ack) return; const msg = obj.state.val; - if (!msg || msg === "" || obj.state.ack === true) return; + if (!msg) return; - const parts = obj.id.split('.'); - const channelId = parseInt(parts[parts.length - 2]); - - // Log-Fix: Falls obj.from undefined ist, nutzen wir einen Fallback - const source = obj.from || "Skript/System"; - - // Sonderzeichen-Schutz: Wir entfernen einfache Anführungszeichen aus der Nachricht, - // um den Shell-Befehl nicht zu brechen. - const safeMsg = msg.replace(/'/g, ""); + const parts = obj.id.split("."); + const channelId = parseInt(parts[parts.length - 2]); - log(`Meshtastic: Sende an Kanal ${channelId} (von ${source}): ${safeMsg}`); + const safeMsg = shellSafe(msg); - // Wir umschließen die Nachricht in EINFACHE Anführungszeichen (') für die Shell - const command = `/home/iobroker/.local/bin/meshtastic --host ${deviceIp} --ch-index ${channelId} --sendtext '${safeMsg}'`; + log(`Meshtastic: Send Chat ${channelId}: ${safeMsg}`); - exec(command, function (error, stdout, stderr) { - if (error) { - log(`Fehler beim Senden (Kanal ${channelId}): ${stderr || error}`, 'error'); - } else { - // Feld leeren nach Erfolg - setTimeout(() => setState(obj.id, "", true), 500); + exec(`/home/iobroker/.local/bin/meshtastic --host ${deviceIp} --ch-index ${channelId} --sendtext '${safeMsg}'`, + function (error, stdout, stderr) { + if (error) log("Send error: " + (stderr || error), "error"); + else setTimeout(() => setState(obj.id, "", true), 300); } - }); + ); }); -// 2. Trigger für Direktnachrichten (Nodes) -on({id: /^0_userdata\.0\.Meshtastic\.Nodes\..*\.command\.sendMessage$/, change: "any", ack: false}, function (obj) { - const msg = obj.state.val; - if (!msg || msg === "") return; - - const parts = obj.id.split('.'); - const nodeId = parts[parts.length - 3]; - const safeMsg = msg.replace(/"/g, '\\"'); - - log(`Meshtastic: Sende Direktnachricht an !${nodeId}: ${safeMsg}`); - - exec(`/home/iobroker/.local/bin/meshtastic --host ${deviceIp} --dest "!${nodeId}" --sendtext "${safeMsg}"`, function (error) { - if (!error) { - setTimeout(() => setState(obj.id, "", true), 500); - } - }); -}); - - // ====================================================== -// 2. CLI Node Polling + Parsing +// 3. SEND DIRECT MESSAGE TO NODE +// ====================================================== +on({ id: new RegExp("^" + NODES.replace(/\./g, "\\.") + "\\..*\\.command\\.sendMessage$"), change: "any", ack: false }, +function (obj) { + + const msg = obj.state.val; + if (!msg) return; + + const nodeId = obj.id.split(".")[4]; + const safeMsg = shellSafe(msg); + + log(`Meshtastic: Direct to !${nodeId}: ${safeMsg}`); + + exec(`/home/iobroker/.local/bin/meshtastic --host ${deviceIp} --dest "!${nodeId}" --sendtext "${safeMsg}"`, + function (error) { + if (!error) setTimeout(() => setState(obj.id, "", true), 300); + } + ); +}); + +// ====================================================== +// 4. NODE COMMAND BUTTONS (Ping/Traceroute/Telemetry/Location) +// ====================================================== +function cliAction(target, cmd) { + exec(`/home/iobroker/.local/bin/meshtastic --host ${deviceIp} ${cmd} --dest "!${target}"`); +} + +// Ping +on({ id: new RegExp("^" + NODES.replace(/\./g, "\\.") + "\\..*\\.command\\.sendPing$"), change: "any" }, +obj => { + if (obj.state.ack || !obj.state.val) return; + const nodeId = obj.id.split(".")[4]; + cliAction(nodeId, "--sendping"); + setState(obj.id, false, true); +}); + +// Traceroute +on({ id: new RegExp("^" + NODES.replace(/\./g, "\\.") + "\\..*\\.command\\.sendTraceRoute$"), change: "any" }, +obj => { + if (obj.state.ack || !obj.state.val) return; + const nodeId = obj.id.split(".")[4]; + cliAction(nodeId, "--traceroute"); + setState(obj.id, false, true); +}); + +// Telemetry +on({ id: new RegExp("^" + NODES.replace(/\./g, "\\.") + "\\..*\\.command\\.getTelemetry$"), change: "any" }, +obj => { + if (obj.state.ack || !obj.state.val) return; + const nodeId = obj.id.split(".")[4]; + cliAction(nodeId, "--request-telemetry"); + setState(obj.id, false, true); +}); + +// Location request +on({ id: new RegExp("^" + NODES.replace(/\./g, "\\.") + "\\..*\\.command\\.getLocation$"), change: "any" }, +obj => { + if (obj.state.ack || !obj.state.val) return; + const nodeId = obj.id.split(".")[4]; + cliAction(nodeId, "--request-position"); + setState(obj.id, false, true); +}); + +// ====================================================== +// 5. NODE POLLING (CLI --nodes) // ====================================================== function updateNodes() { - exec('/home/iobroker/.local/bin/meshtastic --host ' + deviceIp + ' --nodes', function (error, result, stderr) { - if (result && result.includes('Connected to radio')) { - var nodes = parseData(result); + exec(`/home/iobroker/.local/bin/meshtastic --host ${deviceIp} --nodes`, + function (error, result) { + if (!result || !result.includes("Connected")) return; + const nodes = parseData(result); handleNodes(nodes); } - }); + ); } function handleNodes(nodes) { nodes.forEach(node => { node.ID = node.ID.replace("!", ""); - if (nodeIsKnown(node.ID)) { - updateNode(node); - } else { + if (!nodeIsKnown(node.ID)) { createNode(node); - setTimeout(() => updateNode(node), 4000); + setTimeout(() => updateNode(node), 2000); + } else { + updateNode(node); } }); } function nodeIsKnown(id) { - return !!getObject('0_userdata.0.Meshtastic.Nodes.' + id); + return !!getObject(`${NODES}.${id}`); } function parseData(data) { - const lines = data.trim().split('\n'); - const headerIndex = lines.findIndex(l => l.includes('│') && l.includes('ID')); + const lines = data.trim().split("\n"); + const headerIndex = lines.findIndex(l => l.includes("│") && l.includes("ID")); if (headerIndex === -1) return []; - const keys = lines[headerIndex].split('│').map(k => k.trim()).filter((k, i, arr) => i > 0 && i < arr.length - 1); - return lines.filter(l => l.includes('│') && !l.includes('═') && !l.includes('─') && !l.includes(' User ')) + + const keys = lines[headerIndex] + .split("│") + .map(k => k.trim()) + .filter((k, i, arr) => i > 0 && i < arr.length - 1); + + return lines + .filter(l => l.includes("│") && !l.includes("═") && !l.includes("─") && !l.includes(" User ")) .map(line => { - let values = line.split('│').map(v => v.trim()).slice(1, -1); + let values = line.split("│").map(v => v.trim()).slice(1, -1); if (values.length < keys.length) return null; let obj = {}; - keys.forEach((key, index) => { obj[key] = values[index] || "N/A"; }); + keys.forEach((key, i) => obj[key] = values[i] || "N/A"); return obj; - }).filter(obj => obj !== null); + }) + .filter(x => x); } // ====================================================== -// 3. Node Creation + Info States + Command States +// 6. NODE CREATION + STATES // ====================================================== function createNode(data) { - log("creating new node " + data.User, "info"); - - setObject('0_userdata.0.Meshtastic.Nodes.' + data.ID, { - type: 'channel', + safeCreateObject(`${NODES}.${data.ID}`, { + type: "channel", common: { name: data.User }, native: {} }); @@ -206,335 +283,169 @@ function createNode(data) { function createNodeStates(id) { - // Info Channel - setObject('0_userdata.0.Meshtastic.Nodes.' + id + '.info', { - type: 'channel', - common: { name: 'Info' }, + safeCreateObject(`${NODES}.${id}.info`, { + type: "channel", + common: { name: "Info" }, native: {} }); - // Info States - var nodeInfoStates = [ - {id: 'id', name: 'NodeID', type: 'string'}, - {id: 'user', name: 'User', type: 'string'}, - {id: 'alias', name: 'Alias', type: 'string'}, - {id: 'location', name: 'Location', type: 'string', role: 'value.gps'}, // for JARVIS map - {id: 'latitude', name: 'Latitude', type: 'number', role: 'value.gps.latitude'}, - {id: 'longitude', name: 'Longitude', type: 'number', role: 'value.gps.longitude'}, - {id: 'altitude', name: 'Altitude', type: 'number', unit: 'm'}, - {id: 'chanUtil', name: 'Channel util.', type: 'number', unit: '%'}, - {id: 'txAir', name: 'Tx air util.', type: 'number', unit: '%'}, - {id: 'snr', name: 'SNR', type: 'number', unit: 'dB'}, - {id: 'channel', name: 'Channel', type: 'string'}, - {id: 'lastHeard', name: 'Last heard', type: 'string'}, - {id: 'battery', name: 'Battery', type: 'number', unit: '%'}, - {id: 'lastMessage', name: 'Letzte Nachricht', type: 'string'} - ]; + safeCreateObject(`${NODES}.${id}.command`, { + type: "channel", + common: { name: "Command" }, + native: {} + }); - nodeInfoStates.forEach(state => { - setObject('0_userdata.0.Meshtastic.Nodes.' + id + '.info.' + state.id, { - type: 'state', - common: { - name: state.name, - type: state.type === 'string' ? 'string' : - state.type === 'number' ? 'number' : - state.type === 'boolean' ? 'boolean' : 'string', // cast TS - role: state.role || 'value', - unit: state.unit || null, - read: true, - write: false, - def: state.type === 'number' ? 0 : '' - }, - native: {} + // Info states + [ + ["user","User","string"], + ["alias","Alias","string"], + ["location","Location","string"], + ["latitude","Latitude","number"], + ["longitude","Longitude","number"], + ["altitude","Altitude","number"], + ["battery","Battery","number"], + ["lastMessage","Letzte Nachricht","string"] + ].forEach(s => { + safeCreateState(`${NODES}.${id}.info.${s[0]}`, { + name: s[1], type: s[2], read: true, write: false }); }); - // Command Channel - setObject('0_userdata.0.Meshtastic.Nodes.' + id + '.command', { - type: 'channel', - common: { name: 'Command' }, - native: {} - }); - - // Command States - var nodeCommandStates = [ - {id: 'sendMessage', name: 'Direktnachricht senden', type: 'string', role: 'value'}, - {id: 'sendPing', name: 'Ping senden', type: 'boolean', role: 'button'}, - {id: 'sendTraceRoute', name: 'Traceroute starten', type: 'boolean', role: 'button'}, - {id: 'getLocation', name: 'Standort anfordern', type: 'boolean', role: 'button'}, - {id: 'getTelemetry', name: 'Telemetrie anfordern', type: 'boolean', role: 'button'} - ]; - - nodeCommandStates.forEach(state => { - setObject('0_userdata.0.Meshtastic.Nodes.' + id + '.command.' + state.id, { - type: 'state', - common: { - name: state.name, - type: state.type === 'string' ? 'string' : - state.type === 'number' ? 'number' : - state.type === 'boolean' ? 'boolean' : 'string', - role: state.role, - read: true, - write: true, - def: state.type === 'number' ? 0 : '' - }, - native: {} + // Command states + [ + ["sendMessage","Direktnachricht senden","string","text"], + ["sendPing","Ping senden","boolean","button"], + ["sendTraceRoute","Traceroute starten","boolean","button"], + ["getLocation","Standort anfordern","boolean","button"], + ["getTelemetry","Telemetrie anfordern","boolean","button"] + ].forEach(s => { + safeCreateState(`${NODES}.${id}.command.${s[0]}`, { + name: s[1], type: s[2], role: s[3], read: true, write: true }); }); } // ====================================================== -// 4. Update Node Values (Location + Lat/Lon separated) +// 7. UPDATE NODE VALUES // ====================================================== function updateNode(data) { - function parseNum(val) { - if (!val || val === "N/A" || val === "Powered") return 0; - // Entfernt Einheiten wie %, m, dB und konvertiert zu Number - return parseFloat(val.replace(/[^\d.-]/g, "")) || 0; - } + const path = `${NODES}.${data.ID}.info.`; - const path = '0_userdata.0.Meshtastic.Nodes.' + data.ID + '.info.'; + safeSetState(path + "user", data.User || "N/A"); + safeSetState(path + "alias", data.AKA || "N/A"); - // Basis-Informationen - setState(path + "id", data.N || "N/A", true); - setState(path + "user", data.User || "N/A", true); - setState(path + "alias", data.AKA || "N/A", true); - - // Standort-Logik let lat = parseNum(data.Latitude); let lon = parseNum(data.Longitude); - // Nur aktualisieren, wenn echte Koordinaten geliefert werden (verhindert 0,0 Sprünge) if (lat !== 0 && lon !== 0) { - setState(path + "latitude", lat, true); - setState(path + "longitude", lon, true); - // Format lat,lon (ohne Leerzeichen) für Jarvis Maps - setState(path + "location", lat + "," + lon, true); + safeSetState(path + "latitude", lat); + safeSetState(path + "longitude", lon); + safeSetState(path + "location", lat + "," + lon); } - // Hardware- & Netzparameter - setState(path + "altitude", parseNum(data.Altitude), true); - setState(path + "chanUtil", parseNum(data["Channel util."]), true); - setState(path + "txAir", parseNum(data["Tx air util."]), true); - setState(path + "snr", parseNum(data.SNR), true); - setState(path + "channel", data.Channel || "0", true); - setState(path + "lastHeard", data.LastHeard || "N/A", true); - - // Batterie-Sonderbehandlung für "Powered" - let battVal = data.Battery === 'Powered' ? 100 : parseNum(data.Battery); - setState(path + "battery", battVal, true); + let battVal = data.Battery === "Powered" ? 100 : parseNum(data.Battery); + safeSetState(path + "battery", battVal); } // ====================================================== -// 5. Chats + SendMessage Endpoint +// 8. CHAT STRUCTURE + HISTORY // ====================================================== function createChannels() { - setObject('0_userdata.0.Meshtastic', { type: 'channel', common: { name: 'Meshtastic Server' }, native: {} }); - setObject('0_userdata.0.Meshtastic.Nodes', { type: 'channel', common: { name: 'Nodes' }, native: {} }); - setObject('0_userdata.0.Meshtastic.Chats', { type: 'channel', common: { name: 'Chats' }, native: {} }); + safeCreateObject(BASE, { type: "channel", common: { name: "Meshtastic" }, native: {} }); + safeCreateObject(NODES, { type: "channel", common: { name: "Nodes" }, native: {} }); + safeCreateObject(CHATS, { type: "channel", common: { name: "Chats" }, native: {} }); } function createChats() { - chats.forEach(chatObj => { - // create channels - setObject('0_userdata.0.Meshtastic.Chats.' + chatObj.id, { - type: 'channel', - common: { - name: chatObj.name - }, + chats.forEach(c => { + safeCreateObject(`${CHATS}.${c.id}`, { + type: "channel", + common: { name: c.name }, native: {} }); - // last message (read-only) - setObject('0_userdata.0.Meshtastic.Chats.' + chatObj.id + '.lastMessage', { - type: 'state', - common: { - name: 'Letzte Nachricht', - type: 'string', - role: 'text', - read: true, - write: false - }, - native: {} + safeCreateState(`${CHATS}.${c.id}.lastMessage`, { + name: "Letzte Nachricht", + type: "string", + role: "text", + read: true, + write: false }); - // send message (allow writing) - setObject('0_userdata.0.Meshtastic.Chats.' + chatObj.id + '.sendMessage', { - type: 'state', - common: { - name: 'Nachricht senden', - type: 'string', - role: 'text', - read: true, - write: true - }, - native: {} + safeCreateState(`${CHATS}.${c.id}.sendMessage`, { + name: "Nachricht senden", + type: "string", + role: "text", + read: true, + write: true }); - // message history as JSON (read-only) - setObject('0_userdata.0.Meshtastic.Chats.' + chatObj.id + '.history', { - type: 'state', - common: { - name: 'Chat Historie JSON', - type: 'string', - role: 'json', - read: true, - write: false - }, - native: {} - }); - - // message history as JSON html (read-only) - setObject('0_userdata.0.Meshtastic.Chats.' + chatObj.id + '.history_html', { - type: 'state', - common: { - name: 'Chat Historie HTML', - type: 'string', - role: 'html', - read: true, - write: false - }, - native: {} - }); + safeCreateState(`${CHATS}.${c.id}.history`, { + name: "Chat Historie JSON", + type: "string", + role: "json", + read: true, + write: false + }); + + safeCreateState(`${CHATS}.${c.id}.history_html`, { + name: "Chat Historie HTML", + type: "string", + role: "html", + read: true, + write: false + }); }); } -/** - * Fügt eine Nachricht zum JSON-Verlauf und HTML-String eines Kanals hinzu - * @param {number} channelIdx - Index des Kanals (0, 1, 2...) - * @param {string} senderName - Aufgelöster Name oder ID des Absenders - * @param {string} messageText - Inhalt der Nachricht - */ function addToHistory(channelIdx, senderName, messageText) { - const basePath = '0_userdata.0.Meshtastic.Chats.' + channelIdx; - const historyPath = basePath + '.history'; - const htmlPath = basePath + '.history_html'; - const maxEntries = 10; // Anzahl der gespeicherten Nachrichten - - // --- 1. JSON HISTORY (für Jarvis JsonTable) --- + const basePath = `${CHATS}.${channelIdx}`; + const historyPath = basePath + ".history"; + const htmlPath = basePath + ".history_html"; + const maxEntries = 10; + let history = []; if (existsState(historyPath)) { - let currentState = getState(historyPath).val; - try { - history = JSON.parse(currentState) || []; - } catch (e) { history = []; } + try { history = JSON.parse(getState(historyPath).val) || []; } + catch { history = []; } } - // Neues Objekt erstellen - const newEntry = { + history.unshift({ ts: Date.now(), time: formatDate(new Date(), "hh:mm"), from: senderName, text: messageText - }; + }); - // Oben einfügen & Kürzen - history.unshift(newEntry); if (history.length > maxEntries) history = history.slice(0, maxEntries); - // JSON Speichern - setState(historyPath, JSON.stringify(history), true); + safeSetState(historyPath, JSON.stringify(history)); - - // --- 2. HTML HISTORY (für Jarvis StateHTML / Messenger-Look) --- - let html = '
'; - + let html = `
`; history.forEach(m => { - html += ` -
-
- ${m.from} • ${m.time} -
-
- ${m.text} -
-
`; + html += `
+ ${m.from} • ${m.time}
${m.text}
`; }); - - html += '
'; + html += `
`; - // HTML Datenpunkt prüfen und schreiben - if (!existsState(htmlPath)) { - setObject(htmlPath, { - type: 'state', - common: { name: 'Chat Historie HTML', type: 'string', role: 'html', read: true, write: false }, - native: {} - }); - setTimeout(() => { setState(htmlPath, html, true); }, 200); - } else { - setState(htmlPath, html, true); - } + safeSetState(htmlPath, html); } // ====================================================== -// 6. Command Listener + CLI Actions +// INIT // ====================================================== -function registerEndpointListeners() { - - $('state[id=0_userdata.0.Meshtastic.Nodes.*.command.*]').each(function (id) { - on({id: id, change: "any"}, function (obj) { - - let parts = obj.id.split("."); - let nodeId = parts[4]; - let cmd = parts[6]; - - if (cmd === "getTelemetry" || cmd === "getLocation") requestTelemetry(nodeId); - if (cmd === "sendPing") sendPing(nodeId); - if (cmd === "sendTraceRoute") startTraceroute(nodeId); - - if (cmd === "sendMessage") { - let msg = getState(obj.id).val; - sendDirectMessage(nodeId, msg); - } - }); - }); - - $('state[id=0_userdata.0.Meshtastic.Chats.*.sendMessage]').each(function (id) { - on({id: id, change: "any"}, function (obj) { - let parts = obj.id.split("."); - let chatId = parts[4]; - let msg = getState(obj.id).val; - sendChatMessage(chatId, msg); - }); - }); -} - -// --- CLI Actions --- -function requestTelemetry(target) { - exec('/home/iobroker/.local/bin/meshtastic --host '+deviceIp+' --request-telemetry --dest "!'+target+'"'); -} -function startTraceroute(target) { - exec('/home/iobroker/.local/bin/meshtastic --host '+deviceIp+' --traceroute --dest "!'+target+'"'); -} -function sendPing(target) { - exec('/home/iobroker/.local/bin/meshtastic --host '+deviceIp+' --sendping --dest "!'+target+'"'); -} -function sendDirectMessage(target, message) { - exec('/home/iobroker/.local/bin/meshtastic --host '+deviceIp+' --dest "!'+target+'" --sendtext "'+message+'"'); -} -function sendChatMessage(chatId, message) { - exec('/home/iobroker/.local/bin/meshtastic --host '+deviceIp+' --ch-index '+chatId+' --sendtext "'+message+'"'); -} - -// ====================================================== -// 6. INITIALISIERUNG & START -// ====================================================== - -// Erstellt die Grundstruktur (nur beim ersten Start relevant) createChannels(); createChats(); -// Erster Abruf der Nodes beim Skriptstart -setTimeout(function() { - log("Meshtastic: Initialer Node-Abruf gestartet..."); +setTimeout(() => { + log("Meshtastic: Initial Node Poll..."); updateNodes(); }, 2000); -// Regelmäßiges Polling der Node-Liste (alle 5 Minuten) -setInterval(function() { - log("Meshtastic: Geplanter Node-Abruf läuft..."); +setInterval(() => { + log("Meshtastic: Scheduled Node Poll..."); updateNodes(); }, 300000); -log("Meshtastic: Skript erfolgreich initialisiert. MQTT-Trigger aktiv."); +log("Meshtastic: Script fully loaded (MQTT + CLI + Commands).");