feat: Implement type casting for channel states and events, refine Shelly device online/offline logic, and improve event handling on server startup.

This commit is contained in:
sebseb7
2026-01-17 00:50:32 -05:00
parent 55e4dc56c3
commit 2468f7b5e0
3 changed files with 67 additions and 13 deletions

View File

@@ -56,20 +56,36 @@ export async function loadRules() {
console.log(`Loaded ${rules.length} rule(s)`); 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 * Get current state of a channel
*/ */
function getChannelState(mac, component, field) { function getChannelState(mac, component, field) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
db.get( 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 JOIN channels c ON e.channel_id = c.id
WHERE c.mac = ? AND c.component = ? AND c.field = ? WHERE c.mac = ? AND c.component = ? AND c.field = ?
ORDER BY e.id DESC LIMIT 1`, ORDER BY e.id DESC LIMIT 1`,
[mac, component, field], [mac, component, field],
(err, row) => { (err, row) => {
if (err) reject(err); 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 * Run all rules after an event occurs
*/ */
export async function runRules(mac, component, field, type, event) { 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); const ctx = createContext(triggerEvent);
for (const rule of rules) { for (const rule of rules) {

View File

@@ -71,28 +71,31 @@ function getState(mac) {
export default { export default {
async run(ctx) { async run(ctx) {
// Auto-on for water button when it connects // Auto-on for water button when it connects (only if remote switch is online)
if (ctx.trigger.mac === '08A6F773510C' && ctx.trigger.field === 'connected' && ctx.trigger.value === true) { if (ctx.trigger.mac === '08A6F773510C' && ctx.trigger.field === 'online' && ctx.trigger.event === true) {
ctx.log('Water button connected - turning light on'); 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); await setLight(ctx, ctx.trigger.mac, true, 20);
// Double flash if remote switch is already connected // Double flash to indicate both devices are connected
const remoteSwitchConnected = await ctx.getState(REMOTE_SWITCH_MAC, 'sys', 'connected'); ctx.log('Double flashing to confirm connection');
if (remoteSwitchConnected === true) {
ctx.log('Remote switch already connected - double flashing');
for (let i = 0; i < 2; i++) { for (let i = 0; i < 2; i++) {
await ctx.sendRPC('08A6F773510C', 'Light.Set', { id: 0, on: true, brightness: 20 }); await ctx.sendRPC('08A6F773510C', 'Light.Set', { id: 0, on: true, brightness: 20 });
await sleep(200); await sleep(200);
await ctx.sendRPC('08A6F773510C', 'Light.Set', { id: 0, on: false, brightness: 0 }); await ctx.sendRPC('08A6F773510C', 'Light.Set', { id: 0, on: false, brightness: 0 });
await sleep(200); 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; return;
} }
// Auto-off for remote switch when it connects // Auto-off for remote switch when it connects
if (ctx.trigger.mac === REMOTE_SWITCH_MAC && ctx.trigger.field === 'connected' && ctx.trigger.value === true) { if (ctx.trigger.mac === REMOTE_SWITCH_MAC && ctx.trigger.field === 'online' && ctx.trigger.event === true) {
ctx.log('Remote switch connected - turning off and flashing light'); 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 }); await ctx.sendRPC(REMOTE_SWITCH_MAC, 'Switch.Set', { id: 0, on: false });
// Double flash the light // Double flash the light
@@ -102,11 +105,36 @@ export default {
await ctx.sendRPC('08A6F773510C', 'Light.Set', { id: 0, on: false, brightness: 0 }); await ctx.sendRPC('08A6F773510C', 'Light.Set', { id: 0, on: false, brightness: 0 });
await sleep(200); 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; return;
} }
if (ctx.trigger.field !== 'button') 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 mac = ctx.trigger.mac;
const state = getState(mac); const state = getState(mac);

View File

@@ -28,6 +28,14 @@ db.serialize(() => {
// Events table - references channels // 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))"); 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 // Upstream Client Integration