MQTT Projekt Aufbau

Aus Xinux Wiki
Zur Navigation springen Zur Suche springen

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
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).