Files
laufschrift/routes/switch.js
2026-03-11 11:58:49 +01:00

170 lines
4.1 KiB
JavaScript

var express = require('express');
var router = express.Router();
var mqtt = require('mqtt');
var moment = require('moment');
var fs = require('fs/promises');
var crypto = require('crypto');
var DEFAULT_BRENNDAUER = 300;
var brenndauer = DEFAULT_BRENNDAUER;
var MQTTHOST = process.env.MQTTHOST || 'localhost';
var MQTTPORT = process.env.MQTTPORT || 1883;
var MQTTUSR = process.env.MQTTUSR || '';
var MQTTPWD = process.env.MQTTPWD || '';
var TOPIC = process.env.TOPIC || 'sonoff';
var SWITCH_API_TOKEN = process.env.SWITCH_API_TOKEN || '';
var switchOffTimer;
var state = {
connect: 'disconnected',
relais: 'UNKNOWN',
offtime: undefined,
lastError: undefined
};
var client = mqtt.connect('mqtt://' + MQTTHOST + ':' + MQTTPORT, {
username: MQTTUSR,
password: MQTTPWD
});
console.log('Start: ', moment().format('YYYY-MM-DD HH:mm'));
client.on('connect', function() {
state.connect = 'connected';
state.lastError = undefined;
client.subscribe('stat/' + TOPIC + '/POWER', function(err) {
if (err) {
state.lastError = 'subscribe failed: ' + err.message;
}
});
});
client.on('reconnect', function() {
state.connect = 'reconnect';
});
client.on('offline', function() {
state.connect = 'offline';
});
client.on('error', function(err) {
state.lastError = err.message;
});
client.on('message', function(_topic, message) {
state.relais = message.toString();
if (state.relais === 'OFF') {
state.offtime = undefined;
}
});
function getResponse() {
return {
connect: state.connect,
relais: state.relais,
offtime: state.offtime,
lastError: state.lastError
};
}
function doPublish(payload) {
client.publish('cmnd/' + TOPIC + '/Power', payload, function(err) {
if (err) {
state.lastError = err.message;
}
});
if (payload === 'On') {
clearTimeout(switchOffTimer);
state.offtime = moment().add(brenndauer, 's').format('HH.mm');
switchOffTimer = setTimeout(doPublish, brenndauer * 1000, 'Off');
} else if (payload === 'Off') {
clearTimeout(switchOffTimer);
state.offtime = undefined;
}
}
// Read optional runtime config.
(async function loadConfig() {
try {
var json = await fs.readFile('config/config.json', 'utf8');
var cfg = JSON.parse(json);
if (cfg.brenndauer !== undefined) {
brenndauer = Number(cfg.brenndauer) || DEFAULT_BRENNDAUER;
}
} catch (err) {
state.lastError = 'config load failed: ' + err.message;
}
})();
// Query initial state once at startup.
doPublish('');
function timingSafeEqualString(a, b) {
var aBuf = Buffer.from(String(a));
var bBuf = Buffer.from(String(b));
if (aBuf.length !== bBuf.length) {
return false;
}
return crypto.timingSafeEqual(aBuf, bBuf);
}
function extractRequestToken(req) {
var authHeader = req.get('authorization') || '';
if (authHeader.startsWith('Bearer ')) {
return authHeader.slice(7).trim();
}
var apiKeyHeader = req.get('x-api-key');
if (apiKeyHeader) {
return apiKeyHeader;
}
if (typeof req.query.token === 'string') {
return req.query.token;
}
return '';
}
router.use(function(req, res, next) {
if (!SWITCH_API_TOKEN) {
return next();
}
var providedToken = extractRequestToken(req);
if (!providedToken || !timingSafeEqualString(providedToken, SWITCH_API_TOKEN)) {
return res.status(401).json({ error: 'unauthorized' });
}
return next();
});
router.get('/:cmd', function(req, res) {
var cmd = req.params.cmd;
if (cmd === 'get_status') {
doPublish('');
return res.json(getResponse());
}
if (cmd === 'switch_on') {
doPublish('On');
return res.json(getResponse());
}
if (cmd === 'switch_off') {
doPublish('Off');
return res.json(getResponse());
}
if (cmd === 'check') {
return res.json(getResponse());
}
return res.status(400).json({ error: 'invalid command' });
});
module.exports = router;