diff --git a/rule_engine.js b/rule_engine.js index 8c24b54..c55e5e7 100644 --- a/rule_engine.js +++ b/rule_engine.js @@ -218,6 +218,17 @@ export async function getRulesStatus() { 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 */ diff --git a/rules/waterButtonLearnAndCall.js b/rules/waterButtonLearnAndCall.js index d6516c0..61f04ef 100644 --- a/rules/waterButtonLearnAndCall.js +++ b/rules/waterButtonLearnAndCall.js @@ -79,13 +79,27 @@ export default { 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) { // 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) { const remoteSwitchConnected = await ctx.getState(REMOTE_SWITCH_MAC, 'system', 'online'); if (remoteSwitchConnected === true) { 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 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 sleep(200); } + + // Turn light on after flash (ready state) + await setLight(ctx, ctx.trigger.mac, true, 20); } else { 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 }); @@ -151,17 +168,20 @@ export default { // Handle btn_down if (ctx.trigger.event === 'btn_down') { + // Check if timer was active before clearing + const timerWasActive = state.timer !== null; + // Clear any pending timer if (state.timer) { clearTimeout(state.timer); state.timer = null; } - // Turn light off (remote switch turns on) - await setLight(ctx, mac, false, 0); - if (state.countMode) { // 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; state.storedDuration = elapsed; state.countMode = false; @@ -181,8 +201,21 @@ export default { // Turn light on immediately (remote switch turns off) 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 { - // 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`); // Capture updateStatus for use in timer callback diff --git a/status_server.js b/status_server.js index 477e7ef..9ac23ad 100644 --- a/status_server.js +++ b/status_server.js @@ -4,7 +4,7 @@ import path from 'path'; import { fileURLToPath } from 'url'; import { WebSocketServer } from 'ws'; import sqlite3 from 'sqlite3'; -import { getRulesStatus } from './rule_engine.js'; +import { getRulesStatus, setRuleConfig } from './rule_engine.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); @@ -588,12 +588,27 @@ const dashboardHTML = ` } else if (data.type === 'rule_update') { // Real-time rule status update handleRuleUpdate(data); + } else if (data.type === 'rules_update') { + // Full rules refresh (after config change) + renderRules(data.rules); } }; } 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) { // Update the rule in our local cache const ruleIndex = currentRules.findIndex(r => r.name === data.name); @@ -825,10 +840,24 @@ const dashboardHTML = ` } else if (rule.status.error) { statusHTML = \`Error: \${rule.status.error}\`; } 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]) => { let displayValue = value; + let isEditable = key === 'storedDuration'; + if (key.toLowerCase().includes('duration') && typeof value === 'number') { + const secs = Math.round(value / 1000); + if (isEditable) { + return \` +
+ \${key}: + + s +
+ \`; + } displayValue = formatDuration(value); } else if (typeof value === 'boolean') { displayValue = value ? '✓' : '✗'; @@ -908,6 +937,29 @@ wss.on('connection', async (ws) => { 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', () => { console.log('[Status] Browser client disconnected'); wsClients.delete(ws);