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

    cgillinger

    @cgillinger

    30
    Reputation
    16
    Profile views
    52
    Posts
    0
    Followers
    0
    Following
    Joined
    Last Online

    cgillinger Unfollow Follow

    Best posts made by cgillinger

    • Made a birthday module

      I got inspired by MMM-Fireworks and decided to make a birthday module with fireworks and confetti. Its not a “birthday reminder” module, rather it sits quietly, and only erupts when someone has a birthday:
      24b5b63d-b220-492d-9b17-b1d2a5f6f985-image.png

      At that time a message is presented in the middle of the screen, fireworks and confetti is displayed.

      Its somewhat multilingual (machine translated) and has a few different messages to pick from.

      Things I haven’t solved: Currently there is a timer, that is per default set to “infinite” - because its of no use if its invisible. It would probably make sense to make some sortof “off button”, but my main display has no touch or input. So as of now it is as default showed until the day ends.

      Here it is: https://github.com/cgillinger/MMM-Birthday

      posted in Entertainment
      C
      cgillinger
    • Weatherstation using MagicMirror+Rasp3b+7inch touchscreen

      Hi!

      So, I’m using MagicMirror to power a basic Weather Station for the family and I thought I’d share the work and configs I’ve used. So, this is not a MagicMirror!

      First, hardware: I’m using a Rasperry Pi 3b+ with a 7 inch touchscreen I bought off Amazon. I wanted the Weather Station to be able to show time, local current weather and the next ten to twelve hours.

      I’m currently using four modules: The standard Clock, The standard Weather module set to “current”, The standard Weather module set to “forecast” and Eben Kouaos “SmartTouch”-module: https://github.com/EbenKouao/MMM-SmartTouch

      (I have a fifth module that I think will replace the “current”-module: The CFenner Netatmo module (https://github.com/CFenner/MMM-Netatmo), but I had a bit of trouble getting it to work (but have succeeded now, so will implement it when I get the time). I really want to use my Netatmo hardware. )

      I’ll add the code at the end of this write-up.

      Customization
      Since I’m not using it as a mirror I did a bit of configuring. First I found that having everything in shades of gray made it a bit dull. So first thing I did was to add color. And I should add here that I suck at coding, so I had ChatGPT do all the heavy lifting, including picking colors, because there are a gazillion icons to pick for. The icons are easy to find (https://erikflowers.github.io/weather-icons/), and then I just had ChatGPT suggest colors for each of them. It turned out beautiful, imho.

      Secondly the letters are a tad small on the tiny 7-inch screen, so after a bit of tinkering, I managed to enlarge both icons and text of the Weather module. This involed using the developer tools from Chrome to find the correct elements to change. I also changed the text from the standard gray, to more of a white shade.

      Third, I wanted the “Reboot” and “Shutdown” buttons from SmartTouch to be clearer, so I added colors to those too, red for “shutdown” and yellow for “reboot”.

      And here is the result(screenshot) (as you can see language is set to swedish) :
      VirtualBox_Ubunti_04_01_2024_12_21_47.png

      VirtualBox_Ubunti_04_01_2024_12_24_58.png

      And of the entire frame with horrible glare and all:
      IMG_3716.JPG

      I have a few more things I want to do: Replacing standard weather module with the Netatmo one, and I’d like the SmartTouch button for “hiding modules” to instead just dim the screen.

      And code:
      To change colors of icons (added in MagicMirrors “custom.css”):

      .wi-day-sunny { color: #FFD700; } /* Bright Yellow */
      .wi-night-clear { color: #FFD27F; } /* Moon Yellow */
      .wi-day-cloudy { color: #87CEEB; } /* Sky Blue */
      .wi-night-alt-cloudy { color: #1E90FF; } /* Dark Blue */
      .wi-day-cloudy-gusts { color: #A9BACD; } /* Gray Blue */
      .wi-night-alt-cloudy-gusts { color: #4169E1; } /* Twilight Blue */
      .wi-day-cloudy-windy { color: #ADD8E6; } /* Light Blue */
      .wi-night-alt-cloudy-windy { color: #191970; } /* Midnight Blue */
      .wi-day-fog { color: #D3D3D3; } /* Light Gray */
      .wi-night-fog { color: #C0C0C0; } /* Silver */
      .wi-day-hail { color: #ACE5EE; } /* Ice Blue */
      .wi-night-hail { color: #4682B4; } /* Steel Blue */
      .wi-day-lightning { color: #FFA500; } /* Bright Orange */
      .wi-night-alt-lightning { color: #FFD700; } /* Gold */
      .wi-day-rain { color: #0000FF; } /* Blue */
      .wi-night-alt-rain { color: #483D8B; } /* Dark Slate Blue */
      .wi-day-rain-mix { color: #6495ED; } /* Cornflower Blue */
      .wi-night-alt-rain-mix { color: #4169E1; } /* Royal Blue */
      .wi-day-rain-wind { color: #1E90FF; } /* Dodger Blue */
      .wi-night-alt-rain-wind { color: #0000CD; } /* Medium Blue */
      .wi-day-showers { color: #00BFFF; } /* Deep Sky Blue */
      .wi-night-alt-showers { color: #6A5ACD; } /* Slate Blue */
      .wi-day-sleet { color: #778899; } /* Light Slate Gray */
      .wi-night-alt-sleet { color: #A9A9A9; } /* Dark Gray */
      .wi-day-snow { color: #FFFFFF; } /* White */
      .wi-night-alt-snow { color: #DCDCDC; } /* Gainsboro */
      .wi-day-sprinkle { color: #B0C4DE; } /* Light Steel Blue */
      .wi-night-alt-sprinkle { color: #ADD8E6; } /* Light Blue */
      .wi-day-storm-showers { color: #FF8C00; } /* Dark Orange */
      .wi-night-alt-storm-showers { color: #B8860B; } /* Dark Goldenrod */
      .wi-day-sunny-overcast { color: #EEE8AA; } /* Pale Goldenrod */
      .wi-night-alt-cloudy-high { color: #87CEFA; } /* Light Sky Blue */
      .wi-day-light-wind { color: #B0E0E6; } /* Powder Blue */
      .wi-night-alt-partly-cloudy { color: #00CED1; } /* Dark Turquoise */
      .wi-cloudy {
          color: #ADD8E6; /* Light blue color */
      }
      .wi-cloud { color: #D3D3D3; } /* Light Gray */
      .wi-cloudy { color: #A9BACD; } /* Gray Blue */
      .wi-cloudy-gusts { color: #4682B4; } /* Steel Blue */
      .wi-cloudy-windy { color: #87CEEB; } /* Sky Blue */
      .wi-fog { color: #C0C0C0; } /* Silver */
      .wi-hail { color: #ACE5EE; } /* Ice Blue */
      .wi-rain { color: #0000FF; } /* Blue */
      .wi-rain-mix { color: #6495ED; } /* Cornflower Blue */
      .wi-rain-wind { color: #1E90FF; } /* Dodger Blue */
      .wi-showers { color: #00BFFF; } /* Deep Sky Blue */
      .wi-sleet { color: #778899; } /* Light Slate Gray */
      .wi-snow { color: #FFFFFF; } /* White */
      .wi-sprinkle { color: #B0C4DE; } /* Light Steel Blue */
      .wi-storm-showers { color: #FF8C00; } /* Dark Orange */
      .wi-thunderstorm { color: #FFD700; } /* Gold */
      .wi-snow-wind { color: #DCDCDC; } /* Gainsboro */
      .wi-smog { color: #FFFFE0; } /* Light Yellow */
      .wi-smoke { color: #BC8F8F; } /* Rosy Brown */
      .wi-lightning { color: #FFA500; } /* Bright Orange */
      .wi-raindrops { color: #5F9EA0; } /* Cadet Blue */
      .wi-raindrop { color: #ADD8E6; } /* Light Blue */
      .wi-dust { color: #F0E68C; } /* Khaki */
      .wi-snowflake-cold { color: #F0FFFF; } /* Azure */
      .wi-windy { color: #F08080; } /* Light Coral */
      .wi-strong-wind { color: #00008B; } /* DarkBlue */
      .wi-sunrise { color: #FFD700; } /* Bright Yellow */
      .wi-sunset { color: #FF8C00; } /* Sunset Orange */
      

      To change color of the SmartTouch “Reboot” and “Standby” (added in "mmm-smarttouch.css):

      /* Style for Shutdown button text and icon */
      .st-container__main-menu > ul > li:nth-child(1),
      .st-container__main-menu > ul > li:nth-child(1) .fa {
        color: red; /* Changes the text and icon color to red */
      }
      
      /* Style for Reboot button text and icon */
      .st-container__main-menu > ul > li:nth-child(2),
      .st-container__main-menu > ul > li:nth-child(2) .fa {
        color: #ffcc00; /* Changes the text and icon color to yellow */
      }
      
      

      To change color of text overall (edited into “custom.css”)

      :root {
        --color-text: #EEEEEE; /* Almost white, but not as bright as pure white */
        --color-text-dimmed: #666;
        --color-text-bright: #fff;
        --color-background: #000;
        /* ... other variables ... */
      }
      

      To change size of text and icons in the weather module (added in the “custom.css” file):

      /* Weather Icon Size */
      #module_4_weather .weather-icon .wi {
          font-size: 40px; /* Adjust the icon size as needed */
      }
      
      /* Temperature Text Size */
      .weather .align-right.bright {
          font-size: 26px;
      }
          
       .weather .day {
          font-size: 26px !important; /* Forces the font size, overriding other rules */
      }
      
      posted in Show your Mirror
      C
      cgillinger
    • Bird of the day

      My daughter is really interested in birds, so I thought it would be fun to create something bird-related for her. I came across a great API that provides bird facts and images, and decided to make a simple “Bird of the Day” module for MagicMirror². 🐦

      The module displays a random bird at a set interval using the Nuthatch API (https://nuthatch.lastelm.software/). The API is free, and you just need an API key to get started.

      Here’s an example of how it looks:

      
      

      d13a2f61-c2d8-4e07-b22a-f9478d483939-image.png

      You can customize the interval (weekly, daily, or hourly) and choose which bird facts to display, such as name, region, conservation status, or scientific name.

      If this sounds like something you’d enjoy, you can find the project on GitHub:
      👉 https://github.com/cgillinger/MMM-BirdOfTheDay/

      posted in Entertainment
      C
      cgillinger
    • New module: Weather effects

      Earlier, I shared a module that makes it snow on MagicMirror — as soon as you start it, snowflakes begin to fall. When I first created it, I thought it would be cool if it only showed snow when it was actually snowing.

      @sdetweil gave me the idea that MagicMirror’s default weather module broadcasts notifications, so you can let the module pick up its notification using the following code:

      if (this.weatherProvider.currentWeather()) {
      	this.sendNotification("CURRENTWEATHER_TYPE", { type: this.weatherProvider.currentWeather().weatherType.replace("-", "_") });
      }
      

      So, I experimented a bit and got it to work! While I was at it, I also added rain. That is, if the weather module reports rain, raindrops will fall on the MagicMirror screen as well.

      I should mention that so far, I’ve only tested this with the Swedish weather provider SMHI, but it should work with the others too.

      I’ve tried to ensure that the annotations in the script and CSS make it reasonably easy to follow and understand if someone wants to tweak it themselves.

      Screenshot:
      Rich snow
      rich.png
      Light snow (for Raspberry Pies etc)
      light.png
      Rain
      Rain.png

      Also, for obvious reasons this module requires the standard weather module to be active issuing “CurrentWeather” statuses.

      https://github.com/cgillinger/MMM-WeatherEffects

      posted in Entertainment
      C
      cgillinger
    • Let it snow now Magic Mirror

      And this is my second module, again for the entertainment of my daughters. This one just adds a nice snow fall across the screen - for the winter festivities.

      I made it on a Linux computer, but realised when testing it on my Raspberry Pi 3B, that the Pi really struggled with it. So it now has two settings: light and “rich” - the latter for more powerful platforms.

      This is a part of me trying to learn how to make modules, so any input is welcome.

      Next: Im thinking of connecting it to one of the providers from the Weather module, to maybe make it snow when the temperature is freezing, or if the forcast warns of snow.

      But for now, just a simple snowfall on your MagicMirror.

      https://github.com/cgillinger/MMM-SnowEffect

      posted in Entertainment
      C
      cgillinger
    • RE: Swedish Weather provider SMHI has deprecated their API and replaced it

      @sdetweil Yes, done and merged now!

      posted in System
      C
      cgillinger
    • RE: MMM-Netatmo does not load

      @CFenner And just to report back here as well: Now the module works for me again! Thnx!

      posted in Utilities
      C
      cgillinger
    • RE: Let it snow now Magic Mirror

      @KristjanESPERANTO Added to the module list, and working on a screenshot ( I cleared my MM installtion of modules to test this, so I have to re add them, otherwise its just a black image with snow icons on them…)

      posted in Entertainment
      C
      cgillinger
    • RE: Question: Having a module span entire screen?

      @sdetweil Ah, thnx, I found the error, using the dev console. I hadn’t fully understood the regions of the screen. Got it to work now!

      posted in Troubleshooting
      C
      cgillinger
    • Swedish Weather provider SMHI has deprecated their API and replaced it

      Heads up for anyone using the Swedish SMHI weather provider in MagicMirror²!

      As of March 31, 2026, SMHI has deprecated their PMP3gv2 forecast API. It now returns HTTP 404.

      I noticed this, this week when my MM suddenly were empty of weather reports.

      The replacement is SNOW1gv1, hosted on the same domain but with a different data structure.

      What changed in the API

      • URL path: pmp3g/version/2 → snow1g/version/1
      • Time key: validTime → time
      • Data structure: The nested parameters[] array is replaced by a flat data object (e.g. entry.data.air_temperature instead of entry.parameters.find(p => p.name === 't').values[0])
      • Parameter names: Short codes replaced by human-readable names (t → air_temperature, ws → wind_speed, Wsymb2 → symbol_code, etc.)
      • Coordinates: Response now returns flat [lon, lat] instead of nested [[lon, lat]]
      • approvedtime.json endpoint: Removed entirely — use referenceTime from the data response instead

      Weather symbol codes (1–27) remain unchanged, so icon mapping still works as before.

      Updated provider

      I’ve updated the smhi.js provider to work with SNOW1gv1. Key changes:

      • New API URL
      • Rewritten data parser for the flat data object structure
      • All parameter names mapped to their new equivalents
      • Coordinate parsing updated for the new format
      • Added missing value handling (SMHI uses 9999 as sentinel)
      • Added support for precipitation type 11 (drizzle, new in SNOW1gv1)
      • Backward compatible — existing config.js settings (including precipitationValue) work without changes

      Tested and confirmed working with type: "daily" on a Raspberry Pi. The SNOW1gv1 API responds correctly and the weather module displays data as expected.

      Code:

      const Log = require("logger");
      const { getSunTimes, isDayTime, validateCoordinates } = require("../provider-utils");
      const HTTPFetcher = require("#http_fetcher");
      
      /**
       * Server-side weather provider for SMHI (Swedish Meteorological and Hydrological Institute)
       * Sweden only, metric system
       *
       * API: SNOW1gv1 — https://opendata.smhi.se/metfcst/snow1gv1
       * Migrated from PMP3gv2 (deprecated 2026-03-31, returns HTTP 404)
       *
       * Version: 2.0.1 (2026-04-02)
       *
       * Key differences from PMP3gv2:
       * - URL: snow1g/version/1 (was pmp3g/version/2)
       * - Time key: "time" (was "validTime")
       * - Data structure: flat object entry.data.X (was parameters[].find().values[0])
       * - Parameter names: human-readable (air_temperature, wind_speed, etc.)
       * - Coordinates: flat [lon, lat] (was nested [[lon, lat]])
       * - Precipitation types: different value mapping (1=rain, not snow)
       */
      
      /**
       * Maps user-facing config precipitationValue to SNOW1gv1 parameter names.
       * Maintains backward compatibility with existing MagicMirror configs.
       */
      const PRECIP_VALUE_MAP = {
      	pmin: "precipitation_amount_min",
      	pmean: "precipitation_amount_mean",
      	pmedian: "precipitation_amount_median",
      	pmax: "precipitation_amount_max"
      };
      
      class SMHIProvider {
      	constructor (config) {
      		this.config = {
      			lat: 0,
      			lon: 0,
      			precipitationValue: "pmedian", // pmin, pmean, pmedian, pmax
      			type: "current",
      			updateInterval: 5 * 60 * 1000,
      			...config
      		};
      
      		// Validate precipitationValue
      		if (!Object.keys(PRECIP_VALUE_MAP).includes(this.config.precipitationValue)) {
      			Log.warn(`[smhi] Invalid precipitationValue: ${this.config.precipitationValue}, using pmedian`);
      			this.config.precipitationValue = "pmedian";
      		}
      
      		this.fetcher = null;
      		this.onDataCallback = null;
      		this.onErrorCallback = null;
      	}
      
      	initialize () {
      		try {
      			// SMHI requires max 6 decimal places
      			validateCoordinates(this.config, 6);
      			this.#initializeFetcher();
      		} catch (error) {
      			Log.error("[smhi] Initialization failed:", error);
      			if (this.onErrorCallback) {
      				this.onErrorCallback({
      					message: error.message,
      					translationKey: "MODULE_ERROR_UNSPECIFIED"
      				});
      			}
      		}
      	}
      
      	setCallbacks (onData, onError) {
      		this.onDataCallback = onData;
      		this.onErrorCallback = onError;
      	}
      
      	start () {
      		if (this.fetcher) {
      			this.fetcher.startPeriodicFetch();
      		}
      	}
      
      	stop () {
      		if (this.fetcher) {
      			this.fetcher.clearTimer();
      		}
      	}
      
      	#initializeFetcher () {
      		const url = this.#getUrl();
      
      		this.fetcher = new HTTPFetcher(url, {
      			reloadInterval: this.config.updateInterval,
      			logContext: "weatherprovider.smhi"
      		});
      
      		this.fetcher.on("response", async (response) => {
      			try {
      				const data = await response.json();
      				this.#handleResponse(data);
      			} catch (error) {
      				Log.error("[smhi] Failed to parse JSON:", error);
      				if (this.onErrorCallback) {
      					this.onErrorCallback({
      						message: "Failed to parse API response",
      						translationKey: "MODULE_ERROR_UNSPECIFIED"
      					});
      				}
      			}
      		});
      
      		this.fetcher.on("error", (errorInfo) => {
      			if (this.onErrorCallback) {
      				this.onErrorCallback(errorInfo);
      			}
      		});
      	}
      
      	#handleResponse (data) {
      		try {
      			if (!data.timeSeries || !Array.isArray(data.timeSeries)) {
      				throw new Error("Invalid weather data");
      			}
      
      			const coordinates = this.#resolveCoordinates(data);
      			let weatherData;
      
      			switch (this.config.type) {
      				case "current":
      					weatherData = this.#generateCurrentWeather(data.timeSeries, coordinates);
      					break;
      				case "forecast":
      				case "daily":
      					weatherData = this.#generateForecast(data.timeSeries, coordinates);
      					break;
      				case "hourly":
      					weatherData = this.#generateHourly(data.timeSeries, coordinates);
      					break;
      				default:
      					Log.error(`[smhi] Unknown weather type: ${this.config.type}`);
      					if (this.onErrorCallback) {
      						this.onErrorCallback({
      							message: `Unknown weather type: ${this.config.type}`,
      							translationKey: "MODULE_ERROR_UNSPECIFIED"
      						});
      					}
      					return;
      			}
      
      			if (this.onDataCallback) {
      				this.onDataCallback(weatherData);
      			}
      		} catch (error) {
      			Log.error("[smhi] Error processing weather data:", error);
      			if (this.onErrorCallback) {
      				this.onErrorCallback({
      					message: error.message,
      					translationKey: "MODULE_ERROR_UNSPECIFIED"
      				});
      			}
      		}
      	}
      
      	#generateCurrentWeather (timeSeries, coordinates) {
      		const closest = this.#getClosestToCurrentTime(timeSeries);
      		return this.#convertWeatherDataToObject(closest, coordinates);
      	}
      
      	#generateForecast (timeSeries, coordinates) {
      		const filled = this.#fillInGaps(timeSeries);
      		return this.#convertWeatherDataGroupedBy(filled, coordinates, "day");
      	}
      
      	#generateHourly (timeSeries, coordinates) {
      		const filled = this.#fillInGaps(timeSeries);
      		return this.#convertWeatherDataGroupedBy(filled, coordinates, "hour");
      	}
      
      	/**
      	 * Find the time series entry closest to the current time.
      	 * SNOW1gv1 uses "time" instead of PMP3gv2's "validTime".
      	 */
      	#getClosestToCurrentTime (times) {
      		const now = new Date();
      		let minDiff = null;
      		let closest = times[0];
      
      		for (const time of times) {
      			const entryTime = new Date(time.time);
      			const diff = Math.abs(entryTime - now);
      
      			if (minDiff === null || diff < minDiff) {
      				minDiff = diff;
      				closest = time;
      			}
      		}
      
      		return closest;
      	}
      
      	/**
      	 * Convert a single SNOW1gv1 time series entry to MagicMirror weather object.
      	 *
      	 * SNOW1gv1 data structure: entry.data.parameter_name (flat object)
      	 * PMP3gv2 used: entry.parameters[{name, values}] (array of objects)
      	 */
      	#convertWeatherDataToObject (weatherData, coordinates) {
      		const date = new Date(weatherData.time);
      		const { sunrise, sunset } = getSunTimes(date, coordinates.lat, coordinates.lon);
      		const isDay = isDayTime(date, sunrise, sunset);
      
      		const current = {
      			date: date,
      			humidity: this.#paramValue(weatherData, "relative_humidity"),
      			temperature: this.#paramValue(weatherData, "air_temperature"),
      			windSpeed: this.#paramValue(weatherData, "wind_speed"),
      			windFromDirection: this.#paramValue(weatherData, "wind_from_direction"),
      			weatherType: this.#convertWeatherType(this.#paramValue(weatherData, "symbol_code"), isDay),
      			feelsLikeTemp: this.#calculateApparentTemperature(weatherData),
      			sunrise: sunrise,
      			sunset: sunset,
      			snow: 0,
      			rain: 0,
      			precipitationAmount: 0
      		};
      
      		// Map user config (pmedian/pmean/pmin/pmax) to SNOW1gv1 parameter name
      		const precipParamName = PRECIP_VALUE_MAP[this.config.precipitationValue];
      		const precipitationValue = this.#paramValue(weatherData, precipParamName);
      		const pcat = this.#paramValue(weatherData, "predominant_precipitation_type_at_surface");
      
      		// SNOW1gv1 precipitation type mapping (differs from PMP3gv2!):
      		//   0  = no precipitation
      		//   1  = rain
      		//   2  = sleet (snow + rain mix)
      		//   5  = snow / freezing rain
      		//   6  = freezing mixed precipitation
      		//   11 = drizzle / light rain
      		switch (pcat) {
      			case 1: // Rain
      			case 11: // Drizzle / light rain
      				current.rain = precipitationValue;
      				current.precipitationAmount = precipitationValue;
      				break;
      			case 2: // Sleet / mixed rain and snow
      				current.snow = precipitationValue / 2;
      				current.rain = precipitationValue / 2;
      				current.precipitationAmount = precipitationValue;
      				break;
      			case 5: // Snow / freezing rain
      			case 6: // Freezing mixed precipitation
      				current.snow = precipitationValue;
      				current.precipitationAmount = precipitationValue;
      				break;
      			case 0:
      			default:
      				break;
      		}
      
      		return current;
      	}
      
      	#convertWeatherDataGroupedBy (allWeatherData, coordinates, groupBy = "day") {
      		const result = [];
      		let currentWeather = null;
      		let dayWeatherTypes = [];
      
      		const allWeatherObjects = allWeatherData.map((data) => this.#convertWeatherDataToObject(data, coordinates));
      
      		for (const weatherObject of allWeatherObjects) {
      			const objDate = new Date(weatherObject.date);
      
      			// Check if we need a new group (day or hour change)
      			const needNewGroup = !currentWeather || !this.#isSamePeriod(currentWeather.date, objDate, groupBy);
      
      			if (needNewGroup) {
      				currentWeather = {
      					date: objDate,
      					temperature: weatherObject.temperature,
      					minTemperature: Infinity,
      					maxTemperature: -Infinity,
      					snow: 0,
      					rain: 0,
      					precipitationAmount: 0,
      					sunrise: weatherObject.sunrise,
      					sunset: weatherObject.sunset
      				};
      				dayWeatherTypes = [];
      				result.push(currentWeather);
      			}
      
      			// Track weather types during daytime
      			const { sunrise: daySunrise, sunset: daySunset } = getSunTimes(objDate, coordinates.lat, coordinates.lon);
      			const isDay = isDayTime(objDate, daySunrise, daySunset);
      
      			if (isDay) {
      				dayWeatherTypes.push(weatherObject.weatherType);
      			}
      
      			// Use median weather type from daytime hours
      			if (dayWeatherTypes.length > 0) {
      				currentWeather.weatherType = dayWeatherTypes[Math.floor(dayWeatherTypes.length / 2)];
      			} else {
      				currentWeather.weatherType = weatherObject.weatherType;
      			}
      
      			// Aggregate min/max and precipitation
      			currentWeather.minTemperature = Math.min(currentWeather.minTemperature, weatherObject.temperature);
      			currentWeather.maxTemperature = Math.max(currentWeather.maxTemperature, weatherObject.temperature);
      			currentWeather.snow += weatherObject.snow;
      			currentWeather.rain += weatherObject.rain;
      			currentWeather.precipitationAmount += weatherObject.precipitationAmount;
      		}
      
      		return result;
      	}
      
      	#isSamePeriod (date1, date2, groupBy) {
      		if (groupBy === "hour") {
      			return date1.getFullYear() === date2.getFullYear()
      			  && date1.getMonth() === date2.getMonth()
      			  && date1.getDate() === date2.getDate()
      			  && date1.getHours() === date2.getHours();
      		} else { // day
      			return date1.getFullYear() === date2.getFullYear()
      			  && date1.getMonth() === date2.getMonth()
      			  && date1.getDate() === date2.getDate();
      		}
      	}
      
      	/**
      	 * Fill gaps in time series data for forecast/hourly grouping.
      	 * SNOW1gv1 has variable time steps: 1h (0-48h), 2h (49-72h), 6h (73-132h), 12h (133h+).
      	 * Uses "time" key instead of PMP3gv2's "validTime".
      	 */
      	#fillInGaps (data) {
      		if (data.length === 0) return [];
      
      		const result = [];
      		result.push(data[0]);
      
      		for (let i = 1; i < data.length; i++) {
      			const from = new Date(data[i - 1].time);
      			const to = new Date(data[i].time);
      			const hours = Math.floor((to - from) / (1000 * 60 * 60));
      
      			// Fill gaps with previous data point (start at j=1 since j=0 is already pushed)
      			for (let j = 1; j < hours; j++) {
      				const current = { ...data[i - 1] };
      				const newTime = new Date(from);
      				newTime.setHours(from.getHours() + j);
      				current.time = newTime.toISOString();
      				result.push(current);
      			}
      
      			// Push original data point
      			result.push(data[i]);
      		}
      
      		return result;
      	}
      
      	/**
      	 * Extract coordinates from SNOW1gv1 response.
      	 * SNOW1gv1 returns flat GeoJSON Point: { coordinates: [lon, lat] }
      	 * PMP3gv2 returned nested: { coordinates: [[lon, lat]] }
      	 */
      	#resolveCoordinates (data) {
      		const coords = data?.geometry?.coordinates;
      
      		if (Array.isArray(coords) && coords.length >= 2 && typeof coords[0] === "number") {
      			// SNOW1gv1 flat format: [lon, lat]
      			return {
      				lat: coords[1],
      				lon: coords[0]
      			};
      		}
      
      		Log.warn("[smhi] Invalid coordinate structure in response, using config values");
      		return {
      			lat: this.config.lat,
      			lon: this.config.lon
      		};
      	}
      
      	/**
      	 * Calculate apparent (feels-like) temperature using humidity and wind.
      	 * Uses SNOW1gv1 parameter names.
      	 */
      	#calculateApparentTemperature (weatherData) {
      		const Ta = this.#paramValue(weatherData, "air_temperature");
      		const rh = this.#paramValue(weatherData, "relative_humidity");
      		const ws = this.#paramValue(weatherData, "wind_speed");
      
      		if (Ta === null || rh === null || ws === null) {
      			return Ta; // Fallback to raw temperature if data missing
      		}
      
      		const p = (rh / 100) * 6.105 * Math.exp((17.27 * Ta) / (237.7 + Ta));
      		return Ta + 0.33 * p - 0.7 * ws - 4;
      	}
      
      	/**
      	 * Get parameter value from SNOW1gv1 flat data structure.
      	 * SNOW1gv1: weatherData.data.parameter_name (direct property access)
      	 * PMP3gv2 used: weatherData.parameters.find(p => p.name === name).values[0]
      	 *
      	 * Returns null if parameter missing or equals SMHI missing value (9999).
      	 */
      	#paramValue (weatherData, name) {
      		const value = weatherData.data?.[name];
      
      		if (value === undefined || value === null) {
      			return null;
      		}
      
      		// SMHI uses 9999 as missing value sentinel for all parameters
      		if (value === 9999) {
      			return null;
      		}
      
      		return value;
      	}
      
      	/**
      	 * Convert SMHI symbol_code (1-27) to MagicMirror weather icon names.
      	 * Symbol codes are identical between PMP3gv2 and SNOW1gv1.
      	 */
      	#convertWeatherType (input, isDayTime) {
      		switch (input) {
      			case 1:
      				return isDayTime ? "day-sunny" : "night-clear"; // Clear sky
      			case 2:
      				return isDayTime ? "day-sunny-overcast" : "night-partly-cloudy"; // Nearly clear sky
      			case 3:
      			case 4:
      				return isDayTime ? "day-cloudy" : "night-cloudy"; // Variable/halfclear cloudiness
      			case 5:
      			case 6:
      				return "cloudy"; // Cloudy/overcast
      			case 7:
      				return "fog";
      			case 8:
      			case 9:
      			case 10:
      				return "showers"; // Light/moderate/heavy rain showers
      			case 11:
      			case 21:
      				return "thunderstorm";
      			case 12:
      			case 13:
      			case 14:
      			case 22:
      			case 23:
      			case 24:
      				return "sleet"; // Light/moderate/heavy sleet (showers)
      			case 15:
      			case 16:
      			case 17:
      			case 25:
      			case 26:
      			case 27:
      				return "snow"; // Light/moderate/heavy snow (showers/fall)
      			case 18:
      			case 19:
      			case 20:
      				return "rain"; // Light/moderate/heavy rain
      			default:
      				return null;
      		}
      	}
      
      	/**
      	 * Build SNOW1gv1 forecast URL.
      	 * Changed from: pmp3g/version/2
      	 * Changed to:   snow1g/version/1
      	 */
      	#getUrl () {
      		const lon = this.config.lon.toFixed(6);
      		const lat = this.config.lat.toFixed(6);
      		return `https://opendata-download-metfcst.smhi.se/api/category/snow1g/version/1/geotype/point/lon/${lon}/lat/${lat}/data.json`;
      	}
      }
      
      module.exports = SMHIProvider;
      
      posted in System
      C
      cgillinger

    Latest posts made by cgillinger

    • RE: Swedish Weather provider SMHI has deprecated their API and replaced it

      @sdetweil Yes, done and merged now!

      posted in System
      C
      cgillinger
    • Swedish Weather provider SMHI has deprecated their API and replaced it

      Heads up for anyone using the Swedish SMHI weather provider in MagicMirror²!

      As of March 31, 2026, SMHI has deprecated their PMP3gv2 forecast API. It now returns HTTP 404.

      I noticed this, this week when my MM suddenly were empty of weather reports.

      The replacement is SNOW1gv1, hosted on the same domain but with a different data structure.

      What changed in the API

      • URL path: pmp3g/version/2 → snow1g/version/1
      • Time key: validTime → time
      • Data structure: The nested parameters[] array is replaced by a flat data object (e.g. entry.data.air_temperature instead of entry.parameters.find(p => p.name === 't').values[0])
      • Parameter names: Short codes replaced by human-readable names (t → air_temperature, ws → wind_speed, Wsymb2 → symbol_code, etc.)
      • Coordinates: Response now returns flat [lon, lat] instead of nested [[lon, lat]]
      • approvedtime.json endpoint: Removed entirely — use referenceTime from the data response instead

      Weather symbol codes (1–27) remain unchanged, so icon mapping still works as before.

      Updated provider

      I’ve updated the smhi.js provider to work with SNOW1gv1. Key changes:

      • New API URL
      • Rewritten data parser for the flat data object structure
      • All parameter names mapped to their new equivalents
      • Coordinate parsing updated for the new format
      • Added missing value handling (SMHI uses 9999 as sentinel)
      • Added support for precipitation type 11 (drizzle, new in SNOW1gv1)
      • Backward compatible — existing config.js settings (including precipitationValue) work without changes

      Tested and confirmed working with type: "daily" on a Raspberry Pi. The SNOW1gv1 API responds correctly and the weather module displays data as expected.

      Code:

      const Log = require("logger");
      const { getSunTimes, isDayTime, validateCoordinates } = require("../provider-utils");
      const HTTPFetcher = require("#http_fetcher");
      
      /**
       * Server-side weather provider for SMHI (Swedish Meteorological and Hydrological Institute)
       * Sweden only, metric system
       *
       * API: SNOW1gv1 — https://opendata.smhi.se/metfcst/snow1gv1
       * Migrated from PMP3gv2 (deprecated 2026-03-31, returns HTTP 404)
       *
       * Version: 2.0.1 (2026-04-02)
       *
       * Key differences from PMP3gv2:
       * - URL: snow1g/version/1 (was pmp3g/version/2)
       * - Time key: "time" (was "validTime")
       * - Data structure: flat object entry.data.X (was parameters[].find().values[0])
       * - Parameter names: human-readable (air_temperature, wind_speed, etc.)
       * - Coordinates: flat [lon, lat] (was nested [[lon, lat]])
       * - Precipitation types: different value mapping (1=rain, not snow)
       */
      
      /**
       * Maps user-facing config precipitationValue to SNOW1gv1 parameter names.
       * Maintains backward compatibility with existing MagicMirror configs.
       */
      const PRECIP_VALUE_MAP = {
      	pmin: "precipitation_amount_min",
      	pmean: "precipitation_amount_mean",
      	pmedian: "precipitation_amount_median",
      	pmax: "precipitation_amount_max"
      };
      
      class SMHIProvider {
      	constructor (config) {
      		this.config = {
      			lat: 0,
      			lon: 0,
      			precipitationValue: "pmedian", // pmin, pmean, pmedian, pmax
      			type: "current",
      			updateInterval: 5 * 60 * 1000,
      			...config
      		};
      
      		// Validate precipitationValue
      		if (!Object.keys(PRECIP_VALUE_MAP).includes(this.config.precipitationValue)) {
      			Log.warn(`[smhi] Invalid precipitationValue: ${this.config.precipitationValue}, using pmedian`);
      			this.config.precipitationValue = "pmedian";
      		}
      
      		this.fetcher = null;
      		this.onDataCallback = null;
      		this.onErrorCallback = null;
      	}
      
      	initialize () {
      		try {
      			// SMHI requires max 6 decimal places
      			validateCoordinates(this.config, 6);
      			this.#initializeFetcher();
      		} catch (error) {
      			Log.error("[smhi] Initialization failed:", error);
      			if (this.onErrorCallback) {
      				this.onErrorCallback({
      					message: error.message,
      					translationKey: "MODULE_ERROR_UNSPECIFIED"
      				});
      			}
      		}
      	}
      
      	setCallbacks (onData, onError) {
      		this.onDataCallback = onData;
      		this.onErrorCallback = onError;
      	}
      
      	start () {
      		if (this.fetcher) {
      			this.fetcher.startPeriodicFetch();
      		}
      	}
      
      	stop () {
      		if (this.fetcher) {
      			this.fetcher.clearTimer();
      		}
      	}
      
      	#initializeFetcher () {
      		const url = this.#getUrl();
      
      		this.fetcher = new HTTPFetcher(url, {
      			reloadInterval: this.config.updateInterval,
      			logContext: "weatherprovider.smhi"
      		});
      
      		this.fetcher.on("response", async (response) => {
      			try {
      				const data = await response.json();
      				this.#handleResponse(data);
      			} catch (error) {
      				Log.error("[smhi] Failed to parse JSON:", error);
      				if (this.onErrorCallback) {
      					this.onErrorCallback({
      						message: "Failed to parse API response",
      						translationKey: "MODULE_ERROR_UNSPECIFIED"
      					});
      				}
      			}
      		});
      
      		this.fetcher.on("error", (errorInfo) => {
      			if (this.onErrorCallback) {
      				this.onErrorCallback(errorInfo);
      			}
      		});
      	}
      
      	#handleResponse (data) {
      		try {
      			if (!data.timeSeries || !Array.isArray(data.timeSeries)) {
      				throw new Error("Invalid weather data");
      			}
      
      			const coordinates = this.#resolveCoordinates(data);
      			let weatherData;
      
      			switch (this.config.type) {
      				case "current":
      					weatherData = this.#generateCurrentWeather(data.timeSeries, coordinates);
      					break;
      				case "forecast":
      				case "daily":
      					weatherData = this.#generateForecast(data.timeSeries, coordinates);
      					break;
      				case "hourly":
      					weatherData = this.#generateHourly(data.timeSeries, coordinates);
      					break;
      				default:
      					Log.error(`[smhi] Unknown weather type: ${this.config.type}`);
      					if (this.onErrorCallback) {
      						this.onErrorCallback({
      							message: `Unknown weather type: ${this.config.type}`,
      							translationKey: "MODULE_ERROR_UNSPECIFIED"
      						});
      					}
      					return;
      			}
      
      			if (this.onDataCallback) {
      				this.onDataCallback(weatherData);
      			}
      		} catch (error) {
      			Log.error("[smhi] Error processing weather data:", error);
      			if (this.onErrorCallback) {
      				this.onErrorCallback({
      					message: error.message,
      					translationKey: "MODULE_ERROR_UNSPECIFIED"
      				});
      			}
      		}
      	}
      
      	#generateCurrentWeather (timeSeries, coordinates) {
      		const closest = this.#getClosestToCurrentTime(timeSeries);
      		return this.#convertWeatherDataToObject(closest, coordinates);
      	}
      
      	#generateForecast (timeSeries, coordinates) {
      		const filled = this.#fillInGaps(timeSeries);
      		return this.#convertWeatherDataGroupedBy(filled, coordinates, "day");
      	}
      
      	#generateHourly (timeSeries, coordinates) {
      		const filled = this.#fillInGaps(timeSeries);
      		return this.#convertWeatherDataGroupedBy(filled, coordinates, "hour");
      	}
      
      	/**
      	 * Find the time series entry closest to the current time.
      	 * SNOW1gv1 uses "time" instead of PMP3gv2's "validTime".
      	 */
      	#getClosestToCurrentTime (times) {
      		const now = new Date();
      		let minDiff = null;
      		let closest = times[0];
      
      		for (const time of times) {
      			const entryTime = new Date(time.time);
      			const diff = Math.abs(entryTime - now);
      
      			if (minDiff === null || diff < minDiff) {
      				minDiff = diff;
      				closest = time;
      			}
      		}
      
      		return closest;
      	}
      
      	/**
      	 * Convert a single SNOW1gv1 time series entry to MagicMirror weather object.
      	 *
      	 * SNOW1gv1 data structure: entry.data.parameter_name (flat object)
      	 * PMP3gv2 used: entry.parameters[{name, values}] (array of objects)
      	 */
      	#convertWeatherDataToObject (weatherData, coordinates) {
      		const date = new Date(weatherData.time);
      		const { sunrise, sunset } = getSunTimes(date, coordinates.lat, coordinates.lon);
      		const isDay = isDayTime(date, sunrise, sunset);
      
      		const current = {
      			date: date,
      			humidity: this.#paramValue(weatherData, "relative_humidity"),
      			temperature: this.#paramValue(weatherData, "air_temperature"),
      			windSpeed: this.#paramValue(weatherData, "wind_speed"),
      			windFromDirection: this.#paramValue(weatherData, "wind_from_direction"),
      			weatherType: this.#convertWeatherType(this.#paramValue(weatherData, "symbol_code"), isDay),
      			feelsLikeTemp: this.#calculateApparentTemperature(weatherData),
      			sunrise: sunrise,
      			sunset: sunset,
      			snow: 0,
      			rain: 0,
      			precipitationAmount: 0
      		};
      
      		// Map user config (pmedian/pmean/pmin/pmax) to SNOW1gv1 parameter name
      		const precipParamName = PRECIP_VALUE_MAP[this.config.precipitationValue];
      		const precipitationValue = this.#paramValue(weatherData, precipParamName);
      		const pcat = this.#paramValue(weatherData, "predominant_precipitation_type_at_surface");
      
      		// SNOW1gv1 precipitation type mapping (differs from PMP3gv2!):
      		//   0  = no precipitation
      		//   1  = rain
      		//   2  = sleet (snow + rain mix)
      		//   5  = snow / freezing rain
      		//   6  = freezing mixed precipitation
      		//   11 = drizzle / light rain
      		switch (pcat) {
      			case 1: // Rain
      			case 11: // Drizzle / light rain
      				current.rain = precipitationValue;
      				current.precipitationAmount = precipitationValue;
      				break;
      			case 2: // Sleet / mixed rain and snow
      				current.snow = precipitationValue / 2;
      				current.rain = precipitationValue / 2;
      				current.precipitationAmount = precipitationValue;
      				break;
      			case 5: // Snow / freezing rain
      			case 6: // Freezing mixed precipitation
      				current.snow = precipitationValue;
      				current.precipitationAmount = precipitationValue;
      				break;
      			case 0:
      			default:
      				break;
      		}
      
      		return current;
      	}
      
      	#convertWeatherDataGroupedBy (allWeatherData, coordinates, groupBy = "day") {
      		const result = [];
      		let currentWeather = null;
      		let dayWeatherTypes = [];
      
      		const allWeatherObjects = allWeatherData.map((data) => this.#convertWeatherDataToObject(data, coordinates));
      
      		for (const weatherObject of allWeatherObjects) {
      			const objDate = new Date(weatherObject.date);
      
      			// Check if we need a new group (day or hour change)
      			const needNewGroup = !currentWeather || !this.#isSamePeriod(currentWeather.date, objDate, groupBy);
      
      			if (needNewGroup) {
      				currentWeather = {
      					date: objDate,
      					temperature: weatherObject.temperature,
      					minTemperature: Infinity,
      					maxTemperature: -Infinity,
      					snow: 0,
      					rain: 0,
      					precipitationAmount: 0,
      					sunrise: weatherObject.sunrise,
      					sunset: weatherObject.sunset
      				};
      				dayWeatherTypes = [];
      				result.push(currentWeather);
      			}
      
      			// Track weather types during daytime
      			const { sunrise: daySunrise, sunset: daySunset } = getSunTimes(objDate, coordinates.lat, coordinates.lon);
      			const isDay = isDayTime(objDate, daySunrise, daySunset);
      
      			if (isDay) {
      				dayWeatherTypes.push(weatherObject.weatherType);
      			}
      
      			// Use median weather type from daytime hours
      			if (dayWeatherTypes.length > 0) {
      				currentWeather.weatherType = dayWeatherTypes[Math.floor(dayWeatherTypes.length / 2)];
      			} else {
      				currentWeather.weatherType = weatherObject.weatherType;
      			}
      
      			// Aggregate min/max and precipitation
      			currentWeather.minTemperature = Math.min(currentWeather.minTemperature, weatherObject.temperature);
      			currentWeather.maxTemperature = Math.max(currentWeather.maxTemperature, weatherObject.temperature);
      			currentWeather.snow += weatherObject.snow;
      			currentWeather.rain += weatherObject.rain;
      			currentWeather.precipitationAmount += weatherObject.precipitationAmount;
      		}
      
      		return result;
      	}
      
      	#isSamePeriod (date1, date2, groupBy) {
      		if (groupBy === "hour") {
      			return date1.getFullYear() === date2.getFullYear()
      			  && date1.getMonth() === date2.getMonth()
      			  && date1.getDate() === date2.getDate()
      			  && date1.getHours() === date2.getHours();
      		} else { // day
      			return date1.getFullYear() === date2.getFullYear()
      			  && date1.getMonth() === date2.getMonth()
      			  && date1.getDate() === date2.getDate();
      		}
      	}
      
      	/**
      	 * Fill gaps in time series data for forecast/hourly grouping.
      	 * SNOW1gv1 has variable time steps: 1h (0-48h), 2h (49-72h), 6h (73-132h), 12h (133h+).
      	 * Uses "time" key instead of PMP3gv2's "validTime".
      	 */
      	#fillInGaps (data) {
      		if (data.length === 0) return [];
      
      		const result = [];
      		result.push(data[0]);
      
      		for (let i = 1; i < data.length; i++) {
      			const from = new Date(data[i - 1].time);
      			const to = new Date(data[i].time);
      			const hours = Math.floor((to - from) / (1000 * 60 * 60));
      
      			// Fill gaps with previous data point (start at j=1 since j=0 is already pushed)
      			for (let j = 1; j < hours; j++) {
      				const current = { ...data[i - 1] };
      				const newTime = new Date(from);
      				newTime.setHours(from.getHours() + j);
      				current.time = newTime.toISOString();
      				result.push(current);
      			}
      
      			// Push original data point
      			result.push(data[i]);
      		}
      
      		return result;
      	}
      
      	/**
      	 * Extract coordinates from SNOW1gv1 response.
      	 * SNOW1gv1 returns flat GeoJSON Point: { coordinates: [lon, lat] }
      	 * PMP3gv2 returned nested: { coordinates: [[lon, lat]] }
      	 */
      	#resolveCoordinates (data) {
      		const coords = data?.geometry?.coordinates;
      
      		if (Array.isArray(coords) && coords.length >= 2 && typeof coords[0] === "number") {
      			// SNOW1gv1 flat format: [lon, lat]
      			return {
      				lat: coords[1],
      				lon: coords[0]
      			};
      		}
      
      		Log.warn("[smhi] Invalid coordinate structure in response, using config values");
      		return {
      			lat: this.config.lat,
      			lon: this.config.lon
      		};
      	}
      
      	/**
      	 * Calculate apparent (feels-like) temperature using humidity and wind.
      	 * Uses SNOW1gv1 parameter names.
      	 */
      	#calculateApparentTemperature (weatherData) {
      		const Ta = this.#paramValue(weatherData, "air_temperature");
      		const rh = this.#paramValue(weatherData, "relative_humidity");
      		const ws = this.#paramValue(weatherData, "wind_speed");
      
      		if (Ta === null || rh === null || ws === null) {
      			return Ta; // Fallback to raw temperature if data missing
      		}
      
      		const p = (rh / 100) * 6.105 * Math.exp((17.27 * Ta) / (237.7 + Ta));
      		return Ta + 0.33 * p - 0.7 * ws - 4;
      	}
      
      	/**
      	 * Get parameter value from SNOW1gv1 flat data structure.
      	 * SNOW1gv1: weatherData.data.parameter_name (direct property access)
      	 * PMP3gv2 used: weatherData.parameters.find(p => p.name === name).values[0]
      	 *
      	 * Returns null if parameter missing or equals SMHI missing value (9999).
      	 */
      	#paramValue (weatherData, name) {
      		const value = weatherData.data?.[name];
      
      		if (value === undefined || value === null) {
      			return null;
      		}
      
      		// SMHI uses 9999 as missing value sentinel for all parameters
      		if (value === 9999) {
      			return null;
      		}
      
      		return value;
      	}
      
      	/**
      	 * Convert SMHI symbol_code (1-27) to MagicMirror weather icon names.
      	 * Symbol codes are identical between PMP3gv2 and SNOW1gv1.
      	 */
      	#convertWeatherType (input, isDayTime) {
      		switch (input) {
      			case 1:
      				return isDayTime ? "day-sunny" : "night-clear"; // Clear sky
      			case 2:
      				return isDayTime ? "day-sunny-overcast" : "night-partly-cloudy"; // Nearly clear sky
      			case 3:
      			case 4:
      				return isDayTime ? "day-cloudy" : "night-cloudy"; // Variable/halfclear cloudiness
      			case 5:
      			case 6:
      				return "cloudy"; // Cloudy/overcast
      			case 7:
      				return "fog";
      			case 8:
      			case 9:
      			case 10:
      				return "showers"; // Light/moderate/heavy rain showers
      			case 11:
      			case 21:
      				return "thunderstorm";
      			case 12:
      			case 13:
      			case 14:
      			case 22:
      			case 23:
      			case 24:
      				return "sleet"; // Light/moderate/heavy sleet (showers)
      			case 15:
      			case 16:
      			case 17:
      			case 25:
      			case 26:
      			case 27:
      				return "snow"; // Light/moderate/heavy snow (showers/fall)
      			case 18:
      			case 19:
      			case 20:
      				return "rain"; // Light/moderate/heavy rain
      			default:
      				return null;
      		}
      	}
      
      	/**
      	 * Build SNOW1gv1 forecast URL.
      	 * Changed from: pmp3g/version/2
      	 * Changed to:   snow1g/version/1
      	 */
      	#getUrl () {
      		const lon = this.config.lon.toFixed(6);
      		const lat = this.config.lat.toFixed(6);
      		return `https://opendata-download-metfcst.smhi.se/api/category/snow1g/version/1/geotype/point/lon/${lon}/lat/${lat}/data.json`;
      	}
      }
      
      module.exports = SMHIProvider;
      
      posted in System
      C
      cgillinger
    • RE: Made a birthday module

      @plainbroke Yes, that might do the trick! If MagicMirror manages to keep the two settings in sync.

      posted in Entertainment
      C
      cgillinger
    • RE: Made a birthday module

      @xIExodusIx Hi!

      First, the green debug code was displayed because I forgot to set it to “false” by default. You can change this in config.js, but I’ll update the default setting so it only appears when the user wants it (ie when setting “true” in config.js).

      Second, the module still has some quirks in its startup behavior—it requires a full cycle before everything runs smoothly. I’m working on it, but to be honest, it’s a low-priority fix since it’s a fringe case. Typically, the module will be running continuously, so birthday initialization should trigger when the mirror is already active. That’s probably why you’re not seeing the confetti right away.

      Third, regarding the different versions of Fireworks—absolutely! The reason there are multiple versions is simply that I left them in from my experiments. I didn’t originally plan to include that functionality, but once the core features are fully ironed out, I’d be happy to refine and expand on it.

      As for positioning, the module is currently designed to use the entire screen. I might look into allowing more customization in the future, but for now, it just places the message in the center and then launches confetti and fireworks.

      When you say it “ignores the page,” are you referring to the page cycling?

      I’ve also attached my test config.js. It has three pages, and after cycling through them to align the timings, the expected behavior should be:

      Page 1 – Normal modules
      Page 2 – Birthday celebration with dimming
      Page 3 – Birthday celebration without dimming
      

      Test config.js (which needs MMM-page-indicator). I’ve let this run for about an hour now, and everything (apart from weather module because its not part of the test) works as intended:

      /* MagicMirror² Config for testing MMM-Birthday-Paged with immersiveMode */
      let config = {
          address: "localhost",
          port: 8080,
          basePath: "/", 
          ipWhitelist: ["127.0.0.1", "::ffff:127.0.0.1", "::1"],
          useHttps: false, 
          httpsPrivateKey: "", 
          httpsCertificate: "", 
      
          language: "en",
          locale: "en-US",
          logLevel: ["INFO", "LOG", "WARN", "ERROR", "DEBUG"],
          timeFormat: 24,
          units: "metric",
      
          modules: [
              {
                  module: "alert",
              },
              {
                  module: "clock",
                  position: "top_left",
                  classes: "page1 page3" // Show on page 1 and 3
              },
              {
                  module: "calendar",
                  header: "Holidays",
                  position: "top_left",
                  classes: "page1 page3", // Show on page 1 and 3
                  config: {
                      calendars: [
                          {
                              symbol: "calendar-check",
                              url: "webcal://www.calendarlabs.com/ical-calendar/ics/76/US_Holidays.ics"
                          }
                      ]
                  }
              },
              {
                  module: "weather",
                  position: "top_right",
                  classes: "page1 page3", // Show on page 1 and 3
                  config: {
                      weatherProvider: "yr",
                      type: "current",
                      location: "Stockholm",
                      locationID: "2673730", // Stockholm's ID
                      roundTemp: true
                  }
              },
              {
                  module: "compliments",
                  position: "lower_third",
                  classes: "page1 page3", // Show on page 1 and 3
              },
              // MMM-Birthday-Paged with immersiveMode = true (dims modules)
              {
                  module: "MMM-Birthday-Paged",
                  position: "middle_center",
                  classes: "page2", // Show on page 2
                  config: {
                      birthdays: [
                          // Today's date (March 16, 2025) with name John
                          { name: "John", date: "03-16" } 
                      ],
                      fireworkDuration: "infinite",
                      confettiDuration: "infinite",
                      immersiveMode: true, // Dims other modules
                      debug: true,
                      startupDelay: 500 // Shorter startup delay for testing
                  }
              },
              // MMM-Birthday-Paged with immersiveMode = false (original module behavior)
              {
                  module: "MMM-Birthday-Paged",
                  position: "middle_center",
                  classes: "page3", // Show on page 3
                  config: {
                      birthdays: [
                          // Today's date (March 16, 2025) with name John
                          { name: "John", date: "03-16" } 
                      ],
                      fireworkDuration: "infinite",
                      confettiDuration: "infinite",
                      immersiveMode: false, // Does NOT dim other modules
                      debug: true,
                      startupDelay: 500 // Shorter startup delay for testing
                  }
              },
              // Page indicator to see which page is showing
              {
                  module: "MMM-page-indicator",
                  position: "bottom_bar",
                  config: {
                      pages: 3
                  }
              },
              // MMM-pages for page switching
              {
                  module: "MMM-pages",
                  config: {
                      modules: [
                          ["page1"],   // Page 1 - Normal modules
                          ["page2"],   // Page 2 - Birthday celebration with dimming
                          ["page3"]    // Page 3 - Birthday celebration without dimming
                      ],
                      fixed: ["alert", "MMM-page-indicator"],
                      rotationTime: 15000, // 15 seconds between page changes
                      rotationDelay: 15000,
                      animationTime: 1000
                  }
              }
          ]
      };
      
      /*************** DO NOT EDIT THE LINE BELOW ***************/
      if (typeof module !== "undefined") {module.exports = config;}
      
      posted in Entertainment
      C
      cgillinger
    • RE: Made a birthday module

      @xIExodusIx I believe I’ve solved the issue where MMM-Birthday-Pages was taking over the entire screen when active. There’s now a new toggle, immersiveMode, which can be set to either “true” or “false”.

      "false" allows the birthday celebration to overlay existing modules while keeping them visible.
      "true" means only the birthday celebration is displayed, hiding everything else.
      

      This also means you can have different behaviors on different Pages.

      That said, I’ve only had time for a quick test, and it worked as expected on my Linux Mint installation. To be safe, I’ve uploaded it to a separate test repository. If it proves to be stable, I’ll merge it into the main one (I’m sure there are easier ways to do this, but I suck at Git). Repo URL:
      https://github.com/cgillinger/mmm-Birthday-paged-test (and please note that you need to rename the folder to “mmm-Birthday-paged” after cloning)

      So, give it a try and let me know how it goes!

      posted in Entertainment
      C
      cgillinger
    • RE: Let it snow now Magic Mirror

      @sdetweil Yes, I understand, but it’s not the cost itself that’s the issue (although it would have been a problem if there was a cost). The concern is having to provide my credit card details. It’s not that I distrust OpenWeather, but I prefer not to share them unnecessarily. You never know, for example, if they might get hacked.

      posted in Entertainment
      C
      cgillinger
    • RE: Let it snow now Magic Mirror

      Hey @Sebi76-0! I’m not sure what’s causing the issue. Could you share any error messages you’re seeing? Also, which version are you using?

      Just so you know, this module has gone through a couple of transformations—first evolving into Dynamic Snow (available at: https://github.com/cgillinger/MMM-DynamicSnow), and later into Weather Effects (which you can find here: https://github.com/cgillinger/MMM-WeatherEffects).

      I don’t personally use OpenWeather, and from what I understand, they now require a credit card for access. So, I don’t think I’ll be getting an API key anytime soon.

      posted in Entertainment
      C
      cgillinger
    • RE: Let it snow now Magic Mirror

      @det I actually did add rain: https://github.com/cgillinger/MMM-WeatherEffects

      Cheers,
      C

      posted in Entertainment
      C
      cgillinger
    • RE: Made a birthday module

      @xIExodusIx said in Made a birthday module:

      I’m using a Raspberry Pi 4B 4GB with RaspiOS Bookworm
      MM Version is 2.30,
      npm Version is 10.9.2,
      node Version is 23.6.1.

      I think RaspiOS is missing the font. It might be solved by issuing:

      sudo apt update
      sudo apt upgrade -y
      sudo apt install -y fonts-noto-color-emoji fonts-symbola ttf-ancient-fonts
      

      and then rebuild font cache

      sudo fc-cache -f -v
      

      Then restarting Magic Mirror and/or Pi.

      And if I get the time, Ill look into a more elegant solution.

      posted in Entertainment
      C
      cgillinger
    • RE: Made a birthday module

      @xIExodusIx On the first issue, that is, currently, by design as I outline in my previous post. I’m looking into if there is a method to fix it, but Im not there yet.

      On the second issue that sounds like a font problem. What system are you on?

      posted in Entertainment
      C
      cgillinger