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 WOdroid-N2
ArchitectureARMv6arm64
NEON supportNoYes
RAM512MB4GB
Chromium/FirefoxNoYes
StorageSD cardeMMC

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:

1
2
# Run from SD card boot — wipes eMMC bootloader sector only
dd if=/dev/zero of=/dev/mmcblk0 bs=1M count=4

Then flash the Ubuntu image:

1
dd if=ubuntu-22.04-minimal-odroid-n2.img of=/dev/mmcblk0 bs=4M status=progress

Node.js

Ubuntu’s packaged Node.js is too old for MagicMirror². The NodeSource official arm64 repository provides a current LTS version:

1
2
curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs

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:

1
2
sudo add-apt-repository ppa:mozillateam/ppa
sudo apt install -y firefox

The PPA must be pinned before installing to prevent the snap version from being pulled in as a dependency:

1
2
3
4
# /etc/apt/preferences.d/mozilla-firefox
Package: firefox*
Pin: release o=LP-PPA-mozillateam
Pin-Priority: 1001

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:

  1. systemd getty override — configures TTY1 to auto-login as the mm user
  2. .bash_profile — starts X when logged in on TTY1
  3. MagicMirror systemd service — launched by X session startup
1
2
3
4
# /etc/systemd/system/getty@tty1.service.d/override.conf
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin mm --noclear %I $TERM
1
2
3
4
# ~/.bash_profile
if [[ -z $DISPLAY && $XDG_VTNR -eq 1 ]]; then
  exec startx
fi
1
2
3
4
5
# ~/.xinitrc
xset s off
xset -dpms
xset s noblank
exec npm start --prefix /opt/MagicMirror

This boots directly into the MagicMirror display with no login prompt or desktop overhead.


Screen Layout

The layout uses PaperMod-style zone positioning:

ZoneModule
top_barStock ticker (ASX + oil)
top_leftClock and date
top_centerQueenstown Airport webcam
top_rightCurrent weather + 5-day forecast
bottom_leftSpotify now playing
bottom_centerQueenstown traffic camera
bottom_rightHome Assistant sensor panel
bottom_barNews ticker
fullscreen_belowNASA 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:

1
2
url: ["https://www.trafficnz.info/camera/622.jpg"],
updateInterval: 60 * 1000,

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:

  1. A cron job fetches the image every minute and saves it locally:
1
2
3
*/1 * * * * wget -q --referer="https://www.queenstownairport.co.nz/" \
  -O /opt/webcams/airport.jpg \
  "https://www.queenstownairport.co.nz/webcams/stands.jpg"
  1. A Python HTTP server serves /opt/webcams/ locally on port 8081:
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# /etc/systemd/system/webcams.service
[Unit]
Description=Webcam static file server
After=network.target

[Service]
ExecStart=python3 -m http.server 8081 --directory /opt/webcams
Restart=always
User=mm

[Install]
WantedBy=multi-user.target
  1. 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/node vs /usr/local/bin/node breaks 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 --referer or 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 dd of the first 4MB clears it

Comments