MagicMirror Forum
    • Recent
    • Tags
    • Unsolved
    • Solved
    • MagicMirror² Repository
    • Documentation
    • 3rd-Party-Modules
    • Donate
    • Discord
    • Register
    • Login
    1. Home
    2. rkorell
    A New Chapter for MagicMirror: The Community Takes the Lead
    Read the statement by Michael Teeuw here.
    R Offline
    • Profile
    • Following 0
    • Followers 1
    • Topics 26
    • Posts 431
    • Groups 1

    rkorell

    @rkorell

    Module Developer
    83
    Reputation
    24
    Profile views
    431
    Posts
    1
    Followers
    0
    Following
    Joined
    Last Online

    rkorell Unfollow Follow
    Module Developer

    Best posts made by rkorell

    • PIR / MQTT - Presence sensor(s) revived

      Dear MagicMirror-ians

      As may already known I was a real fan of MMM-Pir.
      For also good known reasons this nice module is not longer maintained.

      I have a local clone which runs OK but in the meantime I had switched to MMM-MQTTScreenOnOff wich attaches a “better” (radar-) sensor to my mirror. But I’ve missed the progress bar, the dimming and cron-like functionality. In addition the radar sensor is much slower than PIR- caused by a 5 second update intervall as minimum.

      After plenty of tinkering and testing, and above all torturing AI heavily I’m excited to announce:
MMM-PresenceScreenControl is finally out! 🎉

      This module brings together the best of both worlds:
      • The beloved timer bar and auto-dimming from MMM-Pir,
      • The reliability, MQTT support, and simplicity of MMM-MQTTScreenOnOff,
      • All cleaned up, without unnecessary complexity, cryptic cron strings, or weird build tricks.

      A few highlights:
      • Presence detection with PIR, MQTT, or both (whichever triggers first wins)
      • A visual timer bar so you always know how long the screen will stay on
      • Straightforward “ignore” and newly introduced “always-on” time windows using plain times and days—no cryptic cron needed
      • Customizable screen on/off commands—works on Pi, PC, X11, Wayland, CEC, and more
      • Touch mode for remote or manual override (yes, you can poke your mirror awake remotely!)

      If you’re after spinning circles or relay magic, this isn’t your thing (yet 😉).

      But if you want a solid, readable, and maintainable presence module that just works—even (hopefully) after updates—this is for you.

      Give it a try, share your feedback, open pull requests, or ask questions right here or on GitHub:

      Happy mirroring!


      Ralf

      posted in System
      R
      rkorell
    • MMM-Globe: Meteosat imagery broken — fork with fix available

      Dear community,

      just a heads-up for those of you using MMM-Globe with the europeDiscNat or europeDiscSnow
      style — you’ve probably noticed that the satellite image has been stuck since around
      February 22. Turns out EUMETSAT has pulled the plug on their old static image server at
      eumetview.eumetsat.int. They moved to a shiny new platform at view.eumetsat.int, but
      unfortunately it’s a JavaScript app now — no more simple image URLs we can point a module
      at.

      I’ve also opened an issue on the original repo for reference:
      https://github.com/LukeSkywalker92/MMM-Globe/issues/22

      I realize this probably flew under the radar for most of you — if your globe is pointed at
      the Americas or Asia, everything’s fine. But for the small but proud club of European globe
      watchers, it’s been a rough week staring at the same cloud pattern wondering if the weather
      had simply stopped.

      Since the original MMM-Globe hasn’t seen any updates in a while, I went ahead and created a
      fork that adds a new “meteosat” style. It pulls Meteosat full-disk imagery from the CIRA
      SLIDER service (run by NOAA/RAMMB at Colorado State University). The GeoColor product looks
      really nice on the mirror — natural color during the day, and city lights on a Blue Marble
      background at night. I think you’ll like it!

      Big thanks to Luke Scheffler (@lukecodewalker) for creating MMM-Globe in
      the first place — his idea of turning satellite images into a globe with a simple CSS circle
      clip is just brilliant. This fork builds on his work.

      If you want to give it a try:
      cd ~/MagicMirror/modules
      rm -rf MMM-Globe
      git clone https://github.com/rkorell/MMM-Globe.git

      Then change your config to:
      {
      module: “MMM-Globe”,
      position: “lower_third”,
      config: {
      style: “meteosat”,
      imageSize: 600,
      updateInterval: 15 * 60 * 1000
      }
      },

      The fork also fixes an annoying startup issue where the globe would stay blank after a
      reboot if the network wasn’t ready yet — something that probably bugged a few of us running
      this on a Raspberry Pi.

      All the details, available styles and config options are in the README:
      https://github.com/rkorell/MMM-Globe

      Happy mirroring! 🪞

      posted in Showcase
      R
      rkorell
    • New Mirror

      Hello to all,
      I’ve started end of last week and today I’m nearly happy with my modules and arrangement.

      Had struggled somtimes with several modules, not all of interest are currently maintained so sometimes I cannot get run it properly.
      Just now I’m “ready” with config - future steps are purchasing the mirror (sample pack is shipping) and build up the frame.
      Tomorrow the PIR sensor will arrive so in addition I have to figure out if and how this works and intergrate this sensor into the build.

      Future step is the integration of a background LED stripe - not sure about this because I’m currently not sure if I’m able to switch this LED on/off in conjunction with monitor (out of MMM-Pir).

      Here a current screenshot:
      MagicMirrorProto.jpg

      I’ve used standards (weather, clock, newsfeed) and:

      MMM-Globe
      MMM-MyGarbage
      MMM-CalendarExt3Agenda
      MMM-Strava
      MMM-NowPlayingOnSpotify
      MMM-ToDoist
      birthdaylist
      MMM-SystemStats
      MMM-SystemInfo
      (MMM-Pir - “installed” but lacking sensor, so disabled and not tested)

      Target Monitor is a 27’’ Samsung device and I will mount this behind a 120cm*60cm mirror.
      (Will post an image, if ready…)

      I’m thinking about enriching the szenario with an 8’’ android tablet for streaming, but not sure…

      Nice rest of the day to all of you, thanks for your really GREAT work and highly motivated assistance in this forum.

      Warmest regards,
      Ralf

      posted in Show your Mirror
      R
      rkorell
    • RE: MMM-Globe: Meteosat imagery broken — fork with fix available

      @plainbroke said:

      Will that work in the USA ? I like the prettier styling…

      Dear @plainbroke ,
      well, I owe you an apology — and a thank you!

      When you asked, I answered “wrong satellite” and moved on. Technically correct, but I completely missed the obvious next question: “So… can we get the
      RIGHT satellite?”

      Turns out, we can. The CIRA SLIDER service that provides the beautiful Meteosat GeoColor imagery (with the night city lights) serves four geostationary
      satellites — and one of them is GOES-19, parked right over the Americas at 75.2°W. Same API, same image quality, same stunning day/night visualization.
      I just never looked.

      Your question made me look. So as of v3.1.0, MMM-Globe now supports:

      ┌─────────────────┬────────────────────┬───────────────────────────┐
      │      Style      │     Satellite      │           View            │
      ├─────────────────┼────────────────────┼───────────────────────────┤
      │ geoColorEurope  │ Meteosat (0°)      │ Europe / Africa           │
      ├─────────────────┼────────────────────┼───────────────────────────┤
      │ geoColorUSA     │ GOES-19 (75.2°W)   │ Americas ← this is yours! │
      ├─────────────────┼────────────────────┼───────────────────────────┤
      │ geoColorPacific │ GOES-18 (137.0°W)  │ Pacific                   │
      ├─────────────────┼────────────────────┼───────────────────────────┤
      │ geoColorAsia    │ Himawari (140.7°E) │ Asia / Australia          │
      └─────────────────┴────────────────────┴───────────────────────────┘
      

      Just set style: “geoColorUSA” and you’re good to go. Same auto-polling every 60 seconds, same beautiful globe, just the right half of the planet this
      time. 😊

      So thank you for what I should have recognized as a feature request instead of a geography lesson. Sometimes the best contributions come disguised as
      simple questions.

      Warmest regards,
      Ralf

      posted in Showcase
      R
      rkorell
    • RE: New Mirror

      And here the target monitor in action - 27’’ Samsung…

      MagicMirrorHardwareSmallresolution.jpg

      Keep you posted.

      Regards,
      Ralf

      posted in Show your Mirror
      R
      rkorell
    • RE: Screen "offline" during specific time of day

      @_V_ My module MMM-PresenceScreenControl Offers a comfortable way to switch on or off during defined times - as much as you like per day as much days as you like.

      Regards,
      Ralf

      posted in Troubleshooting
      R
      rkorell
    • Problems with WLAN connectivity - solved

      Dear mirror fans,
      for your information and reference some findings with my mirror.
      I’m running a MagicMirror on a PI5 with an NVME HAT as boot device.
      My first approach was to de-assemble an original Pi power supply (because of its form factor) and to build this internally into the mirror-frame.
      As reported earlier in a different thread this power supply died due to overheating.

      My next approach was to use a new PI-power supply - this time externally.
      Caused by the circumstances of my installation (power plug far below mirror position and Pi mounted on the top of the mirror) I have used a USB-C to USB-C cable (150cm, 5A) to extend the standard-cable.

      As it turns out now this wasn’t a good idea, ether:
      It worked pretty long (several weeks) good and without any problem.
      But since some days I got more and more really stubborn WLAN losses which were often unrecoverable - only plugging out power supply to reforce a restart helped (I’m working headless as majority of you).

      In the meantime I was able to implement a tiny service which automatically detects the connectivity loss and restarts the WLAN, so a sufficient symptomatic treatment is in place - this discovers connectivity every five minutes, which is OK to me.

      While I was just tinkering I’ve thought it could be a nice idea to identify the root cause and so I added some logging features in the mentioned service.

      Now the interesting (unexpected) finding: Obvious root cause was an undervoltage!

      I’ve searched around (because initially I failed to remember my “cable-extension”) but couldn’t find any reason for this (nothing attached else than the NVME and my mirror doesn’t have anything heavily using the harddisk)…

      Then the additional cable came in my mind and - voilà - this was the root cause - despite its thickness and 5A specification.
      For now I have added some 230V cabeling to the top of the mirror, installed there (outside the mirror frame) a third (de-assembled) PI power supply and connected the standard-long cable of this power supply to the Pi.
      Since then no undervoltage detected (prior to this every few minutes).

      So my learning: Pi is bitchy with cable extensions and tiny undervoltages can lead to heavy WLAN problems.

      May one or the other can benefit from these findings.

      Warm regards,
      Ralf

      posted in General Discussion
      R
      rkorell
    • RE: MMM-Temp2IOT

      @chrisfr1976
      have solved it by myself…
      Used @sdetweil 's tip with developer console and identified CSS classes for the icons.
      Have set

      .MMM-Temp2IOT .iconify {
          font-size: 0px; /* Symbole verschwinden lassen */
      }
      

      in custom.css -
      works :-)

      Warm regards,
      Ralf

      posted in Utilities
      R
      rkorell
    • RE: First MM project - family dashboard

      @cpcode
      Really cool!

      For your families’ whishes: A nice module für RTPS strems of security cams is “MMM-MPlayer” …
      There are several other modules around but the other ones do have several dependencies and requirements.
      MMM-Mplayer use (as name suggest) simply mplayer.
      Works like a charm …
      @myfingersarecold has done a really nice job with this.

      Regards,
      Ralf

      posted in Show your Mirror
      R
      rkorell
    • RE: How to add custom weather icons

      @kool said

      Bumping this back up. Pleeeease help

      Dear @kool ,

      if you are using url as suggested by Sam ( @sdetweil ), you are nearly done.
      As far as I can see from my rookie perspective you do have some redundancies in your config.js (e.g. the unit section: units, tempUnits, windUnits) ) but this shouldn’t disturb.
      As far as I understood you can delete these icontables because you do not use the icon-font but try to use some images from sigle file-URLs instead - so you can delete the whole section iconTable {} with all entries.

      In addition you have to adapt (see above, Sam’s tip) the URL of all you images in your custom.css file.
      (pls. double check, if the files are really in these locations)
      You wrote in your current css

      .weatherforecast .wi-day-sunny {
      content: url("/home/kool/MagicMirror/css/icons/6fas/day.svg") !important;
      
      }
      

      IF your weather-icon files are really in this directory (/home/kool/MagicMirror/css/icons/6fas/) than your css should be as follows:

      .weatherforecast .wi-day-sunny {
       content: url("/css/icons/6fas/day.svg");
      
      

      You do not need the !important flag (in my case)

      second wrong thing besides the URL is your qualifier.

      you are using

      .weatherforecast .wi-something  ....
      

      This cannot be recognized because .weatherforecast is not defined.

      At first you have to decide if you want to have different icons for the current weather and for the weather forecast.
      (In my case this is true).
      IF you would like to have this you have to differentiate the two instances of the weather-module by a “classes” definition.

      For example your first instance of the weather module (current weather) than should be

         module: "weather",
          position: "top_right", // Adjust position as needed
          classes: "weather_current",
          config: {
              weatherProvider: "openmeteo", // Specify the Open-Meteo provider
              apiBase: "https://api.open-meteo.com/v1", // REQUIRED: Base URL for Open-Meteo
      
             lat: xxxx, // REQUIRED: Latitude of the location
              lon: xxxx, // REQUIRED: Longitude of the location
              maxNumberOfDays: 8, // OPTIONAL: Number of forecast days (default is 5)
              pastDays: 0, // OPTIONAL: Number of past days of data to include (default is 0)
         units: "imperial", // Set this to 'imperial' for Fahrenheit and miles per hour
              tempUnits: "imperial", // Make sure to match with 'imperial'
              windUnits: "imperial",
              type: "current", // OPTIONAL: Change to "current" if only current weather data is desired
      
       
          }
      },
      

      (pay attention for the third line: classes: “weather_current”, )
      THIS is your qualifier if you are about to diffrentiate between current weather and weather forecast image-wise.
      (“weather_current” is just an example! you can name it like you want every name is suitable and OK, only pay attention that all of these namings are strictly case sensitive, so Weather is NOT the same as weather"

      Your correct line for custom.css than is:

      .weather_current .wi-day-sunny {
       content: url("/css/icons/6fas/day.svg");
      
      

      and the same dance than for your second instance with DIFFERENT classes-clause in config.js
      (e.g. a " classes: “weather_forecast”, " in the definition of the second weather instance ) results in a custom.css entry like this:

      .weather_forecast .wi-day-sunny {
       content: url("/css/icons/some_other_directory_with_smaller_icons/day.svg");
      
      

      Because the “forecast instance” of the weather module is (in my case) organized as table I found it really useful to differentite these logos - big ones for current, smaller ones for the forecast:

      ScreenFloat Bildschirmfoto von Sublime Text am 14_04_2025, 13_38_51 14_04_2025, 13-38-51.jpg

      If You do NOT like to differentiate the current and forecast instance you do NOT need the classes-phrase in config.js and your correct qualifier is the name of the module (so “weather” ) and your custom.css entry is like this:

      .weather .wi-day-sunny {
       content: url("/css/icons/6fas/day.svg");
      
      

      And this than is valid for both instances.

      Keep in mind that there are a LOT more weather conditions than the two times 5 conditions you have defined in your current custom.css!

      my own definition for the current weather for your reference:

      
      .weather_current .wi-fog  {
      content: url("/css/icons/current/wsymbol_0007_fog.png");
      }
      .weather_current .wi-cloudy  {
      content: url("/css/icons/current/wsymbol_0002_sunny_intervals.png");
      }
      .weather_current .wi-cloudy-windy {
      content: url("/css/icons/current/wsymbol_0004_black_low_cloud.png");
      }
      .weather_current .wi-rain  {
      content: url("/css/icons/current/wsymbol_0018_cloudy_with_heavy_rain.png");
      }
      .weather_current .wi-showers  {
      content: url("/css/icons/current/wsymbol_0017_cloudy_with_light_rain.png");
      }
      .weather_current .wi-thunderstorm  {
      content: url("/css/icons/current/wsymbol_0024_thunderstorms.png");
      }
      .weather_current .wi-snow  {
      content: url("/css/icons/current/wsymbol_0019_cloudy_with_light_snow.png");
      }
      .weather_current .wi-snowflake-cold {
      content: url("/css/icons/current/wsymbol_0020_cloudy_with_heavy_snow.png");
      }
      .weather_current .wi-na {
      content: url("/css/icons/current/wsymbol_0999_unknown.png");
      } 
      
      .weather_current .wi-day-sunny  {
        content: url("/css/icons/current/wsymbol_0001_sunny.png");
      }
      .weather_current .wi-day-cloudy  {
        content: url("/css/icons/current/wsymbol_0043_mostly_cloudy.png");
      }
      .weather_current .wi-day-cloudy-gusts  {
        content: url("/css/icons/current/wsymbol_0004_black_low_cloud.png");
      }
      .weather_current .wi-day-cloudy-windy  {
        content: url("/css/icons/current/wsymbol_0004_black_low_cloud.png");
      }
      .weather_current .wi-cloudy-windy  {
        content: url("/css/icons/current/wsymbol_0004_black_low_cloud.png");
      }
      .weather_current .wi-day-fog  {
        content: url("/css/icons/current/wsymbol_0007_fog.png");
      }
      .weather_current .wi-day-hail  {
        content: url("/css/icons/current/wsymbol_0015_heavy_hail_showers.png");
      }
      .weather_current .wi-day-haze  {
        content: url("/css/icons/current/wsymbol_0005_hazy_sun.png");
      }
      .weather_current .wi-day-lightning  {
        content: url("/css/icons/current/wsymbol_0016_thundery_showers.png");
      }
      .weather_current .wi-day-rain  {
        content: url("/css/icons/current/wsymbol_0085_extreme_rain_showers.png");
      }
      .weather_current .wi-day-rain-mix  {
        content: url("/css/icons/current/wsymbol_0009_light_rain_showers.png");
      }
      .weather_current .wi-day-rain-wind  {
        content: url("/css/icons/current/wsymbol_0010_heavy_rain_showers.png");
      }
      .weather_current .wi-day-showers  {
        content: url("/css/icons/current/wsymbol_0018_cloudy_with_heavy_rain.png");
      }
      .weather_current .wi-day-sleet  {
        content: url("/css/icons/current/wsymbol_0087_heavy_sleet_showers.png");
      }
      .weather_current .wi-day-sleet-storm  {
        content: url("/css/icons/current/wsymbol_0089_heavy_sleet.png");
      }
      .weather_current .wi-day-snow  {
        content: url("/css/icons/current/wsymbol_0011_light_snow_showers.png");
      }
      .weather_current .wi-day-snow-thunderstorm  {
        content: url("/css/icons/current/wsymbol_0057_thundery_snow_showers.png");
      }
      .weather_current .wi-day-snow-wind  {
        content: url("/css/icons/current/wsymbol_0053_blowing_snow.png");
      }
      .weather_current .wi-day-sprinkle  {
        content: url("/css/icons/current/wsymbol_0009_light_rain_showers.png");
      }
      .weather_current .wi-day-storm-showers  {
        content: url("/css/icons/current/wsymbol_0018_cloudy_with_heavy_rain.png");
      }
      .weather_current .wi-day-sunny-overcast  {
        content: url("/css/icons/current/wsymbol_0002_sunny_intervals.png");
      }
      .weather_current .wi-day-thunderstorm  {
        content: url("/css/icons/current/wsymbol_0024_thunderstorms.png");
      }
      .weather_current .wi-day-windy  {
        content: url("/css/icons/current/wsymbol_0060_windy.png");
      }
      .weather_current .wi-solar-eclipse  {
        content: url("/css/icons/current/wsymbol_0005_hazy_sun.png");
      }
      .weather_current .wi-hot  {
        content: url("/css/icons/current/wsymbol_0045_hot.png");
      }
      .weather_current .wi-day-cloudy-high  {
        content: url("/css/icons/current/wsymbol_0006_mist.png");
      }
      .weather_current .wi-day-light-wind  {
        content: url("/css/icons/current/wsymbol_0060_windy.png");
      }
      .weather_current .wi-night-clear  {
        content: url("/css/icons/current/wsymbol_0008_clear_sky_night.png");
      }
      .weather_current .wi-night-alt-cloudy  {
        content: url("/css/icons/current/wsymbol_0041_partly_cloudy_night.png");
      }
      .weather_current .wi-night-alt-partly-cloudy  {
        content: url("/css/icons/current/wsymbol_0041_partly_cloudy_night.png");
      }
      .weather_current .wi-night-alt-cloudy-gusts  {
        content: url("/css/icons/current/wsymbol_0041_partly_cloudy_night.png");
      }
      .weather_current .wi-night-alt-cloudy-windy  {
        content: url("/css/icons/current/wsymbol_0041_partly_cloudy_night.png");
      }
      .weather_current .wi-night-alt-hail  {
        content: url("/css/icons/current/wsymbol_0031_heavy_hail_showers_night.png");
      }
      .weather_current .wi-night-alt-lightning  {
        content: url("/css/icons/current/wsymbol_0032_thundery_showers_night.png");
      }
      .weather_current .wi-night-alt-rain  {
        content: url("/css/icons/current/wsymbol_0025_light_rain_showers_night.png");
      }
      .weather_current .wi-night-alt-rain-mix  {
        content: url("/css/icons/current/wsymbol_0026_heavy_rain_showers_night.png");
      }
      .weather_current .wi-night-alt-rain-wind  {
        content: url("/css/icons/current/wsymbol_0026_heavy_rain_showers_night.png");
      }
      .weather_current .wi-night-alt-showers  {
        content: url("/css/icons/current/wsymbol_0025_light_rain_showers_night.png");
      }
      .weather_current .wi-night-alt-sleet  {
        content: url("/css/icons/current/wsymbol_0029_sleet_showers_night.png");
      }
      .weather_current .wi-night-alt-sleet-storm  {
        content: url("/css/icons/current/wsymbol_0029_sleet_showers_night.png");
      }
      .weather_current .wi-night-alt-snow  {
        content: url("/css/icons/current/wsymbol_0028_heavy_snow_showers_night.png");
      }
      .weather_current .wi-night-alt-snow-thunderstorm  {
        content: url("/css/icons/current/wsymbol_0075_thundery_snow_showers_night.png");
      }
      .weather_current .wi-night-alt-snow-wind  {
        content: url("/css/icons/current/wsymbol_0071_blowing_snow_night.png");
      }
      .weather_current .wi-night-alt-sprinkle  {
        content: url("/css/icons/current/wsymbol_0025_light_rain_showers_night.png");
      }
      .weather_current .wi-night-alt-storm-showers  {
        content: url("/css/icons/current/wsymbol_0026_heavy_rain_showers_night.png");
      }
      .weather_current .wi-night-alt-thunderstorm  {
        content: url("/css/icons/current/wsymbol_0032_thundery_showers_night.png");
      }
      .weather_current .wi-night-cloudy  {
        content: url("/css/icons/current/wsymbol_0041_partly_cloudy_night.png");
      }
      .weather_current .wi-night-cloudy-gusts  {
        content: url("/css/icons/current/wsymbol_0044_mostly_cloudy_night.png");
      }
      .weather_current .wi-night-cloudy-windy  {
        content: url("/css/icons/current/wsymbol_0044_mostly_cloudy_night.png");
      }
      .weather_current .wi-night-fog  {
        content: url("/css/icons/current/wsymbol_0064_fog_night.png");
      }
      .weather_current .wi-night-hail  {
        content: url("/css/icons/current/wsymbol_0039_cloudy_with_heavy_hail_night.png");
      }
      .weather_current .wi-night-lightning  {
        content: url("/css/icons/current/wsymbol_0032_thundery_showers_night.png");
      }
      .weather_current .wi-night-partly-cloudy  {
        content: url("/css/icons/current/wsymbol_0041_partly_cloudy_night.png");
      }
      .weather_current .wi-night-rain  {
        content: url("/css/icons/current/wsymbol_0025_light_rain_showers_night.png");
      }
      .weather_current .wi-night-rain-mix  {
        content: url("/css/icons/current/wsymbol_0026_heavy_rain_showers_night.png");
      }
      .weather_current .wi-night-rain-wind  {
        content: url("/css/icons/current/wsymbol_0025_light_rain_showers_night.png");
      }
      .weather_current .wi-night-showers  {
        content: url("/css/icons/current/wsymbol_0026_heavy_rain_showers_night.png");
      }
      .weather_current .wi-night-sleet  {
        content: url("/css/icons/current/wsymbol_0029_sleet_showers_night.png");
      }
      .weather_current .wi-night-sleet-storm  {
        content: url("/css/icons/current/wsymbol_0029_sleet_showers_night.png");
      }
      .weather_current .wi-night-snow  {
        content: url("/css/icons/current/wsymbol_0027_light_snow_showers_night.png");
      }
      .weather_current .wi-night-snow-thunderstorm  {
        content: url("/css/icons/current/wsymbol_0075_thundery_snow_showers_night.png");
      }
      .weather_current .wi-night-snow-wind  {
        content: url("/css/icons/current/wsymbol_0028_heavy_snow_showers_night.png");
      }
      .weather_current .wi-night-sprinkle  {
        content: url("/css/icons/current/wsymbol_0025_light_rain_showers_night.png");
      }
      .weather_current .wi-night-storm-showers  {
        content: url("/css/icons/current/wsymbol_0026_heavy_rain_showers_night.png");
      }
      .weather_current .wi-night-thunderstorm  {
        content: url("/css/icons/current/wsymbol_0032_thundery_showers_night.png");
      }
      
      
      

      HTH & good luck!
      Ralf

      posted in Development
      R
      rkorell

    Latest posts made by rkorell

    • RE: electronSwitches in config.js — am I reading the code wrong, or does it not actually work?

      @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.

      • never touch a running system :-)

      Warm regards,
      Ralf

      posted in Troubleshooting
      R
      rkorell
    • RE: electronSwitches in config.js — am I reading the code wrong, or does it not actually work?

      @sdetweil OK, thanks!
      Still doesn’t sound THAT safe …

      regards,
      Ralf

      posted in Troubleshooting
      R
      rkorell
    • RE: electronSwitches in config.js — am I reading the code wrong, or does it not actually work?

      @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 !

      • changes of standard-module location, changes of location for custom.css, electron new …

      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

      posted in Troubleshooting
      R
      rkorell
    • RE: electronSwitches in config.js — am I reading the code wrong, or does it not actually work?

      Dear Sam, @sdetweil,
      thanks to you as well!

      As mentioned above: A little bit shy …

      Warmest regards,
      Ralf

      posted in Troubleshooting
      R
      rkorell
    • RE: electronSwitches in config.js — am I reading the code wrong, or does it not actually work?

      @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

      posted in Troubleshooting
      R
      rkorell
    • electronSwitches in config.js — am I reading the code wrong, or does it not actually work?

      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:

      1. new Set(a, b) silently drops the second argument.

      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?

      1. appendSwitch only takes one switch per call.

      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:

      • Is my reading of new Set(a, b) correct here?
      • Has anyone successfully used electronSwitches to actually change Electron behaviour, and if so, with what syntax?
      • Is this maybe legacy code that the community has worked around in other ways I am not aware of?

      Thanks,
      Ralf

      posted in Troubleshooting
      R
      rkorell
    • 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

      posted in System
      R
      rkorell
    • 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

      posted in Troubleshooting
      R
      rkorell
    • 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.');
      			}
      		});
      	}
      });
      
      
      posted in Troubleshooting
      R
      rkorell
    • 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

      posted in Troubleshooting
      R
      rkorell