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:
@@ -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) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user