From 2468f7b5e0f359b5f65a01c516c39d765c861a96 Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Sat, 17 Jan 2026 00:50:32 -0500 Subject: [PATCH] feat: Implement type casting for channel states and events, refine Shelly device online/offline logic, and improve event handling on server startup. --- rule_engine.js | 24 ++++++++++++++-- rules/waterButtonLearnAndCall.js | 48 +++++++++++++++++++++++++------- server.js | 8 ++++++ 3 files changed, 67 insertions(+), 13 deletions(-) diff --git a/rule_engine.js b/rule_engine.js index 690f973..0f0bfa3 100644 --- a/rule_engine.js +++ b/rule_engine.js @@ -56,20 +56,36 @@ export async function loadRules() { console.log(`Loaded ${rules.length} rule(s)`); } +/** + * Cast string value to proper type based on channel type + */ +function castValue(value, type) { + if (value === null || value === undefined) return null; + switch (type) { + case 'boolean': + return value === 'true' || value === true; + case 'range': + return parseInt(value, 10); + case 'enum': + default: + return value; + } +} + /** * Get current state of a channel */ function getChannelState(mac, component, field) { return new Promise((resolve, reject) => { db.get( - `SELECT e.event FROM events e + `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) reject(err); - else resolve(row ? row.event : null); + else resolve(row ? castValue(row.event, row.type) : null); } ); }); @@ -137,7 +153,9 @@ function createContext(triggerEvent) { * Run all rules after an event occurs */ export async function runRules(mac, component, field, type, event) { - const triggerEvent = { mac, component, field, type, event }; + // Cast event value to proper type + const typedEvent = castValue(event, type); + const triggerEvent = { mac, component, field, type, event: typedEvent }; const ctx = createContext(triggerEvent); for (const rule of rules) { diff --git a/rules/waterButtonLearnAndCall.js b/rules/waterButtonLearnAndCall.js index ecc6db2..9b1b0be 100644 --- a/rules/waterButtonLearnAndCall.js +++ b/rules/waterButtonLearnAndCall.js @@ -71,28 +71,31 @@ function getState(mac) { export default { async run(ctx) { - // Auto-on for water button when it connects - if (ctx.trigger.mac === '08A6F773510C' && ctx.trigger.field === 'connected' && ctx.trigger.value === true) { - ctx.log('Water button connected - turning light on'); - await setLight(ctx, ctx.trigger.mac, true, 20); - - // Double flash if remote switch is already connected - const remoteSwitchConnected = await ctx.getState(REMOTE_SWITCH_MAC, 'sys', 'connected'); + // Auto-on for water button when it connects (only if remote switch is online) + if (ctx.trigger.mac === '08A6F773510C' && ctx.trigger.field === 'online' && ctx.trigger.event === true) { + const remoteSwitchConnected = await ctx.getState(REMOTE_SWITCH_MAC, 'system', 'online'); if (remoteSwitchConnected === true) { - ctx.log('Remote switch already connected - double flashing'); + 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'); for (let i = 0; i < 2; i++) { await ctx.sendRPC('08A6F773510C', 'Light.Set', { id: 0, on: true, brightness: 20 }); await sleep(200); await ctx.sendRPC('08A6F773510C', 'Light.Set', { id: 0, on: false, brightness: 0 }); await sleep(200); } + } else { + ctx.log('Water button connected - remote switch offline, keeping light off'); + await ctx.sendRPC('08A6F773510C', 'Light.Set', { id: 0, on: false, brightness: 0 }); } return; } // Auto-off for remote switch when it connects - if (ctx.trigger.mac === REMOTE_SWITCH_MAC && ctx.trigger.field === 'connected' && ctx.trigger.value === true) { - ctx.log('Remote switch connected - turning off and flashing light'); + if (ctx.trigger.mac === REMOTE_SWITCH_MAC && ctx.trigger.field === 'online' && ctx.trigger.event === true) { + ctx.log('Remote switch connected - turning switch off, flashing light, then turning light on'); await ctx.sendRPC(REMOTE_SWITCH_MAC, 'Switch.Set', { id: 0, on: false }); // Double flash the light @@ -102,11 +105,36 @@ export default { await ctx.sendRPC('08A6F773510C', 'Light.Set', { id: 0, on: false, brightness: 0 }); await sleep(200); } + + // Turn light on after flash + await setLight(ctx, '08A6F773510C', true, 20); + return; + } + + // Turn off light when remote switch goes offline + if (ctx.trigger.mac === REMOTE_SWITCH_MAC && ctx.trigger.field === 'online' && ctx.trigger.event === false) { + ctx.log('Remote switch went offline - turning light off'); + await ctx.sendRPC('08A6F773510C', 'Light.Set', { id: 0, on: false, brightness: 0 }); + + // Clear any pending timer + const state = getState('08A6F773510C'); + if (state.timer) { + clearTimeout(state.timer); + state.timer = null; + } + state.countMode = false; return; } if (ctx.trigger.field !== 'button') return; + // Ignore button events if remote switch is offline + const remoteSwitchOnline = await ctx.getState(REMOTE_SWITCH_MAC, 'system', 'online'); + if (remoteSwitchOnline !== true) { + ctx.log('Button ignored - remote switch is offline'); + return; + } + const mac = ctx.trigger.mac; const state = getState(mac); diff --git a/server.js b/server.js index bde46f0..a5eb5d2 100644 --- a/server.js +++ b/server.js @@ -28,6 +28,14 @@ db.serialize(() => { // Events table - references channels db.run("CREATE TABLE IF NOT EXISTS events (id INTEGER PRIMARY KEY AUTOINCREMENT, channel_id INTEGER, event TEXT, timestamp TEXT, FOREIGN KEY(channel_id) REFERENCES channels(id))"); + + // Insert offline events for all online channels on startup (to prevent deduplication issues after restart) + db.run(`INSERT INTO events (channel_id, event, timestamp) + SELECT c.id, 'false', datetime('now') + FROM channels c + WHERE c.field = 'online' + AND EXISTS (SELECT 1 FROM events e WHERE e.channel_id = c.id AND e.event = 'true' + AND e.id = (SELECT MAX(e2.id) FROM events e2 WHERE e2.channel_id = c.id))`); }); // Upstream Client Integration