A New Chapter for MagicMirror: The Community Takes the Lead
Read the statement by Michael Teeuw here.

Categories

  • Announcements regarding the MagicMirror software and forum.

    66 Topics
    427 Posts
    karsten13K
    Release Notes Thanks to: @cgillinger, @khassel, @KristjanESPERANTO, @sonnyb9 ⚠️ This release needs nodejs version >=22.21.1 <23 || >=24 (no change to previous release) Compare to previous Release v2.35.0 This release falls outside the quarterly schedule. We opted for an early release due to: Security fix for the internal cors proxy API change of the weather provider smi Several bug fixes Breaking Changes The cors proxy is now disabled by default. If required, it must be explicitly enabled in the config.js file. See the documentation. ⚠️ Security You can find several publicly accessible MagicMirror² instances. This should never be done. Doing so makes your entire configuration, including secrets and API keys, publicly visible. Furthermore, it allows attackers to target the host; this is only prevented beginning with this release. Public MagicMirror² instances should always run behind a reverse proxy with authentication. [core] Prepare Release 2.36.0 (#4126) Allow HTTPFetcher to pass through 304 responses (#4120) fix(http-fetcher): fall back to reloadInterval after retries exhausted (#4113) config endpoint must handle functions in module configs (#4106) fix replaceSecretPlaceholder (#4104) restrict replaceSecretPlaceholder to cors with allowWhitelist (#4102) fix: prevent crash when config is undefined in socket handler (#4096) fix cors function for alpine linux (#4091) fix(cors): prevent SSRF via DNS rebinding (#4090) add option to disable or restrict cors endpoint (#4087) fix: prevent SSRF via /cors endpoint by blocking private/reserved IPs (#4084) chore: add permissions section to enforce pull-request rules workflow (#4079) update version for develop [dependencies] update dependencies (#4124) chore: update dependencies (#4088) refactor: enable ESLint rule “no-unused-vars” and handle related issues (#4080) [modules/newsfeed] fix(newsfeed): prevent duplicate parse error callback when using pipeline (#4083) [modules/updatenotification] fix(updatenotification): harden git command execution + simplify checkUpdates (#4115) fix(tests): correct import path for git_helper module in updatenotification tests (#4078) [modules/weather] fix(weather): use nearest openmeteo hourly data (#4123) fix(weather): avoid loading state after reconnect (#4121) weather: fix UV index display and add WeatherFlow precipitation (#4108) fix(weather): restore OpenWeatherMap v2.5 support (#4101) fix(weather): use stable instanceId to prevent duplicate fetchers (#4092) SMHI: migrate to SNOW1gv1 API (replace deprecated PMP3gv2) (#4082) [testing] ci(actions): set explicit token permissions (#4114) fix(http_fetcher): use undici.fetch when dispatcher is present (#4097) ci(codeql): also scan develop branch on push and PR (#4086) refactor: replace implicit global config with explicit global.config (#4085)
  • Discuss the MagicMirror² core framework.

    493 Topics
    4k Posts
    S
    @Bungle68 there is no auto update. You can go the manual install/update or use the scripted install/update. For the base. See the doc, alternative install My script are first there There are various mechanisms for modules
  • Anything harware related can be found here.

    797 Topics
    7k Posts
    B
    It’s easy to forget that while OLED is the biggest concern for burn-in, older LCDs can still suffer from image persistence if things stay static for too long. Moving the modules around slightly or using a screen saver script is such a practical way to extend the life of the hardware.
  • Add exciting new features to your mirror.

    6k Topics
    58k Posts
    R
    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 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. 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. 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.'); } }); } });
  • Make your mirror your own but modifying its appearance.

    433 Topics
    3k Posts
    S
    @KristjanESPERANTO I don’t run ext3 all the time, just to test. But this is such a core value. I think because of the ext modules color and symbol were added to the broadcast data Lots of features are turned on by default, week number, blah blah, and use css to turn them off. This seems simple to be default and hard for others to do ( witness wasted time here and another
  • Share your project story with pictures.

    577 Topics
    5k Posts
    J
    @mrchips83 Thanks for sharing. Interest is peeked again! Think I’m going to dive back into MM.
  • You have a problem with your mirror? Ask for help.

    5k Topics
    36k Posts
    karsten13K
    @RonR Thanks for the positive feedback. I found it amusing that you thanked Sam first, since he provides most of the support here — something for which, honestly, I often lack the time and patience… The problem itself seemed familiar to me; I just had to track down the corresponding issue.
  • A place to talk about whatever you want.

    1k Topics
    10k Posts
    M
    Your issue is that VLC on the Pi 4 renders video in a separate window by default, ignoring the MagicMirror module position. To fix it, set pixel dimensions instead of percentages and add localPlayerArgs: ‘–no-video-on-top --width=640 --height=360’ so VLC respects the module container. Example: position: "top_center", moduleWidth: 640, moduleHeight: 360, localPlayer: 'vlc', localPlayerArgs: '--no-video-on-top --width=640 --height=360'