MagicMirror Forum
    • Recent
    • Tags
    • Unsolved
    • Solved
    • MagicMirror² Repository
    • Documentation
    • 3rd-Party-Modules
    • Donate
    • Discord
    • Register
    • Login
    A New Chapter for MagicMirror: The Community Takes the Lead
    Read the statement by Michael Teeuw here.

    MMM-FRITZ-Box-Callmonitor-py3 + MMM-Callmonitor-Current-Call Window

    Scheduled Pinned Locked Moved Unsolved Troubleshooting
    8 Posts 2 Posters 455 Views 2 Watching
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • W Offline
      wuermchen
      last edited by

      Hello
      I’m running the modules MMM-FRITZ-Box-Callmonitor-py3 and MMM-Callmonitor-Current-Call. They work well. Unfortunately, there’s an occasional error.
      The caller’s window gets stuck and the background remains grayed out. Can anyone help me?

      R 1 Reply Last reply Reply Quote 0
      • R Offline
        rkorell Module Developer @wuermchen
        last edited by

        @wuermchen which fork are you using?
        There was a newer implementation, I guess last year.
        This version resolved the sticky caller-window (at least for me).

        Good luck,
        Regards,
        Ralf

        1 Reply Last reply Reply Quote 1
        • W Offline
          wuermchen
          last edited by

          I’ve just reinstalled the new version of drtorchwood. Let’s see if the error occurs again.

          W 1 Reply Last reply Reply Quote 0
          • W Offline
            wuermchen @wuermchen
            last edited by

            The error is still there.
            I think the error always occurs with spam calls. Is there a way to hide a specific phonebook?

            W 1 Reply Last reply Reply Quote 0
            • W Offline
              wuermchen @wuermchen
              last edited by

              This post is deleted!
              1 Reply Last reply Reply Quote 0
              • W Offline
                wuermchen
                last edited by

                Hello,
                I am now certain that this issue is related to blocked phone numbers. I am using the following version:
                https://github.com/xIExodusIx/MMM-FRITZ-Box-Callmonitor-py3

                I attempted to modify node_helper.js to prevent these calls from being processed at all, but unfortunately, without success.

                Since the FRITZ!Box sends the RING and DISCONNECT signals almost simultaneously for blocked numbers, the module triggers two notifications to the screen. The browser attempts to render two pop-ups at the same time and subsequently crashes.

                Does anyone have an idea how to solve this problem?
                I would greatly appreciate any input or suggestions.

                R 1 Reply Last reply Reply Quote 0
                • R Offline
                  rkorell Module Developer @wuermchen
                  last edited by

                  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

                  R 1 Reply Last reply Reply Quote 0
                  • R Offline
                    rkorell Module Developer @rkorell
                    last edited by rkorell

                    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.');
                    			}
                    		});
                    	}
                    });
                    
                    
                    1 Reply Last reply Reply Quote 0

                    Hello! It looks like you're interested in this conversation, but you don't have an account yet.

                    Getting fed up of having to scroll through the same posts each visit? When you register for an account, you'll always come back to exactly where you were before, and choose to be notified of new replies (either via email, or push notification). You'll also be able to save bookmarks and upvote posts to show your appreciation to other community members.

                    With your input, this post could be even better 💗

                    Register Login
                    • 1 / 1
                    • First post
                      Last post
                    Enjoying MagicMirror? Please consider a donation!
                    MagicMirror created by Michael Teeuw.
                    Forum managed by Sam, technical setup by Karsten.
                    This forum is using NodeBB as its core | Contributors
                    Contact | Privacy Policy