MQTT Projekt Aufbau
Aufbau der MQTT-Umgebung
Diese Seite beschreibt den vollständigen Aufbau der Lab-Umgebung, sodass sie von Grund auf nachgebaut werden kann. Es gibt drei Maschinen:
- mqtt.dkbi.com
- Der Mosquitto-Broker. Vermittelt die Nachrichten zwischen Sensor und Aktor.
- sensor.dkbi.com (Verzeichnis /usr/local/control-switch)
- Sendet Schaltbefehle (publish) und zeigt den Status an (subscribe). Web-Oberfläche mit klickbaren Schaltflächen.
- aktor.dkbi.com (Verzeichnis /usr/local/control)
- Zeigt den Status der Geräte an (nur subscribe). Read-only Web-Oberfläche.
Die eigentliche Absicherung (ohne Passwort, mit Passwort, TLS, 2FA) wird über die Datei .env gesteuert. Der Programmcode bleibt über alle vier Stufen identisch; umgeschaltet wird nur die .env.
Voraussetzungen
Installation von Node.js und npm
- Alte Node.js-Version entfernen, falls vorhanden
- sudo apt update && sudo apt upgrade
- sudo apt install curl
- sudo apt remove nodejs
- Repository für die aktuelle LTS-Version (22.x) hinzufügen
- curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
- Node.js (Version 22.x) und npm installieren
- sudo apt install -y nodejs
- Installation überprüfen
- node -v
- npm -v
- MQTT-Kommandozeilenwerkzeuge installieren
- sudo apt install mosquitto-clients
Sensor (control-switch)
Projekt anlegen
- Verzeichnis erstellen
- sudo mkdir /usr/local/control-switch
- cd /usr/local/control-switch
- Node.js-Projekt initialisieren
- sudo npm init -y
- Benötigte Pakete installieren
- sudo npm install mqtt express dotenv
server.js
Erstelle die Datei /usr/local/control-switch/server.js mit folgendem Inhalt:
require('dotenv').config();
const mqtt = require('mqtt');
const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
const port = 3000;
let status = {
livingRoom: 'off',
bedroom: 'off',
kitchen: 'off',
frontDoor: 'closed'
};
// Prüfe, ob TLS aktiv ist
const useTLS = process.env.MQTT_TLS === 'true';
// MQTT-Verbindungsoptionen aus .env
const options = {
host: process.env.MQTT_HOST,
port: Number(process.env.MQTT_PORT),
protocol: useTLS ? 'mqtts' : 'mqtt',
reconnectPeriod: 5000
};
// Passwort hinzufügen, falls gesetzt.
if (process.env.MQTT_PASS) {
options.username = process.env.MQTT_USER || undefined;
options.password = process.env.MQTT_PASS;
}
// Falls TLS aktiv ist, CA-Zertifikat laden
if (useTLS && process.env.MQTT_CA) {
try {
options.ca = fs.readFileSync(process.env.MQTT_CA);
} catch (err) {
console.error('Fehler beim Laden der CA-Datei:', err.message);
process.exit(1);
}
}
// Falls Client-Zertifikat gesetzt ist (Stufe 4: 2FA), Cert + Key laden
if (useTLS && process.env.MQTT_CERT && process.env.MQTT_KEY) {
try {
options.cert = fs.readFileSync(process.env.MQTT_CERT);
options.key = fs.readFileSync(process.env.MQTT_KEY);
} catch (err) {
console.error('Fehler beim Laden von Client-Cert/Key:', err.message);
process.exit(1);
}
}
const client = mqtt.connect(options);
client.on('connect', () => {
console.log(`Mit MQTT-Broker verbunden: ${process.env.MQTT_HOST}:${process.env.MQTT_PORT}`);
client.subscribe('home/+/status', (err) => {
if (!err) {
console.log('Abonniert: home/+/status');
} else {
console.error('Fehler beim Abonnieren:', err.message);
}
});
});
client.on('message', (topic, message) => {
const room = topic.split('/')[1];
status[room] = message.toString();
console.log(`Status von ${room}: ${status[room]}`);
});
client.on('error', (err) => {
console.error(`MQTT-Fehler: ${err.message}`);
});
client.on('offline', () => {
console.warn('MQTT-Broker nicht erreichbar. Warte auf Wiederverbindung...');
});
client.on('reconnect', () => {
console.log('Versuche, die MQTT-Verbindung wiederherzustellen...');
});
// Route zum Abfragen des Status
app.get('/status', (req, res) => {
res.json(status);
});
// Route zum Schalten eines Geräts
app.post('/toggle/:device', (req, res) => {
const device = req.params.device;
const currentStatus = status[device];
if (!currentStatus) {
res.status(400).send({ error: 'Invalid device' });
return;
}
let newStatus;
if (device === 'frontDoor') {
newStatus = currentStatus === 'closed' ? 'open' : 'closed';
} else {
newStatus = currentStatus === 'on' ? 'off' : 'on';
}
status[device] = newStatus;
const topic = `home/${device}/status`;
client.publish(topic, newStatus, (err) => {
if (err) {
console.log(`Fehler beim Senden an ${device}: ${err.message}`);
res.status(500).send({ success: false, message: 'Error sending message' });
} else {
console.log(`Nachricht gesendet: ${device} ist jetzt ${newStatus}`);
res.send({ success: true, message: `Device ${device} successfully toggled` });
}
});
});
app.use(express.static(path.join(__dirname)));
app.listen(port, () => {
console.log(`Sensor Control Center läuft auf: http://0.0.0.0:${port}`);
});
index.html
Erstelle die Datei /usr/local/control-switch/index.html mit folgendem Inhalt:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Sensor Control Center</title>
<style>
.device {
margin: 20px;
padding: 10px;
border-radius: 10px;
text-align: center;
font-size: 1.5em;
cursor: pointer;
}
.on { background-color: green; color: black; }
.off { background-color: red; color: white; }
.open { background-color: red; color: white; }
.closed { background-color: green; color: white; }
</style>
<script>
function fetchStatus() {
fetch('/status')
.then(response => response.json())
.then(data => {
document.getElementById('livingRoom').className = 'device ' + (data.livingRoom === 'on' ? 'on' : 'off');
document.getElementById('livingRoom').textContent = 'Living Room Light: ' + (data.livingRoom === 'on' ? 'On' : 'Off');
document.getElementById('bedroom').className = 'device ' + (data.bedroom === 'on' ? 'on' : 'off');
document.getElementById('bedroom').textContent = 'Bedroom Light: ' + (data.bedroom === 'on' ? 'On' : 'Off');
document.getElementById('kitchen').className = 'device ' + (data.kitchen === 'on' ? 'on' : 'off');
document.getElementById('kitchen').textContent = 'Kitchen Light: ' + (data.kitchen === 'on' ? 'On' : 'Off');
document.getElementById('frontDoor').className = 'device ' + (data.frontDoor === 'open' ? 'open' : 'closed');
document.getElementById('frontDoor').textContent = 'Front Door: ' + (data.frontDoor === 'open' ? 'Open' : 'Closed');
});
}
function toggleDevice(device) {
fetch(`/toggle/${device}`, { method: 'POST' })
.then(fetchStatus);
}
setInterval(fetchStatus, 1000);
</script>
</head>
<body>
<h1>Sensor Control Center</h1>
<div id="livingRoom" class="device off" onclick="toggleDevice('livingRoom')">Living Room Light: Off</div>
<div id="bedroom" class="device off" onclick="toggleDevice('bedroom')">Bedroom Light: Off</div>
<div id="kitchen" class="device off" onclick="toggleDevice('kitchen')">Kitchen Light: Off</div>
<div id="frontDoor" class="device closed" onclick="toggleDevice('frontDoor')">Front Door: Closed</div>
</body>
</html>
.env-Vorlagen
Für jede Stufe gibt es eine Vorlage. Die aktive Stufe wird durch Kopieren auf .env gesetzt (z. B. cp env.mit-tls .env), danach Service neu starten.
- env.ohne-passwd (Stufe 1)
MQTT_HOST=mqtt.dkbi.com MQTT_PORT=1883
- env.mit-passwd (Stufe 2)
MQTT_HOST=mqtt.dkbi.com MQTT_PORT=1883 MQTT_USER=kit MQTT_PASS=123Start$
- env.mit-tls (Stufe 3)
MQTT_HOST=mqtt.dkbi.com MQTT_PORT=8883 MQTT_USER=kit MQTT_PASS=123Start$ MQTT_TLS=true MQTT_CA=/usr/local/control-switch/kit-ca.crt
- env.mit-2fa (Stufe 4)
MQTT_HOST=mqtt.dkbi.com MQTT_PORT=8883 MQTT_USER=sensor MQTT_PASS=123Start$ MQTT_TLS=true MQTT_CA=/usr/local/control-switch/kit-ca.crt MQTT_CERT=/usr/local/control-switch/sensor.crt MQTT_KEY=/usr/local/control-switch/sensor.key
systemd-Unit
Erstelle die Datei /etc/systemd/system/control-switch.service:
[Unit] Description=Home Control Switch Server (Sensor) After=network.target [Service] Type=simple WorkingDirectory=/usr/local/control-switch ExecStart=/usr/bin/node server.js Restart=on-failure [Install] WantedBy=multi-user.target
- Dienst starten und beim Booten aktivieren
- sudo systemctl daemon-reload
- sudo systemctl enable --now control-switch.service
Aktor (control)
Der Aktor ist genauso aufgebaut wie der Sensor, mit zwei Unterschieden: Er ist read-only (keine Schaltflächen, keine /toggle-Route) und liegt im Verzeichnis /usr/local/control.
Projekt anlegen
- sudo mkdir /usr/local/control
- cd /usr/local/control
- sudo npm init -y
- sudo npm install mqtt express dotenv
server.js
Identisch zum Sensor, aber ohne die /toggle-Route (der Aktor schaltet nicht, er zeigt nur an). Erstelle die Datei /usr/local/control/server.js:
require('dotenv').config();
const mqtt = require('mqtt');
const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
const port = 3000;
let status = {
livingRoom: 'off',
bedroom: 'off',
kitchen: 'off',
frontDoor: 'closed'
};
const useTLS = process.env.MQTT_TLS === 'true';
const options = {
host: process.env.MQTT_HOST,
port: Number(process.env.MQTT_PORT),
protocol: useTLS ? 'mqtts' : 'mqtt',
reconnectPeriod: 5000
};
if (process.env.MQTT_PASS) {
options.username = process.env.MQTT_USER || undefined;
options.password = process.env.MQTT_PASS;
}
if (useTLS && process.env.MQTT_CA) {
try {
options.ca = fs.readFileSync(process.env.MQTT_CA);
} catch (err) {
console.error('Fehler beim Laden der CA-Datei:', err.message);
process.exit(1);
}
}
if (useTLS && process.env.MQTT_CERT && process.env.MQTT_KEY) {
try {
options.cert = fs.readFileSync(process.env.MQTT_CERT);
options.key = fs.readFileSync(process.env.MQTT_KEY);
} catch (err) {
console.error('Fehler beim Laden von Client-Cert/Key:', err.message);
process.exit(1);
}
}
const client = mqtt.connect(options);
client.on('connect', () => {
console.log(`Mit MQTT-Broker verbunden: ${process.env.MQTT_HOST}:${process.env.MQTT_PORT}`);
client.subscribe('home/+/status', (err) => {
if (!err) {
console.log('Abonniert: home/+/status');
} else {
console.error('Fehler beim Abonnieren:', err.message);
}
});
});
client.on('message', (topic, message) => {
const room = topic.split('/')[1];
status[room] = message.toString();
console.log(`Status von ${room}: ${status[room]}`);
});
client.on('error', (err) => {
console.error(`MQTT-Fehler: ${err.message}`);
});
client.on('offline', () => {
console.warn('MQTT-Broker nicht erreichbar. Warte auf Wiederverbindung...');
});
client.on('reconnect', () => {
console.log('Versuche, die MQTT-Verbindung wiederherzustellen...');
});
app.get('/status', (req, res) => {
res.json(status);
});
app.use(express.static(path.join(__dirname)));
app.listen(port, () => {
console.log(`Aktor läuft auf: http://0.0.0.0:${port}`);
});
index.html
Erstelle die Datei /usr/local/control/index.html (read-only, keine onclick-Schaltflächen):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Aktor Devices</title>
<style>
.device {
margin: 20px;
padding: 10px;
border-radius: 10px;
text-align: center;
font-size: 1.5em;
}
.lamp-on { background-color: yellow; color: black; }
.lamp-off { background-color: gray; color: white; }
.door-open { background-color: red; color: white; }
.door-closed { background-color: gray; color: white; }
</style>
<script>
function fetchStatus() {
fetch('/status')
.then(response => response.json())
.then(data => {
document.getElementById('livingRoom').className = 'device ' + (data.livingRoom === 'on' ? 'lamp-on' : 'lamp-off');
document.getElementById('bedroom').className = 'device ' + (data.bedroom === 'on' ? 'lamp-on' : 'lamp-off');
document.getElementById('kitchen').className = 'device ' + (data.kitchen === 'on' ? 'lamp-on' : 'lamp-off');
document.getElementById('frontDoor').className = 'device ' + (data.frontDoor === 'open' ? 'door-open' : 'door-closed');
});
}
setInterval(fetchStatus, 1000);
</script>
</head>
<body>
<h1>Aktor Devices</h1>
<div id="livingRoom" class="device lamp-off">Living Room Light</div>
<div id="bedroom" class="device lamp-off">Bedroom Light</div>
<div id="kitchen" class="device lamp-off">Kitchen Light</div>
<div id="frontDoor" class="device door-closed">Front Door</div>
</body>
</html>
.env-Vorlagen (Aktor)
Wie beim Sensor, aber die Pfade zeigen auf /usr/local/control und in Stufe 4 auf das Aktor-Zertifikat.
- env.mit-2fa (Stufe 4)
MQTT_HOST=mqtt.dkbi.com MQTT_PORT=8883 MQTT_USER=aktor MQTT_PASS=123Start$ MQTT_TLS=true MQTT_CA=/usr/local/control/kit-ca.crt MQTT_CERT=/usr/local/control/aktor.crt MQTT_KEY=/usr/local/control/aktor.key
systemd-Unit
Erstelle die Datei /etc/systemd/system/control.service:
[Unit] Description=Home Control Server (Aktor) After=network.target [Service] Type=simple WorkingDirectory=/usr/local/control ExecStart=/usr/bin/node server.js Restart=on-failure [Install] WantedBy=multi-user.target
- Dienst starten und beim Booten aktivieren
- sudo systemctl daemon-reload
- sudo systemctl enable --now control.service
Zugriff per HTTPS (nginx)
Vor beide Node-Backends (Port 3000) ist ein nginx als Reverse-Proxy mit TLS geschaltet, sodass die Oberflächen unter https://sensor.dkbi.com und https://aktor.dkbi.com erreichbar sind. Die Zertifikate stammen aus der lab-eigenen CA (kit-ca).