Dear Kristjan, @KristjanESPERANTO ,
Thanks for this hint!
Wasn’t aware that forks obviously need this.
For sure enabled right now for all of my module forks.
Warm regards,
Ralf
Dear Kristjan, @KristjanESPERANTO ,
Thanks for this hint!
Wasn’t aware that forks obviously need this.
For sure enabled right now for all of my module forks.
Warm regards,
Ralf
Dear @soldatino,
very (!) impressive!
Not really MY usecase because of my explicite approach to use my mirror as a viewing port but really nice.
To my big surprise I found your fifth screen with Bring! integration and was therefore inspired to try this again (longer time ago for some strange reasons the module didn’t work on my installation and i stopped trials around that, assuming that +six years old code doesn’t work at all).
With your awesome setup I gave it a second try - and it worked.
But: It throws severe security issues and in addition it doesn’t work exactly as desired (by me).
That’s the reason why I forked & refactured the original module.
You can check the whole story behind my approach in this post - if you like.
I would like to say “thanks” to you for this “kick” and inspiration.
Warm regards,
Ralf
Hi everyone,
I’ve tried a longer time ago without success - recently I gave it a second try and surprisingly it’s “working” right now: David Werth’s MMM-Bring.
It is a lovely little module and I want to be clear up front that all the original credit goes to him. The trouble is that it has not seen any maintenance in roughly six years (the original dates back to 2019), and my recent npm install started throwing security warnings at me.
Rather than let it bit-rot, I reworked it fairly thoroughly and published the result as a fork: https://github.com/rkorell/MMM-Bring.
I thought it might be useful to others here, so here is what changed and, more importantly, why.
The original module goes back to 2019 and has effectively been dormant since. That is not a criticism — it simply did its job and life moved on. But six years is a long time in Node land: it was written against an older dependency and rendering style, and nobody was around to react when its dependencies started aging out. If you install it today on a current MagicMirror / Node setup, you feel that age immediately (see the next point). My goal with the fork was not to reinvent it, but to bring it back to a state where it installs cleanly, runs on current Node, and can be maintained going forward.
This is what actually pushed me to act. Installing the original pulls in axios ^0.21.2 (released back in 2021) and data-store. On that axios line, npm audit reports two high-severity advisories — credential leakage across an HTTP-to-HTTPS redirect, and a prototype-pollution issue — plus a transitive follow-redirects advisory. For a module that authenticates with your Bring! account, shipping a known-vulnerable HTTP client is not great.
Instead of merely bumping versions, I removed both dependencies entirely. The fork now has zero runtime dependencies. All HTTP goes through Node’s built-in fetch (Node 18+), and the auth-token cache is a tiny fs-based JSON file that replaces data-store. The upshot: npm audit is clean, there is nothing to compile, and there is no npm install step at all anymore — you just clone it. Fewer moving parts, no native build, and nothing that can rot silently in the background.
This is the part I am most happy with. The original shows the list in whatever raw order the API returns. The Bring! app, however, groups items into categories (Fruits & Vegetables, Milk & Cheese, Meat & Fish, …) and sorts those categories in an order you can customise per list. I worked out how the app reconstructs that and reproduced it faithfully.
There is a new option, useSections, with three modes:
"off" — the classic flat list in raw API order (the old behaviour)."on" (the default) — items sorted into your list’s category order as one continuous list, without visible headers. Same layout footprint as before, just in the right order."show" — the same sorting, plus a text header per category, exactly like the app.The grouping is genuinely app-identical rather than a guess: it follows your list’s own saved section order (listSectionOrder), honours the sections you have hidden, and shows the localized category names and item names for your list’s language. Items you typed that are not in Bring!'s catalog land in an “own items” section — and even that fallback label is localized (13 language files covering all 20 Bring! locales). Under the hood the canonical item id is kept separate from the translated display name, so marking things bought and adding items keeps working regardless of language. There are screenshots of all three modes in the README if you want to see the difference.
showCount. An optional count in the title line when the list is longer than maxItems, so you can see there is more than what is shown.cd ~/MagicMirror/modules
git clone https://github.com/rkorell/MMM-Bring.git
That is it — no npm install. Then add the module block to your config.js (there is a full options table in the README). If you already run the original module, you can simply replace the folder; your config.js entry stays the same. Step-by-step migration notes are in the README.
All credit for the original module goes to David Werth (https://github.com/werthdavid/MMM-Bring). The API-interaction design was inspired by miaucl/bring-api (https://github.com/miaucl/bring-api), the well-maintained Python client, as a reference for a current, state-of-the-art Bring! implementation.
Standard caveat: Bring! offers no official public API, so this — like every Bring! integration — talks to the same private endpoints the app uses. They could change at any time. It has been stable for years, but be aware of that.
Repo, README and changelog: https://github.com/rkorell/MMM-Bring. Feedback, issues and pull requests are very welcome.
Hope you will find it useful.
Warmest regards,
Ralf
@thartley cool.
Happy that it works.
Have fun!
Warm regards,
Ralf
@thartley
The text size of the newsfeed description is controlled via the global CSS class .small which the template assigns to the element.
Defined in ~/MagicMirror/css/main.css:
.small { font-size: var(--font-size-small); }
If I see it right::
If you want to change the description size independently of other .small elements , the cleanest approach would be an override in css/custom.css:
.newsfeed .newsfeed-desc { font-size: your_desired_font_size_as_number)
(do not alter the global .small).
HTH.
Warm regards,
Ralf
Dear @KristjanESPERANTO ,
thanks :-)
Good idea - I’ve added a screenshot right now.
(closed a documentation error regarding first / re-authorization in parallel).
Warm regards,
Ralf
Hi all,
if you use MMM-NowPlayingOnSpotify (raywo’s lovely “now playing” module), there’s a
change coming from Spotify that will quietly break it — and I wanted to share both the
problem and a fix.
The problem
Spotify announced that, starting 20 July 2026, user refresh tokens will expire after 6
months (blog (https://developer.spotify.com/blog/2026-06-18-refresh-token-expiration)).
Until now those tokens were effectively permanent, so the module just stored one and
kept minting access tokens forever. Once a refresh token expires, Spotify returns an
invalid_grant error, and the app is expected to discard it and send the user through
the sign-in flow again.
The original module doesn’t handle that case: it logs the error and silently falls back
to the “nothing is playing” logo. So one day your mirror would just… stop showing
songs, with no hint why — while hammering the API once per second with a dead token.
First attempt: the polite way
I opened an issue on the original repo to flag it. Unfortunately the project has been
unmaintained for ~6 years, and there was no response. So rather than let everyone’s
module quietly die this summer, I forked it and did the work.
The fix: a fork with proper token handling
👉 https://github.com/rkorell/MMM-NowPlayingOnSpotify (v2.0.0)
What it does now:
One wrinkle worth knowing (loopback + SSH tunnel)
Spotify only accepts http redirect URIs for loopback addresses now — a LAN IP or
hostname over http is rejected at runtime as insecure, and localhost isn’t allowed
either. So the redirect URI is http://127.0.0.1:8888/callback. Since a mirror is
headless, the easiest way to re-authorize from your laptop is a one-line SSH tunnel:
ssh -L 8888:127.0.0.1:8888 pi@<mirror-ip>
…then open http://127.0.0.1:8888 in your laptop’s browser. Full details are in the
README.
Happy to help anyone getting it running, and a big thank-you to raywo for the original
module that many of us have enjoyed for years.
@sdetweil , @kristjanesperanto ,
I’ve decided to resolve it by myself - with Sam’s hint/advice/helper-file.
It seems to work - will double check my cache size in a few days.
Thanks again for your great support.
Migration is a bigger task and wihout any need currently too much - even cache-growth wouldn’t change this.
Warm regards,
Ralf
@sdetweil OK, thanks!
Still doesn’t sound THAT safe …
regards,
Ralf
@sdetweil , yes.
I’ve really thought about this.
How do you see the update?
I’m currently on 2.34.
Switch to 2.35 is SUBSTANCIAL !
Will this break my system?
Or am I fine with just using your great update-script?
Thanks for any advice (I KNOW that you cannot provide any guarantee. I’m just interested in gut-feeling…)
(and: if it brake: is there a way back??? )
Warmest regards,
Ralf
Dear Sam, @sdetweil,
thanks to you as well!
As mentioned above: A little bit shy …
Warmest regards,
Ralf
@KristjanESPERANTO
Thanks!
To be honest, I’m hesitating with upgrades …
And pulling develop branches ist defnitely not my favourite way of using my mirror.
I even refused to install the last reelase- caused by the major changes in config.js location …
I will think about, but …
mmmmhhmmmm…
Thanks anyway for your feedback - highly appreciated!
Warmest regards,
Ralf
Dear all,
I have been chasing a runaway Electron disk cache on my Mirror (Pi 5, MM 2.34.0) and added what I thought was the obvious fix to my config.js:
electronSwitches: ["disk-cache-size", "104857600"], // 100 MB
Some months later the cache had grown to 2 GB (260,000+ files in ~/.config/Electron/Cache/Cache_Data/).
My limit appeared to be ignored entirely.
When I looked at js/electron.js line 47 I think I see why, but I want to sanity-check this with the community — maybe I am simply misreading the code.
The line is:
app.commandLine.appendSwitch(...new Set(electronSwitchesDefaults, config.electronSwitches));
As far as I understand it, two things are happening here:
The Set constructor signature is new Set([iterable]) — it accepts exactly one iterable. Any further arguments are ignored. So I believe this is equivalent to:
new Set(electronSwitchesDefaults)
and config.electronSwitches never makes it in.
Is that right?
Or am I missing some clever JavaScript I have not seen before?
The Electron docs state the signature is appendSwitch(switch, value?). Spreading a flat array of multiple switches into a single call should — if I understand correctly — only honour the first switch/value pair and silently discard the rest.
The defaults array ([“autoplay-policy”, “no-user-gesture-required”]) happens to be exactly one switch with one value, so that one works. But anything a user adds via config.electronSwitches would never take effect, even if point 1 were fixed.
If both readings are correct, the natural fix would be a loop, something like:
const switches = [["autoplay-policy", "no-user-gesture-required"], ...(config.electronSwitches || [])];
switches.forEach(([s, v]) => app.commandLine.appendSwitch(s, v));
or, if electronSwitches is documented as a flat array, the user-facing schema would need to be specified more clearly first.
My questions to the forum:
Thanks,
Ralf
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:
sensor/presence[updatePresence] fires (so messages ARE arriving — otherwise you wouldn’t see those entries triggered)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:
[MQTT] received (field/bare mode): mqttPresence=true/false log line on every message (debug level “complex”)[updatePresence] line now includes mqttPresence= alongside the other sourcesSorry 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
Dear @wuermchen - you’re welcome.
Great that you’ve identified some additional issues and now have a working solution!
Congratulations!
Warm regards,
Ralf
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
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 inboundHIDE_ALERT immediately on disconnectedBoth 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.
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.
| 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 |
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.
SPAM_SUPPRESS_WINDOW_MS = 1500 is a safe default.
this.config.suppressWindow and read in setupMonitorcall.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).
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
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.');
}
});
}
});
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:
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-PresenceScreenControl — two new releases (ecoMode + Notification API)
Dear all
a small but meaningful round of updates for MMM-PresenceScreenControl just landed on main. Both started from a single GitHub issue, so I thought I’d write up the chain of reasoning rather than just drop a changelog — maybe useful for anyone evaluating a migration from MMM-Pir, and definitely useful as a reminder to myself to check the parent project’s feature surface more carefully when forking.
Part 1 — ecoMode (Issue #5)
Rocket78 opened issue #5 with a really well-argued request: even with the screen physically off, Electron keeps repainting hidden DOM. On a Pi 3 with X11, MMM-PresenceScreenControl was leaving things like Newsfeed cross-fades running every 20 seconds — and those repaints showed up clearly as CPU spikes in his graph. He asked for an equivalent of MMM-Pir’s ecoMode, which hides every other module while the screen is off, so the browser skips layout/paint/composite for them.
So I built it, but tried to keep it lean:
ecoMode: false, // opt-in, default off → no surprise
ecoModeIgnore: [] // module names to keep visible
Implementation notes that might interest other module authors:
Rocket78 also dropped a great ddcutil snippet in the issue for monitors that show a “no signal” splash when the video output is cut — that splash is genuinely immersion-breaking, and ddcutil setvcp D6 sends the actual DDC/CI power command, which is much closer to pressing the hardware power button:
onCommand: ddcutil setvcp D6 1 --skip-ddc-checks
offCommand: ddcutil setvcp D6 5 --skip-ddc-checks
Added to the README’s offCommand examples. The --skip-ddc-checks is needed because some monitors stop responding to DDC queries when powered off but still process incoming power-on commands — so the check would fail before the command is even sent.
→ Issue #5 closed, commit 42b68a6.
Part 2 — what else did I miss?
Closing the issue could have been the end of it. But Rocket78’s request raised an uncomfortable question: I never used ecoMode in MMM-Pir myself, so I didn’t notice it was missing. What else did I overlook when reviving MMM-Pir?
Three items jumped out as actual gaps that other modules in the ecosystem could legitimately depend on:
MMM_PIR-USER_PRESENCE — MMM-Pir broadcasts this notification on presence transitions. Other modules (Remote-Control, voice assistants, smart-home bridges) can listen for it.
MMM_PIR-WAKEUP / LOCK / UNLOCK / END — incoming notifications that let other modules control the screen logic externally.
MMM_PIR-SCREEN_POWERSTATUS — broadcast when the physical screen turns on or off.
Without these, anyone migrating from MMM-Pir to my fork would suddenly find their automations dead — because they’d be listening for MMM_PIR-USER_PRESENCE and nothing was coming through. Even worse, they wouldn’t know why it stopped working; the screen-on/off behavior would seem fine, but cross-module integration would be silently broken.
So I built a parallel notification API, namespaced MMM_PSC-* rather than impersonating MMM-Pir’s MMM_PIR-*:
Outgoing notifications — emitted on state transitions only:
MMM_PSC-USER_PRESENCE payload: true / false
fires when combined presence changes
MMM_PSC-SCREEN_POWERSTATUS payload: true / false
fires when physical screen turns on/off
Both fire only on actual transitions — no spam every poll cycle.
Incoming notifications — consumed by the module
MMM_PSC-WAKEUP wake screen, reset timer (equivalent to a touch)
MMM_PSC-END force screen off immediately
MMM_PSC-LOCK freeze presence handling — sensors are still tracked internally, but no longer change screen state
MMM_PSC-UNLOCK resume normal presence handling and re-evaluate the current sensor state
Implementation detail worth flagging: the LOCK guard sits in updatePresence(), which is the single funnel through which all sensor inputs (PIR, MQTT, touch, external wakeup socket) eventually pass. So whatever the trigger source, it’s correctly suppressed while locked. UNLOCK calls updatePresence() again, which re-evaluates the current sensor state — so if you’ve unlocked while a person is still in front of the PIR, the screen comes back on immediately. No need for the caller to send a WAKEUP after UNLOCK.
Useful patterns this enables:
// Wake the mirror when a doorbell event arrives:
this.sendNotification("MMM_PSC-WAKEUP");
// Force-off cleanly from outside (smart-home rule, etc.) without
// bypassing the module and going straight to the screen command:
this.sendNotification("MMM_PSC-END");
// Take exclusive control of the display for a video call,
// then hand it back when done:
this.sendNotification("MMM_PSC-LOCK");
// ... your module is showing its full-screen content ...
this.sendNotification("MMM_PSC-UNLOCK");
END is the clean way to force-off the screen from outside without touching offCommand directly — the module’s internal state stays consistent, all the right outgoing notifications still fire, and any other listeners (logging, analytics, smart home) see the transition.
Tested live end-to-end via MMM-Remote-Control’s notification API:
curl -X GET "http://localhost:8080/api/notification/MMM_PSC-LOCK"
curl -X GET "http://localhost:8080/api/notification/MMM_PSC-END"
→ screen off, stays off
curl -X GET "http://localhost:8080/api/notification/MMM_PSC-WAKEUP"
→ ignored (locked)
curl -X GET "http://localhost:8080/api/notification/MMM_PSC-UNLOCK"
→ screen back on
All four routes verified, log trace clean, outgoing notifications fired in the right order on every transition.
→ Commit 10000ca.
Update / install
If you’re already on the module:
cd ~/MagicMirror/modules/MMM-PresenceScreenControl
rm -rf node_modules
git pull
npm install
Both changes are backwards-compatible. ecoMode is opt-in (default false), and the new notifications are additive — nothing existing is modified. So you can update without touching your config and pick up only what you need.
A genuine thank-you to Rocket78 for the well-reasoned issue and the ddcutil tip — it triggered the full audit, which in turn closed a much bigger latent gap.
Exactly the kind of feedback that improves a fork. If you’ve migrated from MMM-Pir and notice anything else missing that you’d consider load-bearing, please open an issue.
Hope you find it useful.
Warmest regards,
Ralf
Good afternoon, all!
europeDiscNat is back! (plus a small CSS fix for a new white footer)
for you, guys a quick update on the European satellite image situation.
Since my fork EUMETSAT’s new WMS endpoint at view.eumetsat.int/geoserver/wms works fast, reliably and nice.
Yesterday that stopped delivering fresh images!
Today I figured out there even is a new version (small changes in URL) v1.1.0 is now v1.3.0 API - but both of them are only returning a stale image from yesterday evening:
https://view.eumetsat.int/geoserver/wms?service=WMS&version=1.1.0&request=GetMap&layers=msg_fes:rgb_naturalenhncd&...
https://view.eumetsat.int/geoserver/wms?service=WMS&version=1.3.0&request=GetMap&layers=msg_fes:rgb_naturalenhncd&...
But here’s the surprise: the old europeDiscNat URL is back and working again!
So if you’ve been in trouble with looking for actual pictures without the SLIDER API, this works right now:
style: 'europeDiscNat',
is re-born …
One thing to watch out for: Unfortunately EUMETSAT has changed the image format slightly. The footer bar at the bottom (logo + timestamp) used to be black — invisible against the MagicMirror background. It’s now white and a bit taller, which causes a small white artifact at the bottom of the globe.
I’ve pushed a fix that adds a black CSS overlay to mask the footer. Just update the module:
cd ~/MagicMirror/modules/MMM-Globe
git pull
npm install
Of course the SLIDER styles (geoColorEurope, geoColorUSA, etc.) continue to work fine if you prefer those.
Happy mirroring!
Warmest regards,
Ralf