feat: Introduce rule configuration from the status dashboard, allowing storedDuration editing for the water button rule and improving its timer handling.
This commit is contained in:
@@ -218,6 +218,17 @@ export async function getRulesStatus() {
|
|||||||
return statuses;
|
return statuses;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set config on a specific rule
|
||||||
|
*/
|
||||||
|
export function setRuleConfig(ruleName, key, value) {
|
||||||
|
const rule = rules.find(r => r._filename === ruleName);
|
||||||
|
if (rule && typeof rule.setConfig === 'function') {
|
||||||
|
return rule.setConfig(key, value);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Watch rules directory for changes and auto-reload
|
* Watch rules directory for changes and auto-reload
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -79,13 +79,27 @@ export default {
|
|||||||
timerActive: state.timer !== null
|
timerActive: state.timer !== null
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
setConfig(key, value) {
|
||||||
|
if (key === 'storedDuration') {
|
||||||
|
const duration = parseInt(value, 10);
|
||||||
|
if (duration > 0 && duration <= 300000) { // Max 5 minutes
|
||||||
|
const state = getState(WATER_BUTTON_MAC);
|
||||||
|
state.storedDuration = duration;
|
||||||
|
// Persist
|
||||||
|
persistedState[WATER_BUTTON_MAC] = { storedDuration: duration };
|
||||||
|
saveState(persistedState);
|
||||||
|
console.log(`[Rule] storedDuration set to ${duration}ms`);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
async run(ctx) {
|
async run(ctx) {
|
||||||
// Auto-on for water button when it connects (only if remote switch is online)
|
// Auto-on for water button when it connects (only if remote switch is online)
|
||||||
if (ctx.trigger.mac === WATER_BUTTON_MAC && ctx.trigger.field === 'online' && ctx.trigger.event === true) {
|
if (ctx.trigger.mac === WATER_BUTTON_MAC && ctx.trigger.field === 'online' && ctx.trigger.event === true) {
|
||||||
const remoteSwitchConnected = await ctx.getState(REMOTE_SWITCH_MAC, 'system', 'online');
|
const remoteSwitchConnected = await ctx.getState(REMOTE_SWITCH_MAC, 'system', 'online');
|
||||||
if (remoteSwitchConnected === true) {
|
if (remoteSwitchConnected === true) {
|
||||||
ctx.log('Water button connected - remote switch online, turning light on');
|
ctx.log('Water button connected - remote switch online, turning light on');
|
||||||
await setLight(ctx, ctx.trigger.mac, true, 20);
|
|
||||||
|
|
||||||
// Double flash to indicate both devices are connected
|
// Double flash to indicate both devices are connected
|
||||||
ctx.log('Double flashing to confirm connection');
|
ctx.log('Double flashing to confirm connection');
|
||||||
@@ -95,6 +109,9 @@ export default {
|
|||||||
await ctx.sendRPC(WATER_BUTTON_MAC, 'Light.Set', { id: 0, on: false, brightness: 0 });
|
await ctx.sendRPC(WATER_BUTTON_MAC, 'Light.Set', { id: 0, on: false, brightness: 0 });
|
||||||
await sleep(200);
|
await sleep(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Turn light on after flash (ready state)
|
||||||
|
await setLight(ctx, ctx.trigger.mac, true, 20);
|
||||||
} else {
|
} else {
|
||||||
ctx.log('Water button connected - remote switch offline, keeping light off');
|
ctx.log('Water button connected - remote switch offline, keeping light off');
|
||||||
await ctx.sendRPC(WATER_BUTTON_MAC, 'Light.Set', { id: 0, on: false, brightness: 0 });
|
await ctx.sendRPC(WATER_BUTTON_MAC, 'Light.Set', { id: 0, on: false, brightness: 0 });
|
||||||
@@ -151,17 +168,20 @@ export default {
|
|||||||
|
|
||||||
// Handle btn_down
|
// Handle btn_down
|
||||||
if (ctx.trigger.event === 'btn_down') {
|
if (ctx.trigger.event === 'btn_down') {
|
||||||
|
// Check if timer was active before clearing
|
||||||
|
const timerWasActive = state.timer !== null;
|
||||||
|
|
||||||
// Clear any pending timer
|
// Clear any pending timer
|
||||||
if (state.timer) {
|
if (state.timer) {
|
||||||
clearTimeout(state.timer);
|
clearTimeout(state.timer);
|
||||||
state.timer = null;
|
state.timer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Turn light off (remote switch turns on)
|
|
||||||
await setLight(ctx, mac, false, 0);
|
|
||||||
|
|
||||||
if (state.countMode) {
|
if (state.countMode) {
|
||||||
// We're in count mode - calculate elapsed time and turn light on
|
// We're in count mode - calculate elapsed time and turn light on
|
||||||
|
// Turn light off first (remote switch turns on)
|
||||||
|
await setLight(ctx, mac, false, 0);
|
||||||
|
|
||||||
const elapsed = Date.now() - state.countStart;
|
const elapsed = Date.now() - state.countStart;
|
||||||
state.storedDuration = elapsed;
|
state.storedDuration = elapsed;
|
||||||
state.countMode = false;
|
state.countMode = false;
|
||||||
@@ -181,8 +201,21 @@ export default {
|
|||||||
|
|
||||||
// Turn light on immediately (remote switch turns off)
|
// Turn light on immediately (remote switch turns off)
|
||||||
await setLight(ctx, mac, true, 20);
|
await setLight(ctx, mac, true, 20);
|
||||||
|
} else if (timerWasActive) {
|
||||||
|
// Timer was running - cancel it and turn light on immediately
|
||||||
|
ctx.log('Timer cancelled by button press. Turning light on.');
|
||||||
|
await setLight(ctx, mac, true, 20);
|
||||||
|
|
||||||
|
// Push status update to dashboard
|
||||||
|
ctx.updateStatus({
|
||||||
|
storedDuration: state.storedDuration,
|
||||||
|
countMode: false,
|
||||||
|
timerActive: false,
|
||||||
|
lastAction: 'Timer cancelled'
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
// Normal mode - schedule light to turn on after stored duration
|
// Normal mode - turn off light and schedule it to turn on after stored duration
|
||||||
|
await setLight(ctx, mac, false, 0);
|
||||||
ctx.log(`Light off. Will turn on in ${state.storedDuration}ms`);
|
ctx.log(`Light off. Will turn on in ${state.storedDuration}ms`);
|
||||||
|
|
||||||
// Capture updateStatus for use in timer callback
|
// Capture updateStatus for use in timer callback
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import path from 'path';
|
|||||||
import { fileURLToPath } from 'url';
|
import { fileURLToPath } from 'url';
|
||||||
import { WebSocketServer } from 'ws';
|
import { WebSocketServer } from 'ws';
|
||||||
import sqlite3 from 'sqlite3';
|
import sqlite3 from 'sqlite3';
|
||||||
import { getRulesStatus } from './rule_engine.js';
|
import { getRulesStatus, setRuleConfig } from './rule_engine.js';
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
@@ -588,12 +588,27 @@ const dashboardHTML = `<!DOCTYPE html>
|
|||||||
} else if (data.type === 'rule_update') {
|
} else if (data.type === 'rule_update') {
|
||||||
// Real-time rule status update
|
// Real-time rule status update
|
||||||
handleRuleUpdate(data);
|
handleRuleUpdate(data);
|
||||||
|
} else if (data.type === 'rules_update') {
|
||||||
|
// Full rules refresh (after config change)
|
||||||
|
renderRules(data.rules);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
let currentRules = [];
|
let currentRules = [];
|
||||||
|
|
||||||
|
function sendConfig(ruleName, key, value) {
|
||||||
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
||||||
|
ws.send(JSON.stringify({
|
||||||
|
type: 'set_config',
|
||||||
|
ruleName,
|
||||||
|
key,
|
||||||
|
value
|
||||||
|
}));
|
||||||
|
showToast(ruleName, 'Saving...', key + ' = ' + value, 'event');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleRuleUpdate(data) {
|
function handleRuleUpdate(data) {
|
||||||
// Update the rule in our local cache
|
// Update the rule in our local cache
|
||||||
const ruleIndex = currentRules.findIndex(r => r.name === data.name);
|
const ruleIndex = currentRules.findIndex(r => r.name === data.name);
|
||||||
@@ -825,10 +840,24 @@ const dashboardHTML = `<!DOCTYPE html>
|
|||||||
} else if (rule.status.error) {
|
} else if (rule.status.error) {
|
||||||
statusHTML = \`<span style="color: var(--accent-offline);">Error: \${rule.status.error}</span>\`;
|
statusHTML = \`<span style="color: var(--accent-offline);">Error: \${rule.status.error}</span>\`;
|
||||||
} else {
|
} else {
|
||||||
// Display each status property as a badge
|
// Display each status property as a badge or input
|
||||||
const stats = Object.entries(rule.status).map(([key, value]) => {
|
const stats = Object.entries(rule.status).map(([key, value]) => {
|
||||||
let displayValue = value;
|
let displayValue = value;
|
||||||
|
let isEditable = key === 'storedDuration';
|
||||||
|
|
||||||
if (key.toLowerCase().includes('duration') && typeof value === 'number') {
|
if (key.toLowerCase().includes('duration') && typeof value === 'number') {
|
||||||
|
const secs = Math.round(value / 1000);
|
||||||
|
if (isEditable) {
|
||||||
|
return \`
|
||||||
|
<div class="rule-stat editable">
|
||||||
|
<span class="rule-stat-label">\${key}:</span>
|
||||||
|
<input type="number" class="rule-input" value="\${secs}" min="1" max="300"
|
||||||
|
onchange="sendConfig('\${rule.name}', '\${key}', this.value * 1000)"
|
||||||
|
style="width: 50px; background: var(--bg-secondary); color: var(--text-primary); border: 1px solid var(--border-color); border-radius: 4px; padding: 2px 4px; font-family: monospace;">
|
||||||
|
<span style="color: var(--text-secondary);">s</span>
|
||||||
|
</div>
|
||||||
|
\`;
|
||||||
|
}
|
||||||
displayValue = formatDuration(value);
|
displayValue = formatDuration(value);
|
||||||
} else if (typeof value === 'boolean') {
|
} else if (typeof value === 'boolean') {
|
||||||
displayValue = value ? '✓' : '✗';
|
displayValue = value ? '✓' : '✗';
|
||||||
@@ -908,6 +937,29 @@ wss.on('connection', async (ws) => {
|
|||||||
console.error('[Status] Error fetching initial data:', err);
|
console.error('[Status] Error fetching initial data:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ws.on('message', async (message) => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(message);
|
||||||
|
if (data.type === 'set_config') {
|
||||||
|
const { ruleName, key, value } = data;
|
||||||
|
const success = setRuleConfig(ruleName, key, value);
|
||||||
|
if (success) {
|
||||||
|
// Broadcast updated status to all clients
|
||||||
|
const rules = await getRulesStatus();
|
||||||
|
const updateMsg = JSON.stringify({ type: 'rules_update', rules });
|
||||||
|
for (const client of wsClients) {
|
||||||
|
if (client.readyState === 1) {
|
||||||
|
client.send(updateMsg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ws.send(JSON.stringify({ type: 'set_config_result', success }));
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[Status] Error processing message:', err);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
ws.on('close', () => {
|
ws.on('close', () => {
|
||||||
console.log('[Status] Browser client disconnected');
|
console.log('[Status] Browser client disconnected');
|
||||||
wsClients.delete(ws);
|
wsClients.delete(ws);
|
||||||
|
|||||||
Reference in New Issue
Block a user