forked from iarv/meshtastic-cli-iobroker-mqtt
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f11faa8fc8 | ||
|
|
282d0038b0 | ||
|
|
3dc572ec84 |
116
README.md
116
README.md
@@ -34,6 +34,7 @@ Ziel ist es, Meshtastic-Netzwerke in ioBroker sichtbar und steuerbar zu machen:
|
||||
- ioBroker mit JavaScript- und MQTT-Adapter -> Mein Szenario: ioBroker v7.7.22 mit JS adapter 9.0.11
|
||||
- Python 3 + Meshtastic CLI -> Mein Szenario: Debian Bookworm mit Python 3.11 und meshtastic cli 2.7.7
|
||||
- Mosquitto Broker als lokales MQTT-Gateway/Bridge -> Mein Szenario: Mosquitto 2.0.11 aus dem Standard Debian Repo
|
||||
- Meshtastic MQTT Parser für Nachrichten die über einen öffentlichen MQTT kommen, denn die werden von der Node ja nicht nochmal als entschlüsseltes JSON zurückgegeben -> Mein Szenarion: https://github.com/acidvegas/meshtastic-mqtt-json R2.0.0 aus dem Server hinterlegt, auf dem auch die Mosquitto Bridge läuft
|
||||
|
||||
---
|
||||
|
||||
@@ -138,7 +139,116 @@ systemctl restart mosquitto.service
|
||||
|
||||
---
|
||||
|
||||
### 4. Konfigurieren der MQTT Instanz in ioBroker
|
||||
### 4. Meshtastic MQTT Parser installieren
|
||||
|
||||
- Der Meshtastic MQTT Parser wird auf dem gleichen Server installiert, wo auch der zuvor installierte Mosquitto Server läuft. Hier das GitHub Repo: https://github.com/acidvegas/meshtastic-mqtt-json
|
||||
|
||||
```bash
|
||||
pip install meshtastic-mqtt-json
|
||||
```
|
||||
|
||||
- Wir brauchen ein kleines Python Script, welches uns die Nachrichten und Positionsdaten, die verschlüsselt über einen öffentlichen MQTT Server unter msh/EU_868/2/e/<channel>/ kommen, entschlüsselt und in der gleichen Form unter msh/EU_868/2/json/<channel> ablegt wie es eine Meshtastic Node tun würde.
|
||||
- Die restlichen restlichen Telemetriedaten sind nicht so zeitkritisch und können zyklisch über den meshtastic-cli geholt werden.
|
||||
- Je Kanal der gelesen werden soll wird eine entsprechend konfigurierte Datei gebraucht.
|
||||
|
||||
```python
|
||||
import json
|
||||
import time
|
||||
import paho.mqtt.client as mqtt
|
||||
from meshtastic_mqtt_json import MeshtasticMQTT
|
||||
|
||||
# --- KONFIGURATION ---
|
||||
LOCAL_BROKER = "<ip-of-your-mosquitto-server"
|
||||
LOCAL_PORT = 1883
|
||||
LOCAL_USER = "<username>"
|
||||
LOCAL_PASS = "<password>"
|
||||
|
||||
# Kanal-Details
|
||||
CHANNEL_NAME = "<channel-name>"
|
||||
CHANNEL_INDEX = <channel-id/numer>
|
||||
CHANNEL_KEY = "<channel-key>"
|
||||
REGION = "EU_868"
|
||||
ENCRYPTED_ROOT = f"msh/{REGION}/2/e/"
|
||||
|
||||
# --- SETUP LOKALER PUBLISHER ---
|
||||
publisher = mqtt.Client()
|
||||
publisher.username_pw_set(LOCAL_USER, LOCAL_PASS)
|
||||
publisher.connect(LOCAL_BROKER, LOCAL_PORT)
|
||||
publisher.loop_start()
|
||||
|
||||
def send_to_iobroker(sender_id_dec, msg_type, payload_data):
|
||||
"""Baut das Meshtastic-JSON-Format nach und sendet es lokal."""
|
||||
sender_id_hex = hex(sender_id_dec)[2:].lower().zfill(8)
|
||||
# Ziel-Topic für den ioBroker JavaScript-Trigger
|
||||
topic = f"msh/{REGION}/2/json/{CHANNEL_NAME}/!{sender_id_hex}"
|
||||
|
||||
full_payload = {
|
||||
"from": sender_id_dec,
|
||||
"channel": CHANNEL_INDEX,
|
||||
"type": msg_type,
|
||||
"payload": payload_data,
|
||||
"timestamp": int(time.time())
|
||||
}
|
||||
publisher.publish(topic, json.dumps(full_payload), qos=1)
|
||||
|
||||
# --- CALLBACKS ---
|
||||
|
||||
def on_text_message(json_data):
|
||||
"""Verarbeitet reine Textnachrichten."""
|
||||
# json_data["decoded"]["payload"] enthält bei Textnachrichten den String
|
||||
send_to_iobroker(json_data["from"], "text", {"text": json_data["decoded"]["payload"]})
|
||||
print(f'Relayed Text: {json_data["decoded"]["payload"]}')
|
||||
|
||||
def on_position(json_data):
|
||||
"""Verarbeitet GPS-Positionen."""
|
||||
# json_data["decoded"]["payload"] enthält hier das Positions-Objekt
|
||||
p = json_data["decoded"]["payload"]
|
||||
|
||||
# Payload-Struktur für dein ioBroker-Skript aufbauen
|
||||
# Meshtastic nutzt oft latitude_i (Integer) statt Float für Präzision
|
||||
pos_payload = {
|
||||
"latitude_i": p.get("latitude_i"),
|
||||
"longitude_i": p.get("longitude_i"),
|
||||
"altitude": p.get("altitude")
|
||||
}
|
||||
|
||||
send_to_iobroker(json_data["from"], "position", pos_payload)
|
||||
print(f'Relayed Position Update from {json_data["from"]}')
|
||||
|
||||
# --- SETUP DECRYPTOR ---
|
||||
decryptor = MeshtasticMQTT()
|
||||
|
||||
# Registrierung der gewünschten Callbacks
|
||||
decryptor.register_callback('TEXT_MESSAGE_APP', on_text_message)
|
||||
decryptor.register_callback('POSITION_APP', on_position)
|
||||
|
||||
# Authentifizierung am internen Client setzen
|
||||
if hasattr(decryptor, '_client'):
|
||||
decryptor._client.username_pw_set(LOCAL_USER, LOCAL_PASS)
|
||||
|
||||
# Verbindung zum lokalen Broker herstellen
|
||||
decryptor.connect(
|
||||
LOCAL_BROKER,
|
||||
LOCAL_PORT,
|
||||
ENCRYPTED_ROOT,
|
||||
CHANNEL_NAME,
|
||||
LOCAL_USER,
|
||||
LOCAL_PASS,
|
||||
CHANNEL_KEY
|
||||
)
|
||||
```
|
||||
|
||||
Das Script kann für den Test mit Screen im Hintergrund laufen lassen:
|
||||
|
||||
```bash
|
||||
screen -S <channel-name>
|
||||
python <scriptname.py>
|
||||
CTRL-A-D
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 5. Konfigurieren der MQTT Instanz in ioBroker
|
||||
|
||||
- Eine MQTT Instanz im ioBroker muss auf unsere Mosquitto Bridge konfiguriert werden
|
||||
- IP, Port, Username und Password müssen auf die Bridge zeigen
|
||||
@@ -147,7 +257,7 @@ systemctl restart mosquitto.service
|
||||
|
||||
---
|
||||
|
||||
### 5. JS script in ioBroker anlegen und aktivieren
|
||||
### 6. JS script in ioBroker anlegen und aktivieren
|
||||
|
||||
- Das Script meshcli_iobroker.js als JS in ioBroker anlegen und die Konfigurationen im oberen Abschnitt des Scripts an die eigenen Bedürfnisse anpassen
|
||||
- Damit das Script Datenpunkte anlegen kann, muss "Enable command "setObject"" in der JS Instanz erlaubt werden
|
||||
@@ -220,6 +330,8 @@ Das Skript arbeitet hybrid:
|
||||
|
||||
** Integration: Senden und Empfangen beliebiger Nachrichten bzw. Steuermöglichkeit des ioBroker durch Nachrichten
|
||||
|
||||
** Visualisierung in VIS/Jarvis: Sowohl die Positionen als auch der Chatverlauf kann über die History bzw. History-HTML Datenpunkte leicht visualiert werden
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
@@ -22,30 +22,30 @@ on({id: /^mqtt\.3\.msh\..*\.json\..*$/, change: "any"}, function (obj) {
|
||||
if (!obj.state.val) return;
|
||||
const msg = JSON.parse(obj.state.val);
|
||||
|
||||
if (msg.payload && msg.payload.text) {
|
||||
const text = msg.payload.text;
|
||||
const channelIdx = msg.channel || 0;
|
||||
// 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;
|
||||
|
||||
let senderHex = parseInt(msg.from).toString(16).toLowerCase();
|
||||
const nodeBasePath = '0_userdata.0.Meshtastic.Nodes.' + senderHex;
|
||||
// --- Namensauflösung ---
|
||||
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;
|
||||
}
|
||||
|
||||
// --- FALL 1: TEXTNACHRICHTEN ---
|
||||
if (msg.type === "text" && msg.payload && msg.payload.text) {
|
||||
const text = msg.payload.text;
|
||||
const nodeMsgPath = nodeBasePath + '.info.lastMessage';
|
||||
|
||||
// --- Name Resolution ---
|
||||
let displayName = senderHex;
|
||||
let aliasPath = nodeBasePath + '.info.alias';
|
||||
let userPath = nodeBasePath + '.info.user';
|
||||
|
||||
if (existsState(aliasPath)) {
|
||||
let aliasVal = getState(aliasPath).val;
|
||||
if (aliasVal && aliasVal !== 'N/A') {
|
||||
displayName = aliasVal;
|
||||
} else if (existsState(userPath)) {
|
||||
displayName = getState(userPath).val;
|
||||
}
|
||||
}
|
||||
|
||||
// Save in Node Info
|
||||
if (getObject(nodeBasePath)) {
|
||||
// In Node Info speichern
|
||||
if (existsObject(nodeBasePath)) {
|
||||
if (!existsState(nodeMsgPath)) {
|
||||
setObject(nodeMsgPath, {
|
||||
type: 'state',
|
||||
@@ -56,65 +56,96 @@ on({id: /^mqtt\.3\.msh\..*\.json\..*$/, change: "any"}, function (obj) {
|
||||
setState(nodeMsgPath, text, true);
|
||||
}
|
||||
|
||||
// Save in Chat Channel
|
||||
// In Chat & Historie speichern
|
||||
const chatPath = '0_userdata.0.Meshtastic.Chats.' + channelIdx;
|
||||
if (getObject(chatPath)) {
|
||||
if (existsObject(chatPath)) {
|
||||
setState(chatPath + '.lastMessage', `${displayName}: ${text}`, true);
|
||||
|
||||
// --- add to history ---
|
||||
addToHistory(channelIdx, displayName, text);
|
||||
} else {
|
||||
log(`Chat-Kanal ${channelIdx} nicht gefunden unter ${chatPath}`, "warn");
|
||||
}
|
||||
log(`Meshtastic Text: [Kanal ${channelIdx}] ${displayName}: ${text}`);
|
||||
}
|
||||
|
||||
log(`Meshtastic Chat [${channelIdx}]: ${displayName} sagt "${text}"`);
|
||||
// --- FALL 2: POSITIONSDATEN ---
|
||||
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);
|
||||
|
||||
log(`Meshtastic Position: ${displayName} ist bei ${lat}, ${lon}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
log("Fehler im MQTT Trigger: " + e, "error");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Trigger für Nachrichten an Kanäle (Chats)
|
||||
on({id: /^0_userdata\.0\.Meshtastic\.Chats\.\d+\.sendMessage$/, change: "any", ack: false}, function (obj) {
|
||||
on({id: /^0_userdata\.0\.Meshtastic\.Chats\.\d+\.sendMessage$/, change: "any"}, function (obj) {
|
||||
const msg = obj.state.val;
|
||||
if (!msg || msg === "") return;
|
||||
if (!msg || msg === "" || obj.state.ack === true) return;
|
||||
|
||||
const parts = obj.id.split('.');
|
||||
// parseInt stellt sicher, dass channelId eine Nummer ist
|
||||
const channelId = parseInt(parts[parts.length - 2]);
|
||||
|
||||
log(`Meshtastic: Sende Nachricht an Kanal ${channelId}: ${msg}`);
|
||||
// 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, "");
|
||||
|
||||
// In der CLI-Exec wird channelId wieder zum String für den Befehl,
|
||||
// aber für addToHistory ist es nun die korrekte Nummer.
|
||||
exec(`/home/iobroker/.local/bin/meshtastic --host ${deviceIp} --ch-index ${channelId} --sendtext "${msg}"`, function (error, result, stderr) {
|
||||
log(`Meshtastic: Sende an Kanal ${channelId} (von ${source}): ${safeMsg}`);
|
||||
|
||||
// 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}'`;
|
||||
|
||||
exec(command, function (error, stdout, stderr) {
|
||||
if (error) {
|
||||
log(`Fehler beim Senden: ${stderr}`, 'error');
|
||||
log(`Fehler beim Senden (Kanal ${channelId}): ${stderr || error}`, 'error');
|
||||
} else {
|
||||
setState(obj.id, "", true);
|
||||
// Jetzt passt der Typ für den ersten Parameter (number)
|
||||
addToHistory(channelId, "ICH (ioBroker)", msg);
|
||||
// Feld leeren nach Erfolg
|
||||
setTimeout(() => setState(obj.id, "", true), 500);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Trigger für Direktnachrichten an einzelne Nodes
|
||||
// 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]; // Pfad ist Nodes.ID.command.sendMessage
|
||||
|
||||
log(`Meshtastic: Sende Direktnachricht an !${nodeId}: ${msg}`);
|
||||
const nodeId = parts[parts.length - 3];
|
||||
const safeMsg = msg.replace(/"/g, '\\"');
|
||||
|
||||
exec(`/home/iobroker/.local/bin/meshtastic --host ${deviceIp} --dest "!${nodeId}" --sendtext "${msg}"`, function (error, result, stderr) {
|
||||
if (error) {
|
||||
log(`Fehler beim Senden an Node: ${stderr}`, 'error');
|
||||
} else {
|
||||
setState(obj.id, "", true);
|
||||
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
|
||||
// ======================================================
|
||||
@@ -258,30 +289,41 @@ function createNodeStates(id) {
|
||||
function updateNode(data) {
|
||||
|
||||
function parseNum(val) {
|
||||
if (!val || val === "N/A") return 0;
|
||||
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 = '0_userdata.0.Meshtastic.Nodes.' + data.ID + '.info.';
|
||||
|
||||
setState(path + "id", data.N, true);
|
||||
setState(path + "user", data.User, true);
|
||||
setState(path + "alias", data.AKA, true);
|
||||
// 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);
|
||||
|
||||
setState(path + "latitude", lat, true);
|
||||
setState(path + "longitude", lon, true);
|
||||
setState(path + "location", lat + ", " + lon, true);
|
||||
// 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);
|
||||
}
|
||||
|
||||
// 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, true);
|
||||
setState(path + "lastHeard", data.LastHeard, true);
|
||||
setState(path + "battery", parseNum(data.Battery), 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);
|
||||
}
|
||||
|
||||
// ======================================================
|
||||
@@ -342,6 +384,19 @@ function createChats() {
|
||||
},
|
||||
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: {}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -355,7 +410,7 @@ function addToHistory(channelIdx, senderName, messageText) {
|
||||
const basePath = '0_userdata.0.Meshtastic.Chats.' + channelIdx;
|
||||
const historyPath = basePath + '.history';
|
||||
const htmlPath = basePath + '.history_html';
|
||||
const maxEntries = 20; // Anzahl der gespeicherten Nachrichten
|
||||
const maxEntries = 10; // Anzahl der gespeicherten Nachrichten
|
||||
|
||||
// --- 1. JSON HISTORY (für Jarvis JsonTable) ---
|
||||
let history = [];
|
||||
@@ -369,7 +424,7 @@ function addToHistory(channelIdx, senderName, messageText) {
|
||||
// Neues Objekt erstellen
|
||||
const newEntry = {
|
||||
ts: Date.now(),
|
||||
time: formatDate(new Date(), "HH:mm"),
|
||||
time: formatDate(new Date(), "hh:mm"),
|
||||
from: senderName,
|
||||
text: messageText
|
||||
};
|
||||
@@ -463,11 +518,23 @@ function sendChatMessage(chatId, message) {
|
||||
}
|
||||
|
||||
// ======================================================
|
||||
// INIT
|
||||
// 6. INITIALISIERUNG & START
|
||||
// ======================================================
|
||||
|
||||
// Erstellt die Grundstruktur (nur beim ersten Start relevant)
|
||||
createChannels();
|
||||
createChats();
|
||||
registerEndpointListeners();
|
||||
|
||||
updateNodes();
|
||||
setInterval(updateNodes, 300000);
|
||||
// Erster Abruf der Nodes beim Skriptstart
|
||||
setTimeout(function() {
|
||||
log("Meshtastic: Initialer Node-Abruf gestartet...");
|
||||
updateNodes();
|
||||
}, 2000);
|
||||
|
||||
// Regelmäßiges Polling der Node-Liste (alle 5 Minuten)
|
||||
setInterval(function() {
|
||||
log("Meshtastic: Geplanter Node-Abruf läuft...");
|
||||
updateNodes();
|
||||
}, 300000);
|
||||
|
||||
log("Meshtastic: Skript erfolgreich initialisiert. MQTT-Trigger aktiv.");
|
||||
|
||||
Reference in New Issue
Block a user