Overview
MagicMirror² is an open-source smart display platform built on Electron and Node.js. The idea is simple: a screen on the wall showing whatever information you care about — weather, news, calendar, home sensors — in a clean, always-on layout.
This implementation runs on an Odroid-N2 and is tuned for Queenstown, NZ. The display covers:
- Clock, date, and 5-day weather forecast
- Live Home Assistant sensor states (doors, locks, lights, motion, garden)
- Spotify now playing
- NZ and world news ticker
- ASX mining stock prices and oil futures
- NASA Astronomy Picture of the Day as a rotating background
- Live traffic and airport camera feeds
Hardware: Why Not a Raspberry Pi
The obvious choice for a project like this is a Raspberry Pi — but the hardware on hand was a Pi Zero W, and that turned out to be a dead end.
The Pi Zero W uses an ARMv6 CPU. Modern browsers (Chromium, Firefox) require ARMv7 with NEON floating-point instructions at minimum. MagicMirror² uses Electron (essentially a bundled Chromium), so the Pi Zero W simply cannot run it. The Pi Zero 2 W would work, but wasn’t available.
The Odroid-N2 was already in the parts box. It’s an arm64 board with an Amlogic S922X SoC, 4GB RAM, and eMMC storage. Every ARMv7/NEON requirement is satisfied, and Node.js, Firefox, and Electron all work without modification.
| Pi Zero W | Odroid-N2 | |
|---|---|---|
| Architecture | ARMv6 | arm64 |
| NEON support | No | Yes |
| RAM | 512MB | 4GB |
| Chromium/Firefox | No | Yes |
| Storage | SD card | eMMC |
The N2 is overkill for a dashboard, but it eliminates an entire class of compatibility problems.
OS Setup
The N2 runs Ubuntu 22.04 LTS Minimal (arm64), sourced from Hardkernel’s image repository.
Flashing eMMC
One immediate quirk: the Hardkernel image site is behind Cloudflare, which blocks wget user agents. The image has to be downloaded on a PC and transferred via SCP — then flashed to eMMC from a booted SD card.
If the eMMC previously ran CoreELEC or another Amlogic bootloader, the bootloader sector needs wiping before the new image will boot:
| |
Then flash the Ubuntu image:
| |
Node.js
Ubuntu’s packaged Node.js is too old for MagicMirror². The NodeSource official arm64 repository provides a current LTS version:
| |
Note: NodeSource installs Node to /usr/bin/node, not /usr/local/bin/node. Systemd service files need to reference the correct path.
Firefox
Snap packages do not work on the Hardkernel kernel — the mount namespace implementation is incompatible. Attempting to install Firefox via snap results in a non-starting application with no useful error output.
The solution is the Mozilla-maintained apt PPA:
| |
The PPA must be pinned before installing to prevent the snap version from being pulled in as a dependency:
| |
WiFi
The N2 has no built-in WiFi. A USB Raspberry Pi WiFi dongle using the Ralink RT5370 chipset was used — it auto-detects on Ubuntu without any driver installation.
Auto-Start
MagicMirror² needs to launch at boot into a full-screen browser without a desktop environment or login prompt.
The approach:
- systemd getty override — configures TTY1 to auto-login as the
mmuser .bash_profile— starts X when logged in on TTY1- MagicMirror systemd service — launched by X session startup
| |
| |
| |
This boots directly into the MagicMirror display with no login prompt or desktop overhead.
Screen Layout
The layout uses PaperMod-style zone positioning:
| Zone | Module |
|---|---|
top_bar | Stock ticker (ASX + oil) |
top_left | Clock and date |
top_center | Queenstown Airport webcam |
top_right | Current weather + 5-day forecast |
bottom_left | Spotify now playing |
bottom_center | Queenstown traffic camera |
bottom_right | Home Assistant sensor panel |
bottom_bar | News ticker |
fullscreen_below | NASA APOD rotating background |
Modules
Clock
Built-in MagicMirror module. Configured for 24h format, Pacific/Auckland timezone, and a full date string.
Weather
Built-in weather module, used twice — once for current conditions and once for the 5-day forecast. Both pull from OpenWeatherMap using Queenstown’s coordinates. Current conditions shows humidity, wind direction (as an arrow), and sunrise/sunset times.
Home Assistant
The MMM-HomeAssistantDisplay module connects via WebSocket to Home Assistant. Unlike polling-based approaches, WebSocket means state changes appear on screen within a second or two of occurring.
The panel shows:
- Lounge lights and lobby switch state (with colour-coded icons)
- Front and back door open/closed state
- Front door lock state
- Lobby motion sensor
- Garden watering switch
Each entity uses a Jinja2 template to render an MDI icon with conditional colour alongside the entity state. The Home Assistant host is referenced by hostname rather than IP to avoid TLS certificate issues.
Spotify Now Playing
MMM-NowPlayingOnSpotify polls the Spotify API every 10 seconds to display the currently playing track and album art. Setup requires creating a Spotify developer app, authorising it once to get a refresh token, and supplying the client credentials in the module config. After that it runs unattended — the module handles token refresh automatically.
News
MMM-NewsFeedTicker scrolls headlines from three RSS feeds: RNZ National, Stuff NZ, and BBC World. Several other feeds were tested and dropped:
- 1News: RSS feed broken
- Reuters: CORS blocks embedded RSS
- NZ Herald: RSS feed discontinued
The ticker reloads feeds every 30 minutes and scrolls continuously.
Stock Ticker
MMM-Jast pulls from Yahoo Finance — no API key required. The ticker shows a handful of ASX-listed junior mining stocks alongside WTI crude and Brent futures. It scrolls horizontally across the top of the screen and updates every 5 minutes.
NASA APOD Background
MMM-Wallpaper fetches NASA’s Astronomy Picture of the Day via the NASA Open APIs. It rotates through the last 10 entries, changing image every 30 minutes, with a crossfade transition. A brightness(30%) CSS filter darkens the image so the foreground modules remain readable.
Camera Feeds
Two live camera feeds are embedded using MMM-iFrame:
Traffic Camera
The NZTA traffic camera at Queenstown is publicly accessible and can be embedded directly:
| |
The module reloads the image every 60 seconds.
Airport Camera — The Workaround
The Queenstown Airport webcam cannot be embedded directly. The airport site returns X-Frame-Options: SAMEORIGIN headers, which prevent any <iframe> or image embed from an external origin. Hotlinking the .jpg URL also fails — the server returns an error for requests without the correct Referer header.
The solution is a local proxy:
- A cron job fetches the image every minute and saves it locally:
| |
- A Python HTTP server serves
/opt/webcams/locally on port 8081:
| |
- The MagicMirror module points to
http://localhost:8081/, which serves a small HTML page with JavaScript that forces a cache-busting reload of the image on the same interval.
The result is a live feed that refreshes every 60 seconds, with the airport none the wiser.
Lessons Learned
- ARMv6 is a dead end for modern software — check architecture requirements before choosing hardware for browser-based projects
- Snap packages don’t work on all kernels — always verify snap compatibility early; use the Mozilla PPA for Firefox on Hardkernel systems
- WebSocket beats polling for home automation displays — state changes from Home Assistant appear immediately rather than on the next poll interval
/usr/bin/nodevs/usr/local/bin/nodebreaks systemd services — always verify the Node path after a NodeSource install- Cloudflare blocks wget by default — useful to know when fetching images from protected CDNs; supply a
--refereror use a browser user-agent - eMMC bootloader sectors survive a full wipe — a previous Amlogic bootloader will prevent a new image from booting; a targeted
ddof the first 4MB clears it
Comments