// Meshtastic ioBroker Integration Kit
// ======================================================
// CONFIG
// ======================================================
const deviceIp = "192.168.1.xxx";
const mqttPath = "mqtt.3.msh.*.json.*";
const MESHTASTIC_BIN = "/home/iobroker/.local/bin/meshtastic";
const BASE = "0_userdata.0.Meshtastic";
const NODES = BASE + ".Nodes";
const CHATS = BASE + ".Chats";
const POLL_INTERVAL = 300000;
const HISTORY_MAX = 10;
const chats = [
{ name: "Default", id: 0 },
{ name: "Privat", id: 1 },
{ name: "Public", id: 2 }
];
// Node.js execFile verfügbar
const { execFile } = require("child_process");
// ======================================================
// HELPERS
// ======================================================
function safeCreateObject(id, obj) {
if (!existsObject(id)) {
setObject(id, obj);
log("Created object: " + id, "info");
}
}
function safeCreateState(id, common) {
if (!existsObject(id)) {
safeCreateObject(id, {
type: "state",
common,
native: {}
});
}
}
function escapeHtml(str) {
return String(str)
.replace(/&/g, "&")
.replace(//g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
// Robust senderHex conversion
function toSenderHex(fromField) {
let raw = String(fromField || "").replace(/^!/, "");
let hex;
if (/^\d+$/.test(raw)) {
hex = Number(raw).toString(16);
} else {
hex = raw.replace(/^0x/, "");
}
return hex.toLowerCase().padStart(8, "0");
}
// Wrapper: meshtastic CLI call
function runMeshtastic(args, cb) {
execFile(MESHTASTIC_BIN, args, (err, stdout, stderr) => {
if (err) {
log("Meshtastic CLI error: " + (stderr || err), "error");
}
if (cb) cb(err, stdout, stderr);
});
}
// ======================================================
// INIT STRUCTURE
// ======================================================
function createChannels() {
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(c => {
const base = CHATS + "." + c.id;
safeCreateObject(base, {
type: "channel",
common: { name: c.name },
native: {}
});
safeCreateState(base + ".lastMessage", {
name: "Letzte Nachricht",
type: "string",
role: "text",
read: true,
write: false
});
safeCreateState(base + ".sendMessage", {
name: "Nachricht senden",
type: "string",
role: "text",
read: true,
write: true
});
safeCreateState(base + ".history", {
name: "Chat Historie JSON",
type: "string",
role: "json",
read: true,
write: false
});
safeCreateState(base + ".history_html", {
name: "Chat Historie HTML",
type: "string",
role: "html",
read: true,
write: false
});
});
}
// ======================================================
// HISTORY
// ======================================================
function addToHistory(channelIdx, senderName, messageText) {
const base = CHATS + "." + channelIdx;
const historyPath = base + ".history";
const htmlPath = base + ".history_html";
let history = [];
if (existsState(historyPath)) {
try {
history = JSON.parse(getState(historyPath).val) || [];
} catch {
history = [];
}
}
history.unshift({
ts: Date.now(),
time: formatDate(new Date(), "hh:mm"),
from: senderName,
text: messageText
});
if (history.length > HISTORY_MAX) {
history = history.slice(0, HISTORY_MAX);
}
setState(historyPath, JSON.stringify(history), true);
// HTML output (escaped)
let html = `
`;
history.forEach(m => {
html += `
${escapeHtml(m.from)} • ${escapeHtml(m.time)}
${escapeHtml(m.text)}
`;
});
html += `
`;
setState(htmlPath, html, true);
}
// ======================================================
// MQTT TRIGGER (Realtime)
// ======================================================
on({ id: new RegExp("^" + mqttPath.replace(/\./g, "\\.").replace(/\*/g, ".*") + "$"), change: "any" }, obj => {
try {
if (!obj.state.val) return;
const msg = JSON.parse(obj.state.val);
const channelIdx = parseInt(msg.channel) || 0;
const senderHex = toSenderHex(msg.from);
const nodeBase = NODES + "." + senderHex;
const infoBase = nodeBase + ".info.";
// Name resolution
let displayName = senderHex;
if (existsState(infoBase + "alias")) {
const v = getState(infoBase + "alias").val;
if (v && v !== "N/A") displayName = v;
} else if (existsState(infoBase + "user")) {
const v = getState(infoBase + "user").val;
if (v && v !== "N/A") displayName = v;
}
// TEXT
if (msg.type === "text" && msg.payload?.text) {
const text = msg.payload.text;
safeCreateState(infoBase + "lastMessage", {
name: "Letzte Nachricht",
type: "string",
role: "text",
read: true,
write: false
});
setState(infoBase + "lastMessage", text, true);
const chatBase = CHATS + "." + channelIdx;
if (existsObject(chatBase)) {
setState(chatBase + ".lastMessage", `${displayName}: ${text}`, true);
addToHistory(channelIdx, displayName, text);
}
log(`Meshtastic Text [${channelIdx}] ${displayName}: ${text}`, "info");
}
// POSITION
if (msg.type === "position" && msg.payload) {
const p = msg.payload;
if (p.latitude_i && p.longitude_i) {
const lat = p.latitude_i / 10000000;
const lon = p.longitude_i / 10000000;
const alt = p.altitude || 0;
setState(infoBase + "latitude", lat, true);
setState(infoBase + "longitude", lon, true);
setState(infoBase + "altitude", alt, true);
setState(infoBase + "location", `${lat},${lon}`, true);
log(`Meshtastic Position ${displayName}: ${lat},${lon}`, "info");
}
}
} catch (e) {
log("MQTT Trigger Fehler: " + e, "error");
}
});
// ======================================================
// SEND MESSAGE TRIGGERS
// ======================================================
// Chat send
on({ id: /^0_userdata\.0\.Meshtastic\.Chats\.\d+\.sendMessage$/, change: "any" }, obj => {
if (obj.state.ack) return;
const msg = obj.state.val;
if (!msg) return;
const parts = obj.id.split(".");
const channelId = Number(parts[parts.length - 2]);
runMeshtastic(
["--host", deviceIp, "--ch-index", String(channelId), "--sendtext", msg],
err => {
if (!err) setTimeout(() => setState(obj.id, "", true), 300);
}
);
});
// Direct send
on({ id: /^0_userdata\.0\.Meshtastic\.Nodes\..*\.command\.sendMessage$/, change: "any", ack: false }, obj => {
const msg = obj.state.val;
if (!msg) return;
const parts = obj.id.split(".");
const nodeId = parts[parts.length - 3];
runMeshtastic(
["--host", deviceIp, "--dest", "!" + nodeId, "--sendtext", msg],
err => {
if (!err) setTimeout(() => setState(obj.id, "", true), 300);
}
);
});
// ======================================================
// CLI NODE POLLING
// ======================================================
/**
* Parst die CLI-Ausgabe von meshtastic --nodes in ein Array von Node-Objekten
* @param {string} data - CLI-Ausgabe
* @returns {Array