Module Developers

Private

You have developed a module for the MagicMirror? Join the group and get a "Module Developer" badge!

Posts

  • RE: PIR / MQTT - Presence sensor(s) revived

    Dear @atwist,

    first of all apologies for long delay - I was on vacation and offline.

    A small disclaimer upfront: I haven’t seen your actual MQTT traffic, so the following is a hypothesis based on the log excerpt you posted. It fits the symptoms cleanly, but please verify before changing anything.

    What I see in your log:

    • The module connects to the broker fine
    • It subscribes successfully to sensor/presence
    • [updatePresence] fires (so messages ARE arriving — otherwise you wouldn’t see those entries triggered)
    • But no parse error is logged, which means JSON.parse() did not throw
    • The result is consistently mqttPresence=false, screen turns off after the counter or even never turns on.

    Most likely cause: HomeAssistant publishes the value true directly (a JSON primitive), not a JSON object like {"presence": true}.
    The module currently expects an object and reads the field onfigured in mqttPayloadOccupancyField (default: “presence”). JSON.parse("true") succeeds and returns the boolean true, but true["presence"] is undefined — which evaluates to no presence.
    No exception, no parse error log, just silent false.

    Quick way to verify what HA actually sends:

    mosquitto_sub -h 192.168.4.160 -t sensor/presence -v
    

    That’ll print one line per message. You’ll see exactly what arrives.
    If the payload turns out to be a bare value (just true, "ON", etc.), there are two paths forward:

    Either:
    1. Update the module — I just pushed support for bare-string payloads exactly because of this case:

    cd ~/MagicMirror/modules/MMM-PresenceScreenControl
    git pull
    

    Then add to your module config:

    mqttPayloadOn: "true"
    

    (or whatever HA actually publishes — "ON", "on", etc.; check with the mosquitto_sub command above). With this set, the module compares the raw MQTT payload exactly against your string. Match → presence detected. Anything else → no presence.

    Or:
    2. Configure HA to publish a JSON object like {"presence": true} / {"presence": false} instead of the bare value (e.g. via the payload template of your MQTT publish action). Then your existing config works unchanged.

    The new release also adds two diagnostic improvements that would have made this immediately visible:

    • A new [MQTT] received (field/bare mode): mqttPresence=true/false log line on every message (debug level “complex”)
    • The [updatePresence] line now includes mqttPresence= alongside the other sources

    Sorry for the silent-fail behavior in the previous version — that’s been a documentation gap as well as a feature gap. Let me know if my hypothesis turns out to be wrong; if HA publishes something else entirely, the diagnosis would change.

    Good luck.
    Warmest regards,
    Ralf

  • Calendar events broadcasting, nothing showing...
    0|run8080  | [2026-05-27 08:48:46.741] [DEBUG] [calendar] title: Winlink Wednesday Zoom class
    0|run8080  | [2026-05-27 08:48:46.741] [DEBUG] [calendar] Event: Winlink Wednesday Zoom class | start: Wed Sep 16 2020 20:30:00 GMT-0700 (Pacific Daylight Time) | end: Wed Sep 16 2020 21:30:00 GMT-0700 (Pacific Daylight Time) | recurring: true
    0|run8080  | [2026-05-27 08:48:46.743] [INFO]  [calendar] Broadcasting 165 events from https://calendar.google.com/calendar/ical/b7jirihj85d3klbcvja8im40fk%40group.calendar.google.com/public/basic.ics.
    

    Running newest Magic Mirror, installed April 27, using Sam’s scripts.

    I’m getting broadcasts as you can see, but default calendar and all modules that depend on it show nothing.

    any clue what’s going on?

  • RE: Problems with KristjanESPERANTO/MMM-PublicTransportHafas

    @wimthoelke
    Please update to the new version (v4.4.3). Your stationID works for me.
    If this doesn’t work: use the profile dbweb.
    If this doesn’t work, checkout this module: MMM-PublicTransportHub

  • RE: Problems with KristjanESPERANTO/MMM-PublicTransportHafas

    @Volkae said:
    everything worked fine, but since two days it does not work anymore. It only signs “Abfahrtsschnittstelle nicht erreichbar”.

    There have been some recent issues with certain APIs (we use different ones depending on the profile). Tell me which profile you’re using and which station and I’ll check. If you don’t want to share the station here, you can also send it to me via private message.

  • RE: MMM-AFL – Australian Football League Module

    @joelueng

    I’m not australian but il love it ! Its a good initiative !

    e7075517-9824-428b-bc9f-ad36784815b2-image.jpeg
    In your example its missing an “space” between dates an team. like - Recent Results

    If it can help you can inspire you from this module https://github.com/fewieden/MMM-soccer

  • RE: MMM-Bambulink: MagicMirror Module for Bambu Lab Printers

    0.2 Graphical module done !
    Hey question how i can promote my module now ?

  • RE: MMM-FRITZ-Box-Callmonitor-py3 + MMM-Callmonitor-Current-Call Window

    Dear @wuermchen - you’re welcome.

    Great that you’ve identified some additional issues and now have a working solution!
    Congratulations!

    Warm regards,
    Ralf

  • RE: MMM-FRITZ-Box-Callmonitor-py3 + MMM-Callmonitor-Current-Call Window

    Dear @wuermchen ,
    as I read that you’ve already tried to solve this - may a more concrete description is useful for you:

    Repo: https://github.com/xIExodusIx/MMM-FRITZ-Box-Callmonitor-py3
    Patched file: node_helper.js (only this one file is changed)
    Frontend file: unchanged

    Problem

    Numbers blocked via the FRITZ!Box “number range” blocklist (Rufnummern-Bereichsliste, not phonebook-blocked) cause the FRITZ!Box to emit RING and DISCONNECT events within a few milliseconds of each other.

    The original module fires:

    • SHOW_ALERT immediately on inbound
    • HIDE_ALERT immediately on disconnected

    Both DOM updates happen within the same render frame → browser crashes.

    Phonebook-blocked numbers are handled separately (the library emits a dedicated blocked event) and are not affected by this issue.


    Fix concept

    Don’t fire SHOW_ALERT immediately. Buffer the inbound notification for SPAM_SUPPRESS_WINDOW_MS (default: 1500 ms). If a disconnected event for the same call.id arrives within that window → drop the call entirely. No SHOW_ALERT, no HIDE_ALERT, no DOM churn.

    Real calls (ring duration > 1.5 s, or accepted within 1.5 s) work exactly as before — the only difference is the pop-up appears with a ~1.5 s delay, which is invisible in practice.


    Behavior matrix

    Scenario Behavior
    Real call, rings > 1.5 s Pop-up appears with ~1.5 s delay (imperceptible)
    Real call, accepted within 1.5 s connected handler flushes the timer, fires CALL_CONNECTED
    Spam call (RING + DISCONNECT < 1.5 s) Timer is cancelled, no notification at all → no crash
    Phonebook-blocked number Unchanged — library emits dedicated blocked event
    Outbound call Unchanged — separate event path, Map is never touched

    Changes (all in node_helper.js)

    # Location Action
    1 After line 11 (after require imports) Add constant SPAM_SUPPRESS_WINDOW_MS = 1500
    2 start: (line 23–29) Add one line: this.pendingInbound = new Map();
    3 monitor.on("inbound", ...) (line 82–87) Replace body — instead of firing sendSocketNotification immediately, store a timer in the Map
    4 monitor.on("connected", ...) (line 105–110) Add timer-cleanup at the start, rest unchanged
    5 monitor.on("disconnected", ...) (line 113–117) Add timer-check at the start — if pending → clearTimeout + return

    All changes are marked with // CHANGES for wuermchen comments in the file.


    Tuning

    SPAM_SUPPRESS_WINDOW_MS = 1500 is a safe default.

    • Very fast hardware/network: 800–1000 ms is enough
    • Slow setups: bump to 2000 ms
    • For a runtime config option, expose it as this.config.suppressWindow and read in setupMonitor

    Caveats / things to verify in your setup

    1. call.id must exist on the library’s event objects.
      The library node-fritzbox-callmonitor derives it from the FRITZ!Box stream’s ConnectionId. Should work, but if your installed library version uses a different property name (e.g. call.connectionId), adjust accordingly. Easy to verify with console.log(call) in the inbound handler.

    2. Assumption: blocklist-range calls emit inbound + disconnected.
      This matches the forum description (RING + DISCONNECT). If your specific FRITZ!Box firmware emits something different (e.g. directly blocked), the patch won’t help — but won’t break anything either, because the existing blocked path is untouched.

    3. Side effect: no RELOAD_CALLS for suppressed spam calls.
      The original disconnected handler triggers a refresh of the call list from the FRITZ!Box API. Suppressed spam calls won’t trigger this refresh — the on-screen list updates only at the next real event. Acceptable in practice (spam calls are in the FRITZ!Box history anyway).


    Installation

    cd ~/MagicMirror/modules/MMM-FRITZ-Box-Callmonitor-py3
    cp node_helper.js node_helper.js.orig           # backup
    # replace node_helper.js with the patched version
    touch ~/MagicMirror/config/config.js            # graceful restart via pm2 file-watch
    

    Rollback:

    cd ~/MagicMirror/modules/MMM-FRITZ-Box-Callmonitor-py3
    cp node_helper.js.orig node_helper.js
    touch ~/MagicMirror/config/config.js
    

    Complexity assessment

    Structurally simple — one file, one function (setupMonitor), ~15 new lines. No race conditions (Node.js is single-threaded). No memory leak (timer + Map entry are explicitly cleaned up in every code path).

    The uncertainty is not in the code, but in the two library assumptions above (point 1 and 2 under Caveats). The fix has not been verified against a live spam call yet — it is derived from the forum description and the library’s documented event model.

    complete patched node_helper.js:
    (You will find all changes marked with “// CHANGES for wuermchen — Spam-Call Suppression”

    "use strict";
    
    const NodeHelper = require("node_helper");
    const CallMonitor = require("node-fritzbox-callmonitor");
    const vcard = require("vcard-json");
    const phoneFormatter = require("phone-formatter");
    const xml2js = require("xml2js");
    const moment = require('moment');
    const exec = require('child_process').exec;
    const {PythonShell} = require('python-shell');
    const path = require("path");
    
    // ============================================================================
    // CHANGES for wuermchen — Spam-Call Suppression
    // ----------------------------------------------------------------------------
    // Background: FRITZ!Box "number range" blocklist entries trigger RING and
    // DISCONNECT almost simultaneously. The original code fires SHOW_ALERT on
    // "inbound" and HIDE_ALERT on "disconnected" immediately, causing two
    // overlapping DOM updates within a few ms → browser crashes.
    //
    // Fix: buffer "inbound" notifications for SPAM_SUPPRESS_WINDOW_MS. If a
    // "disconnected" with the same call.id arrives before the timer fires, drop
    // the call entirely (no SHOW_ALERT, no HIDE_ALERT). Real calls (ring > 1.5 s
    // or accepted) work as before, just with ~1.5 s delay on the pop-up.
    // ============================================================================
    const SPAM_SUPPRESS_WINDOW_MS = 1500;
    // ============================================================================
    
    const CALL_TYPE = Object.freeze({
    	INCOMING: "1",
    	MISSED: "2",
    	OUTGOING: "3",
    	BLOCKED: "10"		//New entry for blocked calls as found on: https://fritzconnection.readthedocs.io/en/1.14.0/sources/library_modules.html
    })
    //outgoing missed calls are not in the list
    
    module.exports = NodeHelper.create({
    	// Subclass start method.
    	start: function () {
    		this.ownNumbers = []
    		this.started = false;
    		//create addressbook dictionary
    		this.AddressBook = {};
    		// CHANGES for wuermchen — pending inbound calls awaiting suppression decision
    		this.pendingInbound = new Map();   // call.id -> timer
    		console.log("Starting module: " + this.name);
    	},
    
    	normalizePhoneNumber(number) {
    		return phoneFormatter.normalize(number.replace(/\s/g, ""));
    	},
    
    	getName: function (number) {
    		//Normalize number
    		var number_formatted = this.normalizePhoneNumber(number);
    		//Check if number is in AdressBook if yes return the name
    		if (number_formatted in this.AddressBook) {
    			return this.AddressBook[number_formatted];
    		} else {
    			//Not in AdressBook return original number
    			return number;
    		}
    	},
    
    	socketNotificationReceived: function (notification, payload) {
    		//Received config from client
    		if (notification === "CONFIG") {
    			//set config to config send by client
    			this.config = payload;
    			//if monitor has not been started before (makes sure it does not get started again if the web interface is reloaded)
    			if (!this.started) {
    				//set started to true, so it won't start again
    				this.started = true;
    				console.log("Received config for " + this.name);
    
    				this.parseVcardFile();
    				this.setupMonitor();
    			}
    			//send fresh data to front end (page might have been refreshed)
    			if (this.config.password !== "") {
    				this.loadDataFromAPI();
    			}
    		}
    		if (notification === "RELOAD_CALLS") {
    			this.loadDataFromAPI("--calls-only");
    		}
    		if (notification === "RELOAD_CONTACTS") {
    			this.loadDataFromAPI("--contacts-only");
    		}
    	},
    
    	setupMonitor: function () {
    		//helper variable so that the module-this is available inside our callbacks
    		var self = this;
    
    		//Set up CallMonitor with config received from client
    		var monitor = new CallMonitor(this.config.fritzIP, this.config.fritzPort);
    
    		// ====================================================================
    		// CHANGES for wuermchen — buffer inbound instead of firing immediately
    		// ====================================================================
    		monitor.on("inbound", function (call) {
    			//If caller is not empty
    			if (call.caller == "") return;
    			var payload = self.getName(call.caller);
    			var timer = setTimeout(function () {
    				self.pendingInbound.delete(call.id);
    				self.sendSocketNotification("call", payload);
    			}, SPAM_SUPPRESS_WINDOW_MS);
    			self.pendingInbound.set(call.id, timer);
    		});
    		// ====================================================================
    
    		monitor.on("outbound", function (call) {
    			//Save own number (call.caller) to ownNumbers Array to distinguish inbound/outbound on "connected" handler
    			if (!self.ownNumbers.includes(call.caller))
    				self.ownNumbers.push(call.caller)
    				self.sendSocketNotification("outbound", call.called);
    
    		});
    
    		//Call blocked
    		monitor.on("blocked", function (call) {
    				var name = call.type === "blocked" ? self.getName(call.called) : self.getName(call.caller);
    				//send clear command to interface
    				self.sendSocketNotification("blocked", self.getName(call.caller)); //{ "caller": name, "duration": call.duration });
    		});
    
    		//Call accepted
    		monitor.on("connected", function (call) {
    			// CHANGES for wuermchen — flush pending inbound timer before firing connected
    			var pending = self.pendingInbound.get(call.id);
    			if (pending) {
    				clearTimeout(pending);
    				self.pendingInbound.delete(call.id);
    			}
    			var name = self.ownNumbers.includes(call.caller) ? self.getName(call.called) : self.getName(call.caller);
    			var direction = self.ownNumbers.includes(call.caller) ? "out" : "in";
    			self.sendSocketNotification("connected", { "caller": name, "direction": direction });
    
    		});
    
    		//Caller disconnected
    		monitor.on("disconnected", function (call) {
    			// CHANGES for wuermchen — if inbound was still buffered, this is a spam call: suppress both
    			var pending = self.pendingInbound.get(call.id);
    			if (pending) {
    				clearTimeout(pending);
    				self.pendingInbound.delete(call.id);
    				return;   // never showed the pop-up, no need to clear it
    			}
    			var name = call.type === 'outbound' ? self.getName(call.called) : self.getName(call.caller);
    			//send clear command to interface
    			self.sendSocketNotification("disconnected", { "caller": name, "duration": call.duration });
    		});
    		console.log(this.name + " is waiting for incoming calls.");
    	},
    
    	parseVcardFile: function () {
    		var self = this;
    
    		if (!this.config.vCard) {
    			return;
    		}
    		vcard.parseVcardFile(self.config.vCard, function (err, data) {
    			//In case there is an error reading the vcard file
    			if (err) {
    				self.sendSocketNotification("error", "vcf_parse_error");
    				if (self.config.debug) {
    					console.log("[" + self.name + "] error while parsing vCard " + err);
    				}
    				return
    			}
    
    			//For each contact in vcf file
    			for (var i = 0; i < data.length; i++) {
    				//For each phone number in contact
    				for (var a = 0; a < data[i].phone.length; a++) {
    					//normalize and add to AddressBook
    					self.AddressBook[self.normalizePhoneNumber(data[i].phone[a].value)] = data[i].fullname;
    				}
    			}
    			self.sendSocketNotification("contacts_loaded", Object.keys(self.AddressBook).length);
    		});
    	},
    
    	loadCallList: function (body) {
    		var self = this;
    
    		xml2js.parseString(body, function (err, result) {
    			if (err) {
    				self.sendSocketNotification("error", "calllist_parse_error");
    				console.error(self.name + " error while parsing call list: " + err);
    				return;
    			}
    			var callArray = result.root.Call;
    			var callHistory = []
    
    			for (var index in callArray) {
    				var call = callArray[index];
    				var type = call.Type[0];
    				//Trying to handle blocked calls these lines 164-171 are new from "if to else" and it works!
    				if  (name = type == CALL_TYPE.BLOCKED || type == CALL_TYPE.INCOMING ? self.getName(call.Caller[0]) : self.getName(call.Called[0]));
    					var duration = call.Duration[0];
    					if (type == CALL_TYPE.INCOMING && self.config.deviceFilter && self.config.deviceFilter.indexOf(call.Device[0]) > -1) {
    						continue;
    				}
    				else
    				//From here the original script is ongoing
    				var name = type == CALL_TYPE.MISSED || type == CALL_TYPE.INCOMING ? self.getName(call.Caller[0]) : self.getName(call.Called[0]);
    				var duration = call.Duration[0];
    					if (type == CALL_TYPE.INCOMING && self.config.deviceFilter && self.config.deviceFilter.indexOf(call.Device[0]) > -1) {
    						continue;
    				}
    				var callInfo = { "time": moment(call.Date[0], "DD.MM.YY HH:mm"), "caller": name, "type": type, "duration": duration };
    				if (call.Name[0]) {
    					callInfo.caller = call.Name[0];
    				}
    				callHistory.push(callInfo)
    		}
    			self.sendSocketNotification("call_history", callHistory);
    		});
    	},
    
    	loadPhonebook: function (body) {
    		var self = this;
    
    		xml2js.parseString(body, function (err, result) {
    			if (err) {
    				self.sendSocketNotification("error", "phonebook_parse_error");
    				if (self.config.debug) {
    					console.error(self.name + " error while parsing phonebook: " + err);
    				}
    				return;
    			}
    			var contactsArray = result.phonebooks.phonebook[0].contact;
    			for (var index in contactsArray) {
    				var contact = contactsArray[index];
    				var contactNumbers = contact.telephony[0].number;
    				var contactName = contact.person[0].realName;
    
    				for (var index in contactNumbers) {
    					var currentNumber = self.normalizePhoneNumber(contactNumbers[index]._);
    					self.AddressBook[currentNumber] = contactName[0];
    				}
    			}
    			self.sendSocketNotification("contacts_loaded", Object.keys(self.AddressBook).length);
    		});
    	},
    
    	loadDataFromAPI: function (additionalOption) {
    		var self = this;
    
    		if (self.config.debug) {
    			console.log('Starting access to FRITZ!Box...');
    		}
    
    		let args = ['-i', self.config.fritzIP, '-p', self.config.password];
    		if (self.config.username !== "") {
    			args.push('-u');
    			args.push(self.config.username);
    		}
    		if (additionalOption) {
    			args.push(additionalOption);
    		}
    
    		let options = {
    			pythonPath: 'python3',
    			mode: 'json',
    			scriptPath: path.resolve(__dirname),
    			args: args
    		};
    
    		let pyshell = new PythonShell('fritz_access.py', options);
    
    		pyshell.on('message', function (message) {
    			if (message.filename.indexOf("calls") !== -1) {
    				//Call list file
    				self.loadCallList(message.content);
    			} else {
    				//Phone book file
    				self.loadPhonebook(message.content);
    			}
    		});
    
    		//End the input stream and allow the process to exit
    		pyshell.end(function (error) {
    			if (error) {
    				var errorUnknown = true;
    				if (error.traceback.indexOf("XMLSyntaxError") !== -1) {
    					//Password is probably wrong
    					self.sendSocketNotification("error", "login_error");
    					errorUnknown = false;
    				}
    				if (error.traceback.indexOf("failed to load external entity") !== -1) {
    					//Probably no network connection
    					self.sendSocketNotification("error", "network_error");
    					errorUnknown = false;
    				}
    				if (errorUnknown) {
    					self.sendSocketNotification("error", "unknown_error");
    				}
    				if (self.config.debug) {
    					console.error(self.name + " error while accessing FRITZ!Box: ");
    					console.error(error.traceback);
    				}
    				return;
    			}
    			if (self.config.debug) {
    				console.log('Access to FRITZ!Box finished.');
    			}
    		});
    	}
    });
    
    
  • RE: MMM-FRITZ-Box-Callmonitor-py3 + MMM-Callmonitor-Current-Call Window

    dear @wuermchen,

    this should be a classic race condition and should be solvable in node_helper.js without much effort.

    Background: The FRITZ!Box call monitor (port 1012) tags every event of a call with the same ConnectionId:

    date;RING;<id>;<caller>;<callee>;...
    date;DISCONNECT;<id>;<duration>;
    

    For numbers blocked by the FRITZ!Box, RING and DISCONNECT arrive within a few milliseconds. The module currently fires the notification immediately
    on RING — that’s why you end up with two overlapping pop-ups and the browser crashes.

    Fix: Don’t propagate RING immediately. Buffer it briefly, and if a DISCONNECT for the same ConnectionId arrives within a short window, drop the
    event entirely.

    Rough sketch for node_helper.js:

    const SUPPRESS_WINDOW_MS = 1500;
    const pendingRings = new Map();   // connectionId -> timer
    
    // on RING:
    const timer = setTimeout(() => {
      pendingRings.delete(connectionId);
      this.sendSocketNotification("call", ringPayload);   // original notify
    }, SUPPRESS_WINDOW_MS);
    pendingRings.set(connectionId, timer);
    
    // on DISCONNECT:
    const timer = pendingRings.get(connectionId);
    if (timer) {
      clearTimeout(timer);
      pendingRings.delete(connectionId);
      return;   // spam call suppressed, no notification at all
    }
    // otherwise: handle DISCONNECT as before
    

    Why this works:

    • Only suppresses calls that terminate almost instantly (FRITZ!Box block list, “do not disturb”)
    • Regular missed calls (phone rings 10 s, nobody answers) still trigger the pop-up — just delayed by ~1.5 s, which is invisible in practice
    • No second pop-up, no browser crash

    Tuning: 1500 ms is a safe default. You can lower it to 500–800 ms if you want faster feedback, or expose it as a config option

    suppressWindow: 1500,
    

    Hope that helps.
    Warmest regards,

    Ralf

  • MMM-Bambulink: MagicMirror Module for Bambu Lab Printers

    Bambulink Module for MagicMirror

    Module: MMM-Bambulink

    This MagicMirror module,

    MMM-Bambulink is a third-party module for MagicMirror² designed to display information about your Bambu Lab printer directly on your MagicMirror screen. The module connects to your printer over the local network using Bambu Lab’s LAN credentials and updates information automatically at a configurable interval.

    Important: ONLY LAN mode NOT must be enabled on the printer.

    Example-1.png

    Requirements

    To use this module, you need the following information:

    • The IP address of your Bambu Lab printer
    • The printer’s LAN Access Code
    • The printer serial number

    These three values are required in the configuration to establish the connection with your printer. MagicMirror² documentation recommends clearly listing required external dependencies and setup information in a module README.

    Key Features

    • Monitor your Bambu Lab printer directly from MagicMirror².
    • Secure local connection using MQTT/TLS on port 8883.
    • Configurable refresh interval for automatic updates.
    • Custom printer title using the printerModel option.
    • Two temperature display modes:
      • tile view (tiles)
      • graph view (graph)
    • Visual AMS display including:
      • AMS temperature,
      • filament color,
      • loaded filament type for each slot.
    • Automatic highlight of the active AMS slot during printing, including when the printer switches filament. Bambu’s AMS workflow is built around slot-based filament selection and color mapping, which makes this type of visual highlighting consistent with how AMS data is handled in local workflows.
    • Simple installation inside the standard MagicMirror² module structure.

    Development Status

    The module is functional and ready to use in a standard MagicMirror² environment. Installation, Node.js dependencies, configuration, and update steps follow the usual module workflow inside ~/MagicMirror/modules, which is the standard location for third-party MagicMirror² modules.

    The project is still evolving, especially in terms of UI improvements, advanced display options, and visual customization.

    Compatibility

    MMM-Bambulink is designed for MagicMirror² and Bambu Lab printers with LAN mode enabled.

    The required parameters such as ip, accessCode, serial, mqttPort, and useTLS indicate that the module is intended to communicate with the printer locally through a secure network connection.

    Installation

    Go to your MagicMirror modules directory:

    cd ~/MagicMirror/modules
    

    Clone the repository:

    git clone https://github.com/TAGinside/MMM-Bambulink
    

    Enter the module directory:

    cd MMM-Bambulink
    

    Install the Node.js dependencies:

    npm install
    

    MagicMirror’s module guides recommend keeping installation steps simple and directly copyable from the README.

    Configuration

    Add the module to the modules array in your config/config.js file:

    modules: [
      {
        module: "MMM-Bambulink",
        position: "top_left",
        config: {
          ip: "192.168.1.x",                // Printer IP address
          accessCode: "xxxxxxxx",           // LAN Access Code
          serial: "XXXXXXXX",               // Printer serial number
          updateInterval: 5000,             // Refresh interval in ms
    
          printerModel: "H2S",              // Displayed printer name or model
          temperatureDisplayMode: "tiles",  // "tiles" or "graph"
    
          display: {
            scale: 1,
            width: 320,
            graphMinutes: 1
          },
    
          temperatureColors: {
            nozzle: "#ff4d4f",              // Red
            bed: "#ff9f1a",                 // Orange
            chamber: "#4da3ff"              // Blue
          }
        }
      }
    ]
    

    MagicMirror² modules are configured through config/config.js, where each module is added to the modules array with its own config object. [web:134][web:267]

    How to Use

    After adding the module to config/config.js, restart MagicMirror².

    Once running, the module will display your Bambu Lab printer information in the selected MagicMirror position, such as top_left, using the temperature display mode you configured.

    Update

    To update the module:

    cd ~/MagicMirror/modules/MMM-Bambulink
    git pull
    npm ci
    

    Then restart MagicMirror².

    Use Cases

    MMM-Bambulink is especially useful in:

    • a workshop,
    • a technical office,
    • a home lab,
    • a smart home dashboard centered around MagicMirror².

    It allows you to quickly monitor an active print, confirm that the printer is responding, and keep a live visual overview of the current job without constantly opening Bambu Studio.

    About

    MMM-Bambulink brings Bambu Lab printer monitoring directly into MagicMirror², making it possible to integrate 3D printing status into a broader smart home, workshop, or technical dashboard.

    The module is open source and free to use and modify. For more information, updates, or contributions, visit the GitHub repository:

    https://github.com/TAGinside/MMM-Bambulink

    Feedback is always welcome, especially for printer compatibility, AMS behavior, and display improvements.