Building your own magic mirror is a gratifying experience, and the result is an unobtrusive piece of furniture; and a visual part of your smart home. Open source projects such as MagicMirror² may assist you in creating your own custom smart mirror interfaces.
It only takes an afternoon to create your own plugin, if your are familiar with JavaScript and Node.js, and takes additional 1-2 hours of testing.
A MagicMirror² module idea
I regularly take out my trash, and from time to time forgot to, or did not know for sure, which type will be disposed at a specific date. What I hoped for, was, that the city I live in exposes a live calendar f.e. a calDAV server, that allows you to access the required data without relying on email, a third party app, and another ecosystem I did not want to subscribe to. Well, the city government had not offered its citizen this service. The ironic thing is: They have a public accessible API, that provides this kind of data.
Initialize the MagicMirror² module
A MagicMirror² module consists of a core module script, to display the data, and a node helper script, to handle the background tasks required. Further module structure recommendations can be found here.
Run the following commands to prepare the new module, adjust the core module name to match your module name.
## New node package manager project
npm init
## Init core module script, named by module name
touch MMM-WasteCollectionWuerzburg.js
## Init node helper script
touch node_helper.js
Accessing the API data
The node helper script is necessary to connect to the API, and forwards the data to the core module via socket connection.
'use strict';
const Helper = require('node_helper');
const Log = require('logger');
module.exports = Helper.create({
config: {},
getData: async function() {},
processData: function(data) {},
socketNotificationReceived: function(name, payload) {}
});
The getData() function connects to the API. We limit the resultset to 50 items, and fetch the data in ascending date order. To narrow down the search, the user is allowed to define city districts, to only get the data for the districts he lives in. The data returned by the API is being socket sent to the core module script.
...
module.exports = Helper.create({
config: {
sourceUrl: 'https://opendata.wuerzburg.de/api/explore/',
apiVersion: 'v2.1',
limit: 50,
orderBy: 'start ASC',
fields: ['kategorie', 'start', 'bild', 'stadtteil_name']
},
getData: async function(data) {
var _this = this;
const currentDate = new Date();
var where = 'start >= \'' + currentDate.toISOString().split('T')[0] + '\'';
var districts = data.districts || [];
if (districts.length > 0) {
where += 'AND stadtteil_name IN (' + districts.map(a => `'${a}'`).join() + ')';
}
var urlSearchParams = new URLSearchParams({
limit: this.config.limit,
order_by: this.config.orderBy,
select: this.config.fields.join(),
where: where
});
var url = this.config.sourceUrl + this.config.apiVersion + '/catalog/datasets/abfallkalender-wuerzburg/records?' + urlSearchParams.toString();
Log.log(`[${this.name}] Fetching opendata.wuerzburg.de data via ${url} ...`);
try {
const response = await fetch(url, { method: 'GET' });
if (!response.ok) {
throw new Error('Invalid API request');
}
const res = await response.json();
if (!res || !res.results || res.results.length === 0) return;
_this.sendSocketNotification('API_DATA_RECEIVED', {
rows: _this.processData(res.results)
});
} catch (error) {
Log.error(`[${this.name}] ${error}`);
}
},
...
The processData() function prepares the API return data for the socket processing.
...
processData: function(data) {
var byDate = {};
for (var i = 0; i < data.length; i++) {
data[i].cat = deUmlaut(data[i].kategorie);
data[i].district = deUmlaut(data[i].stadtteil_name);
if (typeof byDate[data[i].start] !== 'object') {
byDate[data[i].start] = {};
}
if (typeof byDate[data[i].start][data[i].district] !== 'object') {
byDate[data[i].start][data[i].district] = {};
}
byDate[data[i].start][data[i].district][data[i].cat] = data[i];
}
return byDate;
},
...
Displaying the data
The core module script showcases the data provided by the node helper script. Districts and waste types can be filtered by the user. Ditricts, year and waste types can be toggled, so they are not rendered by DOM.
Not to stress out the public API or risk a ban, the interval, at which the module contacts the API, should be limited. I recommended a daily or weekly cycle.
'use strict';
Module.register("MMM-WasteCollectionWuerzburg", {
requiresVersion: "2.1.0",
defaults: {
title: "Abfallkalender Würzburg",
districts: [ // shows all if empty
// 'Altstadt',
// 'Frauenland',
// 'Dürrbach alle mit Hafen',
// 'Grombühl',
// 'Heidingsfeld',
// 'Heuchelhof innen',
// 'Heuchelhof aussen',
// 'Lengfeld',
// 'Lindleinsmühle',
// 'Mainviertel',
// 'Neumühle',
// 'Pilziggrund',
// 'Rottenbauer',
// 'Sanderau',
// 'Steinbachtal',
// 'Versbach',
// 'Zellerau'
],
categories: [ // shows all if empty
// 'Restmüll',
// 'Biomüll',
// 'Gelbe Säcke',
// 'Papier',
// 'Problemmüll-Sammlung',
// 'Wertstoffmobil'
],
updateInterval: 604800*1000, // once a week
rows: {},
showTypes: true, // displays waste type names next to icons
showDistrict: false, // this should be enaled if you use multiple districts
showYear: false,
rowsMax: 5 // max num rows to be displayed; 0 for unlimited
},
...
The start() function launches the module, and asks the node helper script via socket to fetch the required data periodically.
...
getData: function() {
this.sendSocketNotification('GET_DATA', {
districts: this.config.districts
});
},
socketNotificationReceived: function(name, payload) {
if (name !== 'API_DATA_RECEIVED') return this.rows = [];
this.config.rows = payload.rows;
this.updateDom();
},
start: function() {
const _this = this;
setInterval(function() {_this.getData()}, _this.config.updateInterval);
_this.getData();
}
...
The socketNotificationReceived() function applies the given user settings, and renders the received data.
...
getDom: function() {
var tbl = document.createElement("table");
var rowCount = 0;
Object.keys(this.config.rows).forEach(k => {
var districts = this.config.rows[k];
var date = k;
Object.keys(districts).forEach(districtName => {
var categories = districts[districtName];
if (this.config.rowsMax && this.config.rowsMax > 0 && rowCount == this.config.rowsMax) {
return;
}
var tr = this.createDomRow(date, categories);
if (tr) tbl.appendChild(tr);
rowCount++;
});
});
return tbl;
},
createDomRow: function(date, categories) {
var districtName = null;
var catArr = [];
var tr = document.createElement('tr');
/**
* waste type icons & names
*/
var td = document.createElement("td");
td.className = 'waste-icons';
Object.keys(categories).forEach(categoryName => {
var category = categories[categoryName];
if (!districtName) {
districtName = category.stadtteil_name;
}
if (this.config.categories.length === 0 || this.config.categories.indexOf(category.kategorie) > -1) {
catArr.push(category.kategorie);
var img = document.createElement('img');
img.src = category.bild;
td.appendChild(img);
}
});
// skip row if no desired category available
if (catArr.length === 0) return false;
tr.appendChild(td);
/**
* waste type names
*/
if (this.config.showTypes) {
var td = document.createElement("td");
td.innerHTML = catArr.join(', ');
tr.appendChild(td);
}
/**
* district name
*/
if (this.config.showDistrict) {
var td = document.createElement("td");
td.innerHTML = districtName;
tr.appendChild(td);
}
/**
* collection date
*/
var td = document.createElement("td");
var collectionDate = new Date(date);
var dateOptions = {month: "numeric", day: "numeric"};
if (this.config.showYear) {
dateOptions.year = 'numeric';
}
td.innerHTML = collectionDate.toLocaleDateString('de-DE', dateOptions);
tr.appendChild(td);
return tr;
},
...
How to use the plugin
The MagicMirror² implementation provides a config.js to configure your magic mirror instance and add desired modules.
To render this module at the top left position, filtered by district and categories, inject the code below.
...
{
module: "MMM-WasteCollectionWuerzburg",
position: "top_left",
config: {
title: 'Abfallkalender Frauenland',
rowsMax: 2,
districts: ['Frauenland'],
categories: [
'Restmüll',
'Biomüll',
'Gelbe Säcke',
'Papier'
]
}
},
...
Great, we just completed our MagicMirror² module.
The complete code can be accessed here.