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;