Ich war so frei und habe eine Scriptversion gebsatelt.
Die Dezimaldaten werden direkt konvertiert sodas direkt lesbare Datenpunkte entstehen.
Das ganze kann als Script in iobroker verwendet werden.
Einfach Moonraker ip anpassen, ggf Alexa ID anpassen.
Der Code darf gerne ganz oder Teilweise für den Adapter genutzt werden.
Spoiler
// ======== CONFIG ========
const MOONRAKER_HOST = "192.168.1.160";
const MOONRAKER_PORT = 7125;
const BASE = `http://${MOONRAKER_HOST}:${MOONRAKER_PORT}`;
const POLL_MS = 10_000;
// Ablagepfad in ioBroker
const ROOT = "javascript.0.moonraker.Kossel";
// Optional: Testfunktion (Alexa-Ansage jede Minute)
const ENABLE_TEST_SPEAK = false; // ← bei Bedarf auf true
const ALEXA_SPEAK_DP = "alexa2.0.Echo-Devices.IDANPASSEN.Commands.speak"; // Deine Echo-ID einsetzen
const TEST_MESSAGE = "Moonraker Script Test läuft.";
// ======== HELFER ========
// HTTP GET (Node.js Core)
function httpGet(url, timeout = 5000) {
return new Promise((resolve, reject) => {
const http = require("http");
const req = http.get(url, (res) => {
if (res.statusCode < 200 || res.statusCode >= 300) {
reject(new Error(`HTTP ${res.statusCode} for ${url}`));
res.resume();
return;
}
let data = "";
res.setEncoding("utf8");
res.on("data", (chunk) => (data += chunk));
res.on("end", () => {
try {
resolve(JSON.parse(data));
} catch (e) {
reject(new Error(`JSON parse error for ${url}: ${e.message}`));
}
});
});
req.on("error", reject);
req.setTimeout(timeout, () => {
req.destroy(new Error(`Timeout after ${timeout}ms for ${url}`));
});
});
}
// State-Helper
async function ensureState(id, def) {
if (!existsState(id)) {
await createStateAsync(id, def, { role: def.common?.role || "state" });
}
}
async function writeNum(id, val, unit = "", role = "value") {
await ensureState(id, {
type: "number",
read: true,
write: false,
def: 0,
name: id.split(".").slice(-1)[0],
role,
unit
});
setState(id, { val: Number(val), ack: true });
}
async function writeStr(id, val, role = "text") {
await ensureState(id, {
type: "string",
read: true,
write: false,
def: "",
name: id.split(".").slice(-1)[0],
role
});
setState(id, { val: String(val ?? ""), ack: true });
}
async function writeBool(id, val, role = "indicator") {
await ensureState(id, {
type: "boolean",
read: true,
write: false,
def: false,
name: id.split(".").slice(-1)[0],
role
});
setState(id, { val: !!val, ack: true });
}
// Formatierungen / Konvertierungen
function toPercent01(v) {
if (v == null || isNaN(v)) return null;
return Math.round(Number(v) * 1000) / 10; // 1 Nachkommastelle
}
function toHMS(seconds) {
if (seconds == null || isNaN(seconds)) return "";
const s = Math.max(0, Math.floor(seconds));
const h = Math.floor(s / 3600);
const m = Math.floor((s % 3600) / 60);
const sec = s % 60;
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}:${String(sec).padStart(2, "0")}`;
}
function round1(v) {
if (v == null || isNaN(v)) return null;
return Math.round(Number(v) * 10) / 10;
}
// Dynamik-Cache für Objekte (Fans, Temp-Sensoren, etc.)
let discovered = {
fans: [],
tempSensors: [],
hasHeaterBed: false,
hasExtruder: false,
hasDisplayStatus: false,
hasGcodeMove: false,
hasVirtualSd: false,
hasToolhead: false,
hasPrintStats: false,
lastDiscoveryTs: 0
};
async function discoverObjects() {
try {
const list = await httpGet(`${BASE}/printer/objects/list`);
const raw = list?.result?.objects;
let names = [];
if (Array.isArray(raw)) {
names = raw
.map((o) => (typeof o === "string" ? o : (o && o.name) ? String(o.name) : ""))
.filter((n) => typeof n === "string" && n.length > 0);
} else if (raw && typeof raw === "object") {
names = Object.keys(raw);
} else {
names = [];
}
const isStr = (n) => typeof n === "string" && n.length > 0;
discovered.fans = names.filter((n) =>
isStr(n) && (n.startsWith("fan") || n.startsWith("heater_fan") || n.startsWith("controller_fan") || n.startsWith("fan_generic"))
);
discovered.tempSensors = names.filter((n) =>
isStr(n) && (n.startsWith("temperature_sensor") || n.startsWith("temperature_fan") || n.startsWith("bme280") || n.startsWith("htu21d") || n.startsWith("lm75"))
);
discovered.hasHeaterBed = names.includes("heater_bed");
discovered.hasExtruder = names.includes("extruder");
discovered.hasDisplayStatus = names.includes("display_status");
discovered.hasGcodeMove = names.includes("gcode_move");
discovered.hasVirtualSd = names.includes("virtual_sdcard");
discovered.hasToolhead = names.includes("toolhead");
discovered.hasPrintStats = names.includes("print_stats");
discovered.lastDiscoveryTs = Date.now();
await writeStr(`${ROOT}.meta.last_discovery`, new Date().toISOString());
await writeNum(`${ROOT}.meta.discovered.fans_count`, discovered.fans.length, "", "value");
await writeNum(`${ROOT}.meta.discovered.temp_sensors_count`, discovered.tempSensors.length, "", "value");
} catch (e) {
log(`Moonraker discovery error: ${e.message}`, "warn");
}
}
function buildQueryParams() {
const objs = ["print_stats"];
if (discovered.hasHeaterBed) objs.push("heater_bed");
if (discovered.hasExtruder) objs.push("extruder");
if (discovered.hasDisplayStatus) objs.push("display_status");
if (discovered.hasGcodeMove) objs.push("gcode_move");
if (discovered.hasVirtualSd) objs.push("virtual_sdcard");
if (discovered.hasToolhead) objs.push("toolhead");
if (discovered.hasPrintStats) objs.push("print_stats");
objs.push(...discovered.fans);
objs.push(...discovered.tempSensors);
const q = objs.map(encodeURIComponent).join("&");
return `${BASE}/printer/objects/query?${q}`;
}
// ======== REST DES POLLERS (pollOnce, Startup, Timer, etc.) ========
// Alexa-Ansage Tracking (Doppelansagen vermeiden)
let lastPrintState = "";
let lastAnnouncedFile = "";
let lastAnnouncedTs = 0;
const ANNOUNCE_COOLDOWN_MS = 5 * 60 * 1000; // 5 Minuten
async function pollOnce() {
try {
// 1) Basisinfos
const [serverInfo, printerInfo] = await Promise.all([
httpGet(`${BASE}/server/info`),
httpGet(`${BASE}/printer/info`)
]);
const s = serverInfo?.result || {};
await writeStr(`${ROOT}.server.version`, s.version || "");
await writeStr(`${ROOT}.server.klippy_state`, s.klippy_state || "");
await writeStr(`${ROOT}.server.hostname`, s.hostname || "");
await writeStr(`${ROOT}.server.websocket_address`, s.websocket_address || "");
const p = printerInfo?.result || {};
await writeStr(`${ROOT}.printer.state`, p.state || "unknown");
await writeBool(`${ROOT}.printer.ready`, p.state === "ready", "indicator.reachable");
// 2) Objekte ggf. alle 5 Min neu entdecken
if (Date.now() - discovered.lastDiscoveryTs > 5 * 60 * 1000 || discovered.lastDiscoveryTs === 0) {
await discoverObjects();
}
// 3) Printer Objects abfragen
const queryUrl = buildQueryParams();
const q = await httpGet(queryUrl);
const res = q?.result?.status || {};
// ---- print_stats ----
let currentFilename = "";
let layerInfo = { current: null, total: null };
let metaFromFile = { layer_height: null, object_height: null };
if (res.print_stats) {
const ps = res.print_stats;
await writeStr(`${ROOT}.print_stats.state`, ps.state || "");
await writeStr(`${ROOT}.print_stats.filename`, ps.filename || "");
await writeStr(`${ROOT}.print_stats.message`, ps.message || "");
currentFilename = ps.filename || "";
// === Alexa-Ansage bei Fertigstellung ===
try {
let shouldAnnounce = false;
if ((ps.state || "") === "complete") {
if (currentFilename) {
if (currentFilename !== lastAnnouncedFile) {
shouldAnnounce = true;
lastAnnouncedFile = currentFilename;
}
} else if (lastPrintState !== "complete" && (Date.now() - lastAnnouncedTs) > ANNOUNCE_COOLDOWN_MS) {
shouldAnnounce = true;
}
}
if (shouldAnnounce) {
const msg = currentFilename ? `Druckauftrag abgeschlossen: ${currentFilename}.` : "Der 3D Druck ist fertig.";
setState(ALEXA_SPEAK_DP, { val: msg, ack: false }); // nutzt deinen Alexa-Datenpunkt
lastAnnouncedTs = Date.now();
await writeStr(`${ROOT}.meta.last_completed_file`, currentFilename || "");
await writeStr(`${ROOT}.meta.last_announcement`, new Date().toISOString());
}
lastPrintState = ps.state || "";
} catch (e) {
log("Alexa-Fertig-Ansage fehlgeschlagen: " + e.message, "warn");
}
if (ps.total_duration != null) await writeStr(`${ROOT}.print_stats.total_duration_hms`, toHMS(ps.total_duration), "time");
if (ps.print_duration != null) await writeStr(`${ROOT}.print_stats.print_duration_hms`, toHMS(ps.print_duration), "time");
if (ps.filament_used != null) {
const m = Math.round((Number(ps.filament_used) || 0) / 10) / 100; // mm → m
await writeNum(`${ROOT}.print_stats.filament_used_m`, m, "m", "value.length");
}
if (ps.progress != null) {
await writeNum(`${ROOT}.print_stats.progress_percent`, toPercent01(ps.progress), "%", "value.percent");
}
// Layer bevorzugt aus print_stats.info (nur vorhanden, wenn Slicer SET_PRINT_STATS_INFO mitschreibt)
const info = ps.info || {};
if (info.current_layer != null) layerInfo.current = Number(info.current_layer);
if (info.total_layer != null) layerInfo.total = Number(info.total_layer);
}
// ---- toolhead (für Z→Layer-Fallback) ----
let zPos = null;
if (res.toolhead) {
const th = res.toolhead;
if (Array.isArray(th.position)) {
const [x, y, z] = th.position;
zPos = (z != null ? Number(z) : null);
await writeNum(`${ROOT}.toolhead.x`, round1(x), "mm", "value.length");
await writeNum(`${ROOT}.toolhead.y`, round1(y), "mm", "value.length");
await writeNum(`${ROOT}.toolhead.z`, round1(z), "mm", "value.length");
}
if (th.max_velocity != null) await writeNum(`${ROOT}.toolhead.max_velocity`, round1(th.max_velocity), "mm/s", "value.speed");
if (th.max_accel != null) await writeNum(`${ROOT}.toolhead.max_accel`, round1(th.max_accel), "mm/s²", "value.acceleration");
if (th.homing_origin && Array.isArray(th.homing_origin)) {
const [hx, hy, hz] = th.homing_origin;
await writeNum(`${ROOT}.toolhead.homing_origin.x`, round1(hx), "mm", "value.length");
await writeNum(`${ROOT}.toolhead.homing_origin.y`, round1(hy), "mm", "value.length");
await writeNum(`${ROOT}.toolhead.homing_origin.z`, round1(hz), "mm", "value.length");
}
if (th.print_time != null) await writeStr(`${ROOT}.toolhead.print_time_hms`, toHMS(th.print_time), "time");
}
// ---- gcode_move ----
if (res.gcode_move) {
const gm = res.gcode_move;
if (gm.speed != null) await writeNum(`${ROOT}.gcode_move.speed`, round1(gm.speed), "mm/s", "value.speed");
if (gm.speed_factor != null) await writeNum(`${ROOT}.gcode_move.speed_factor_percent`, toPercent01(gm.speed_factor), "%", "value.percent");
if (gm.extrude_factor != null) await writeNum(`${ROOT}.gcode_move.extrude_factor_percent`, toPercent01(gm.extrude_factor), "%", "value.percent");
if (Array.isArray(gm.gcode_position)) {
const [x, y, z, e] = gm.gcode_position;
await writeNum(`${ROOT}.gcode_move.gcode_x`, round1(x), "mm", "value.length");
await writeNum(`${ROOT}.gcode_move.gcode_y`, round1(y), "mm", "value.length");
await writeNum(`${ROOT}.gcode_move.gcode_z`, round1(z), "mm", "value.length");
await writeNum(`${ROOT}.gcode_move.gcode_e`, round1(e), "mm", "value.length");
}
}
// ---- heater_bed ----
if (res.heater_bed) {
const hb = res.heater_bed;
if (hb.temperature != null) await writeNum(`${ROOT}.heater_bed.temperature`, round1(hb.temperature), "°C", "value.temperature");
if (hb.target != null) await writeNum(`${ROOT}.heater_bed.target`, round1(hb.target), "°C", "value.temperature");
if (hb.power != null) await writeNum(`${ROOT}.heater_bed.power_percent`, toPercent01(hb.power), "%", "value.percent");
}
// ---- extruder ----
if (res.extruder) {
const ex = res.extruder;
if (ex.temperature != null) await writeNum(`${ROOT}.extruder.temperature`, round1(ex.temperature), "°C", "value.temperature");
if (ex.target != null) await writeNum(`${ROOT}.extruder.target`, round1(ex.target), "°C", "value.temperature");
if (ex.power != null) await writeNum(`${ROOT}.extruder.power_percent`, toPercent01(ex.power), "%", "value.percent");
if (ex.pressure_advance != null) await writeNum(`${ROOT}.extruder.pressure_advance`, round1(ex.pressure_advance), "", "value");
if (ex.smooth_time != null) await writeNum(`${ROOT}.extruder.smooth_time`, round1(ex.smooth_time), "s", "value.interval");
}
// ---- virtual_sdcard ----
if (res.virtual_sdcard) {
const vsd = res.virtual_sdcard;
if (vsd.progress != null) await writeNum(`${ROOT}.virtual_sdcard.progress_percent`, toPercent01(vsd.progress), "%", "value.percent");
if (vsd.file_position != null) await writeNum(`${ROOT}.virtual_sdcard.file_position`, Number(vsd.file_position), "B", "value");
}
// ---- display_status ----
if (res.display_status) {
const ds = res.display_status;
await writeStr(`${ROOT}.display_status.message`, ds.message || "");
if (ds.progress != null) await writeNum(`${ROOT}.display_status.progress_percent`, toPercent01(ds.progress), "%", "value.percent");
}
// ---- Fans ----
for (const f of discovered.fans) {
const key = f;
const safeKey = key.replace(/\s+/g, "_");
const obj = res[key];
if (!obj) continue;
if (obj.speed != null) await writeNum(`${ROOT}.fans.${safeKey}.speed_percent`, toPercent01(obj.speed), "%", "value.percent");
if (obj.rpm != null) await writeNum(`${ROOT}.fans.${safeKey}.rpm`, Number(obj.rpm), "rpm", "value");
}
// ---- Temperatur-Sensoren ----
for (const t of discovered.tempSensors) {
const key = t;
const safeKey = key.replace(/\s+/g, "_");
const obj = res[key];
if (!obj) continue;
if (obj.temperature != null) await writeNum(`${ROOT}.temp_sensors.${safeKey}.temperature`, round1(obj.temperature), "°C", "value.temperature");
if (obj.speed != null) await writeNum(`${ROOT}.temp_sensors.${safeKey}.speed_percent`, toPercent01(obj.speed), "%", "value.percent");
}
// ---- Datei-Metadaten (Layer-Fallback) ----
if ((layerInfo.total == null || layerInfo.current == null) && currentFilename) {
try {
const meta = await httpGet(`${BASE}/server/files/metadata?filename=${encodeURIComponent(currentFilename)}`);
const md = meta?.result || {};
const lh = md?.layer_height ?? md?.slicer?.layer_height ?? null;
const oh = md?.object_height ?? md?.metadata?.object_height ?? null;
if (lh != null) metaFromFile.layer_height = Number(lh);
if (oh != null) metaFromFile.object_height = Number(oh);
if (metaFromFile.layer_height != null) await writeNum(`${ROOT}.file.meta.layer_height_mm`, round1(metaFromFile.layer_height), "mm", "value.length");
if (metaFromFile.object_height != null) await writeNum(`${ROOT}.file.meta.object_height_mm`, round1(metaFromFile.object_height), "mm", "value.length");
if (layerInfo.total == null && metaFromFile.layer_height && metaFromFile.object_height) {
layerInfo.total = Math.max(1, Math.round(metaFromFile.object_height / metaFromFile.layer_height));
}
if (layerInfo.current == null && metaFromFile.layer_height && zPos != null) {
layerInfo.current = Math.max(0, Math.min(layerInfo.total || 999999, Math.floor(zPos / metaFromFile.layer_height)));
}
} catch (e) {
// nicht kritisch
}
}
// ---- Layer in Datenpunkte schreiben ----
if (layerInfo.current != null) await writeNum(`${ROOT}.layers.current`, layerInfo.current, "", "value");
if (layerInfo.total != null) await writeNum(`${ROOT}.layers.total`, layerInfo.total, "", "value");
if (layerInfo.current != null && layerInfo.total != null && layerInfo.total > 0) {
const pct = Math.round((layerInfo.current / layerInfo.total) * 1000) / 10;
await writeNum(`${ROOT}.layers.progress_percent`, pct, "%", "value.percent");
}
await writeStr(`${ROOT}.meta.last_update`, new Date().toISOString(), "date");
await writeBool(`${ROOT}.meta.ok`, true, "indicator.working");
} catch (e) {
await writeBool(`${ROOT}.meta.ok`, false, "indicator.working");
await writeStr(`${ROOT}.meta.error`, e.message || String(e));
log(`Moonraker poll error: ${e.message}`, "warn");
}
}
// ======== STARTUP ========
(async () => {
await ensureState(`${ROOT}.meta.ok`, { type: "boolean", def: false, read: true, write: false, role: "indicator.working" });
await discoverObjects();
pollOnce();
})();
// Intervall
let pollTimer = setInterval(pollOnce, POLL_MS);
// ======== OPTIONALE TESTFUNKTION (Alexa) ========
let testTimer = null;
if (ENABLE_TEST_SPEAK) {
testTimer = setInterval(() => {
try {
setState(ALEXA_SPEAK_DP, { val: TEST_MESSAGE, ack: false });
} catch (e) {
log("Alexa-Testansage fehlgeschlagen: " + e.message, "warn");
}
}, 60_000);
}
iScreen Shoter - Firefox - 250816165523.jpg