feat: Enhance water valve control with Telegram bot integration, allowing remote commands and user authorization. Add channel history querying and improve server functionality for rule execution and static file serving.
This commit is contained in:
119
server.js
119
server.js
@@ -1,10 +1,12 @@
|
||||
import 'dotenv/config';
|
||||
import { WebSocketServer } from 'ws';
|
||||
import http from 'http';
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import sqlite3 from 'sqlite3';
|
||||
import { fileURLToPath } from 'url';
|
||||
import { initRuleEngine, loadRules, runRules, watchRules } from './rule_engine.js';
|
||||
import { Interpreter } from './ruleUITest/interpreter.js'; // Import for manual execution
|
||||
import { broadcastEvent, broadcastRuleUpdate, startStatusServer } from './status_server.js';
|
||||
import { TapoManager } from './tapo_client.js';
|
||||
|
||||
@@ -171,9 +173,99 @@ function checkAndLogEvent(mac, component, field, type, event, connectionId = nul
|
||||
});
|
||||
}
|
||||
|
||||
const wss = new WebSocketServer({ port: 8080 });
|
||||
const ruleContext = {
|
||||
getState: (mac, component, field) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
db.get(`SELECT e.event, c.type FROM events e
|
||||
JOIN channels c ON e.channel_id = c.id
|
||||
WHERE c.mac = ? AND c.component = ? AND c.field = ?
|
||||
ORDER BY e.id DESC LIMIT 1`, [mac, component, field], (err, row) => {
|
||||
if (err) resolve(null);
|
||||
else resolve(row ? row.event : null); // Simple cast?
|
||||
});
|
||||
});
|
||||
},
|
||||
setOutput: (mac, component, on) => sendRPCToDevice(mac, 'Switch.Set', { id: parseInt(component.split(':')[1]), on: !!on }),
|
||||
setLevel: (mac, component, level) => sendRPCToDevice(mac, 'Light.Set', { id: parseInt(component.split(':')[1]), brightness: parseInt(level) }),
|
||||
notify: (msg) => console.log(`[RuleNotify] ${msg}`),
|
||||
log: (msg) => console.log(`[RuleLog] ${msg}`)
|
||||
};
|
||||
|
||||
console.log('Shelly Agent Server listening on port 8080');
|
||||
const server = http.createServer(async (req, res) => {
|
||||
// Basic Static File Serving
|
||||
if (req.method === 'GET') {
|
||||
let filePath = '.' + req.url;
|
||||
if (filePath === './rule-editor') filePath = './ruleUITest/index.html';
|
||||
|
||||
// Map root requests to ruleUITest internal files if referred
|
||||
if (req.url.startsWith('/blocks.js')) filePath = './ruleUITest/blocks.js';
|
||||
|
||||
const extname = path.extname(filePath);
|
||||
let contentType = 'text/html';
|
||||
switch (extname) {
|
||||
case '.js': contentType = 'text/javascript'; break;
|
||||
case '.css': contentType = 'text/css'; break;
|
||||
case '.json': contentType = 'application/json'; break;
|
||||
}
|
||||
|
||||
if (fs.existsSync(filePath) && fs.lstatSync(filePath).isFile()) {
|
||||
fs.readFile(filePath, (error, content) => {
|
||||
if (error) {
|
||||
res.writeHead(500);
|
||||
res.end('Error: ' + error.code);
|
||||
} else {
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.end(content, 'utf-8');
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// API Routes
|
||||
if (req.method === 'GET' && req.url === '/api/channels') {
|
||||
db.all("SELECT DISTINCT mac, component, field, type FROM channels", [], (err, rows) => {
|
||||
if (err) {
|
||||
res.writeHead(500);
|
||||
res.end(JSON.stringify({ error: err.message }));
|
||||
return;
|
||||
}
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(rows));
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (req.method === 'POST' && req.url === '/api/execute-rule') {
|
||||
let body = '';
|
||||
req.on('data', chunk => { body += chunk.toString(); });
|
||||
req.on('end', async () => {
|
||||
try {
|
||||
const ast = JSON.parse(body);
|
||||
const logs = [];
|
||||
// Capture logs
|
||||
const ctx = { ...ruleContext, log: (msg) => logs.push(String(msg)), notify: (msg) => logs.push('Notify: ' + msg) };
|
||||
|
||||
const interpreter = new Interpreter(ctx);
|
||||
await interpreter.execute(ast);
|
||||
|
||||
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify({ status: 'ok', logs }));
|
||||
} catch (err) {
|
||||
res.writeHead(500);
|
||||
res.end(JSON.stringify({ error: err.message }));
|
||||
}
|
||||
});
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const wss = new WebSocketServer({ server });
|
||||
|
||||
server.listen(8080, () => {
|
||||
console.log('Shelly Agent Server listening on port 8080 (HTTP + WS)');
|
||||
console.log('Rule Editor available at http://localhost:8080/rule-editor');
|
||||
});
|
||||
|
||||
// Initialize and load rules
|
||||
initRuleEngine(db, sendRPCToDevice, broadcastRuleUpdate);
|
||||
@@ -324,6 +416,19 @@ wss.on('connection', (ws, req) => {
|
||||
checkAndLogEvent(possibleMac, key, 'input', 'boolean', eventVal, connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Log humidity
|
||||
if (key.startsWith('humidity') && typeof value.rh !== 'undefined') {
|
||||
if (possibleMac) {
|
||||
checkAndLogEvent(possibleMac, key, 'rh', 'range', value.rh, connectionId);
|
||||
}
|
||||
}
|
||||
// Log temperature
|
||||
if (key.startsWith('temperature') && typeof value.tC !== 'undefined') {
|
||||
if (possibleMac) {
|
||||
checkAndLogEvent(possibleMac, key, 'tC', 'range', value.tC, connectionId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Log extracted RSSI from NotifyFullStatus
|
||||
@@ -419,6 +524,16 @@ wss.on('connection', (ws, req) => {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for humidity updates
|
||||
if (key.startsWith('humidity') && typeof value.rh !== 'undefined') {
|
||||
checkAndLogEvent(mac, key, 'rh', 'range', value.rh, connectionId);
|
||||
}
|
||||
|
||||
// Check for temperature updates
|
||||
if (key.startsWith('temperature') && typeof value.tC !== 'undefined') {
|
||||
checkAndLogEvent(mac, key, 'tC', 'range', value.tC, connectionId);
|
||||
}
|
||||
}
|
||||
|
||||
// Check for wifi updates in NotifyStatus
|
||||
|
||||
Reference in New Issue
Block a user