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:
sebseb7
2026-02-27 11:22:49 -05:00
parent 63544160d8
commit 7d96ed29c4
5 changed files with 862 additions and 80 deletions

119
server.js
View File

@@ -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