u
This commit is contained in:
@@ -141,8 +141,6 @@ export class ACInfinityClient {
|
|||||||
.replace(/[^a-z0-9]+/g, '-')
|
.replace(/[^a-z0-9]+/g, '-')
|
||||||
.replace(/^-|-$/g, '');
|
.replace(/^-|-$/g, '');
|
||||||
|
|
||||||
console.log(`[AC] Debug: Device ${deviceId} (${device.devName}) Type: ${device.devType}`);
|
|
||||||
|
|
||||||
// --- Device Level Sensors ---
|
// --- Device Level Sensors ---
|
||||||
|
|
||||||
// Temperature (Celsius * 100)
|
// Temperature (Celsius * 100)
|
||||||
@@ -274,10 +272,6 @@ export class ACInfinityClient {
|
|||||||
const settings = await this.getDeviceModeSettings(devId, port);
|
const settings = await this.getDeviceModeSettings(devId, port);
|
||||||
if (!settings) throw new Error('Could not fetch existing settings');
|
if (!settings) throw new Error('Could not fetch existing settings');
|
||||||
|
|
||||||
console.log(`[AC] Debug: Device Settings for ${devId}:${port}`, JSON.stringify(settings));
|
|
||||||
// Log device info if available to check type? We don't have devType here easily without passing it or fetching list again.
|
|
||||||
// But settings usually contains some info.
|
|
||||||
|
|
||||||
// 2. Prepare updates
|
// 2. Prepare updates
|
||||||
// Constrain level 0-10
|
// Constrain level 0-10
|
||||||
const safeLevel = Math.max(0, Math.min(10, Math.round(level)));
|
const safeLevel = Math.max(0, Math.min(10, Math.round(level)));
|
||||||
@@ -345,10 +339,7 @@ export class ACInfinityClient {
|
|||||||
if (!params.has('transitionType')) params.append('transitionType', '0');
|
if (!params.has('transitionType')) params.append('transitionType', '0');
|
||||||
|
|
||||||
// 3. Send update
|
// 3. Send update
|
||||||
const requestUrl = `${this.host}/api/dev/addDevMode?${params.toString()}`;
|
const response = await fetch(`${this.host}/api/dev/addDevMode?${params.toString()}`, {
|
||||||
console.log(`[AC] Debug: Sending request to ${requestUrl}`);
|
|
||||||
|
|
||||||
const response = await fetch(requestUrl, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': 'ACController/1.9.7 (com.acinfinity.humiture; build:533; iOS 18.5.0) Alamofire/5.10.2',
|
'User-Agent': 'ACController/1.9.7 (com.acinfinity.humiture; build:533; iOS 18.5.0) Alamofire/5.10.2',
|
||||||
|
|||||||
@@ -246,6 +246,10 @@ function handleAgentMessage(ws, message, clientState, clientId) {
|
|||||||
try {
|
try {
|
||||||
const validReadings = readings.filter(r => r.device && r.channel && (r.value !== undefined || r.data !== undefined));
|
const validReadings = readings.filter(r => r.device && r.channel && (r.value !== undefined || r.data !== undefined));
|
||||||
const result = insertReadingsSmart(clientState.devicePrefix, validReadings);
|
const result = insertReadingsSmart(clientState.devicePrefix, validReadings);
|
||||||
|
|
||||||
|
// Trigger rules immediately on new data
|
||||||
|
if (runRules) runRules();
|
||||||
|
|
||||||
ws.send(JSON.stringify({ type: 'ack', count: result.inserted + result.updated }));
|
ws.send(JSON.stringify({ type: 'ack', count: result.inserted + result.updated }));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('[WS] Error inserting readings:', err.message);
|
console.error('[WS] Error inserting readings:', err.message);
|
||||||
@@ -321,6 +325,202 @@ function syncOutputStates() {
|
|||||||
|
|
||||||
// Start output state sync interval (every 60s)
|
// Start output state sync interval (every 60s)
|
||||||
setInterval(syncOutputStates, 60000);
|
setInterval(syncOutputStates, 60000);
|
||||||
|
|
||||||
|
// =============================================
|
||||||
|
// RULE ENGINE (Global Scope)
|
||||||
|
// =============================================
|
||||||
|
|
||||||
|
// Virtual output channel definitions
|
||||||
|
const OUTPUT_CHANNELS = [
|
||||||
|
{ channel: 'CircFanLevel', type: 'number', min: 0, max: 10, description: 'Circulation Fan Level' },
|
||||||
|
{ channel: 'CO2Valve', type: 'boolean', min: 0, max: 1, description: 'CO2 Valve' },
|
||||||
|
{ channel: 'BigDehumid', type: 'boolean', min: 0, max: 1, description: 'Big Dehumidifier' },
|
||||||
|
{ channel: 'TentExhaust', type: 'boolean', min: 0, max: 1, description: 'Tent Exhaust Fan' },
|
||||||
|
];
|
||||||
|
|
||||||
|
// Get current sensor value
|
||||||
|
function getSensorValue(channel) {
|
||||||
|
// channel format: "device:channel" e.g. "ac:controller:co2"
|
||||||
|
const lastColonIndex = channel.lastIndexOf(':');
|
||||||
|
if (lastColonIndex === -1) return null;
|
||||||
|
const device = channel.substring(0, lastColonIndex);
|
||||||
|
const ch = channel.substring(lastColonIndex + 1);
|
||||||
|
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
SELECT value FROM sensor_events
|
||||||
|
WHERE device = ? AND channel = ?
|
||||||
|
ORDER BY timestamp DESC LIMIT 1
|
||||||
|
`);
|
||||||
|
const row = stmt.get(device, ch);
|
||||||
|
return row ? row.value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get current output value
|
||||||
|
function getOutputValue(channel) {
|
||||||
|
const stmt = db.prepare(`
|
||||||
|
SELECT value FROM output_events
|
||||||
|
WHERE channel = ?
|
||||||
|
ORDER BY timestamp DESC LIMIT 1
|
||||||
|
`);
|
||||||
|
const row = stmt.get(channel);
|
||||||
|
return row ? row.value : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write output value with RLE
|
||||||
|
function writeOutputValue(channel, value) {
|
||||||
|
const now = new Date().toISOString();
|
||||||
|
|
||||||
|
const lastStmt = db.prepare(`
|
||||||
|
SELECT id, value FROM output_events
|
||||||
|
WHERE channel = ?
|
||||||
|
ORDER BY timestamp DESC LIMIT 1
|
||||||
|
`);
|
||||||
|
const last = lastStmt.get(channel);
|
||||||
|
|
||||||
|
const valueChanged = !last || Math.abs(last.value - value) >= Number.EPSILON;
|
||||||
|
|
||||||
|
if (!valueChanged) {
|
||||||
|
// Same value - update the until timestamp (RLE)
|
||||||
|
const updateStmt = db.prepare('UPDATE output_events SET until = ? WHERE id = ?');
|
||||||
|
updateStmt.run(now, last.id);
|
||||||
|
} else {
|
||||||
|
// New value - insert new record
|
||||||
|
const insertStmt = db.prepare(`
|
||||||
|
INSERT INTO output_events (timestamp, until, channel, value, data_type)
|
||||||
|
VALUES (?, NULL, ?, ?, 'number')
|
||||||
|
`);
|
||||||
|
insertStmt.run(now, channel, value);
|
||||||
|
console.log(`[RuleRunner] Output changed: ${channel} = ${value}`);
|
||||||
|
|
||||||
|
// Send command to bound physical device
|
||||||
|
const binding = OUTPUT_BINDINGS[channel];
|
||||||
|
if (binding) {
|
||||||
|
let commandValue = value;
|
||||||
|
if (binding.type === 'switch') {
|
||||||
|
commandValue = value > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[RuleRunner] Binding for ${channel}: type=${binding.type}, val=${value}, cmdVal=${commandValue}`);
|
||||||
|
|
||||||
|
sendCommandToDevicePrefix(`${binding.device}:`, {
|
||||||
|
device: binding.channel,
|
||||||
|
action: 'set_state',
|
||||||
|
value: commandValue
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compare values with operator
|
||||||
|
function compareValues(actual, operator, target) {
|
||||||
|
if (actual === null || actual === undefined) return false;
|
||||||
|
switch (operator) {
|
||||||
|
case '=':
|
||||||
|
case '==': return actual === target;
|
||||||
|
case '!=': return actual !== target;
|
||||||
|
case '<': return actual < target;
|
||||||
|
case '>': return actual > target;
|
||||||
|
case '<=': return actual <= target;
|
||||||
|
case '>=': return actual >= target;
|
||||||
|
default: return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Evaluate a single condition
|
||||||
|
function evaluateCondition(condition) {
|
||||||
|
const { type, operator, value, channel } = condition;
|
||||||
|
|
||||||
|
// Handle AND/OR groups
|
||||||
|
if (operator === 'AND' || operator === 'OR') {
|
||||||
|
const results = (condition.conditions || []).map(c => evaluateCondition(c));
|
||||||
|
return operator === 'AND'
|
||||||
|
? results.every(r => r)
|
||||||
|
: results.some(r => r);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case 'time': {
|
||||||
|
const now = new Date();
|
||||||
|
const currentTime = now.getHours() * 60 + now.getMinutes(); // minutes since midnight
|
||||||
|
|
||||||
|
if (operator === 'between' && Array.isArray(value)) {
|
||||||
|
const [start, end] = value.map(t => {
|
||||||
|
const [h, m] = t.split(':').map(Number);
|
||||||
|
return h * 60 + m;
|
||||||
|
});
|
||||||
|
return currentTime >= start && currentTime <= end;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [h, m] = String(value).split(':').map(Number);
|
||||||
|
const targetTime = h * 60 + m;
|
||||||
|
return compareValues(currentTime, operator, targetTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'date': {
|
||||||
|
const now = new Date();
|
||||||
|
const today = now.toISOString().split('T')[0];
|
||||||
|
|
||||||
|
if (operator === 'between' && Array.isArray(value)) {
|
||||||
|
return today >= value[0] && today <= value[1];
|
||||||
|
}
|
||||||
|
if (operator === 'before') return today < value;
|
||||||
|
if (operator === 'after') return today > value;
|
||||||
|
return today === value;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'sensor': {
|
||||||
|
const sensorValue = getSensorValue(channel);
|
||||||
|
return compareValues(sensorValue, operator, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'output': {
|
||||||
|
const outputValue = getOutputValue(channel);
|
||||||
|
return compareValues(outputValue, operator, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
console.warn(`[RuleRunner] Unknown condition type: ${type}`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run all rules
|
||||||
|
function runRules() {
|
||||||
|
if (!db) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const rules = db.prepare('SELECT * FROM rules WHERE enabled = 1 ORDER BY position ASC').all();
|
||||||
|
|
||||||
|
// Default all outputs to OFF (0) - if no rule sets them, they stay off
|
||||||
|
const desiredOutputs = {};
|
||||||
|
for (const ch of OUTPUT_CHANNELS) {
|
||||||
|
desiredOutputs[ch.channel] = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
try {
|
||||||
|
const conditions = JSON.parse(rule.conditions || '{}');
|
||||||
|
const action = JSON.parse(rule.action || '{}');
|
||||||
|
|
||||||
|
if (evaluateCondition(conditions)) {
|
||||||
|
// Rule matches - set output (later rules override)
|
||||||
|
if (action.channel && action.value !== undefined) {
|
||||||
|
desiredOutputs[action.channel] = action.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[RuleRunner] Error evaluating rule ${rule.id}:`, err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write output values
|
||||||
|
for (const [channel, value] of Object.entries(desiredOutputs)) {
|
||||||
|
writeOutputValue(channel, value);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('[RuleRunner] Error running rules:', err.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
// Also sync immediately on startup after a short delay
|
// Also sync immediately on startup after a short delay
|
||||||
setTimeout(syncOutputStates, 5000);
|
setTimeout(syncOutputStates, 5000);
|
||||||
|
|
||||||
@@ -531,12 +731,7 @@ module.exports = {
|
|||||||
app.use('/api/rules', checkAuth);
|
app.use('/api/rules', checkAuth);
|
||||||
|
|
||||||
// Virtual output channel definitions
|
// Virtual output channel definitions
|
||||||
const OUTPUT_CHANNELS = [
|
// Virtual output channel definitions - MOVED TO GLOBAL SCOPE
|
||||||
{ channel: 'CircFanLevel', type: 'number', min: 0, max: 10, description: 'Circulation Fan Level' },
|
|
||||||
{ channel: 'CO2Valve', type: 'boolean', min: 0, max: 1, description: 'CO2 Valve' },
|
|
||||||
{ channel: 'BigDehumid', type: 'boolean', min: 0, max: 1, description: 'Big Dehumidifier' },
|
|
||||||
{ channel: 'TentExhaust', type: 'boolean', min: 0, max: 1, description: 'Tent Exhaust Fan' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// GET /api/outputs - List output channel definitions
|
// GET /api/outputs - List output channel definitions
|
||||||
app.get('/api/outputs', (req, res) => {
|
app.get('/api/outputs', (req, res) => {
|
||||||
@@ -642,6 +837,7 @@ module.exports = {
|
|||||||
JSON.stringify(action),
|
JSON.stringify(action),
|
||||||
req.user?.id || null
|
req.user?.id || null
|
||||||
);
|
);
|
||||||
|
runRules(); // Trigger rules immediately
|
||||||
res.json({ id: info.lastInsertRowid, name, type, enabled, conditions, action });
|
res.json({ id: info.lastInsertRowid, name, type, enabled, conditions, action });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
@@ -665,6 +861,7 @@ module.exports = {
|
|||||||
req.params.id
|
req.params.id
|
||||||
);
|
);
|
||||||
if (info.changes > 0) {
|
if (info.changes > 0) {
|
||||||
|
runRules(); // Trigger rules immediately
|
||||||
res.json({ id: req.params.id, name, type, enabled, conditions, action });
|
res.json({ id: req.params.id, name, type, enabled, conditions, action });
|
||||||
} else {
|
} else {
|
||||||
res.status(404).json({ error: 'Rule not found' });
|
res.status(404).json({ error: 'Rule not found' });
|
||||||
@@ -680,6 +877,7 @@ module.exports = {
|
|||||||
const stmt = db.prepare('DELETE FROM rules WHERE id = ?');
|
const stmt = db.prepare('DELETE FROM rules WHERE id = ?');
|
||||||
const info = stmt.run(req.params.id);
|
const info = stmt.run(req.params.id);
|
||||||
if (info.changes > 0) {
|
if (info.changes > 0) {
|
||||||
|
runRules(); // Trigger rules immediately
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} else {
|
} else {
|
||||||
res.status(404).json({ error: 'Rule not found' });
|
res.status(404).json({ error: 'Rule not found' });
|
||||||
@@ -703,6 +901,7 @@ module.exports = {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
updateMany(order);
|
updateMany(order);
|
||||||
|
runRules(); // Trigger rules immediately
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
res.status(500).json({ error: err.message });
|
res.status(500).json({ error: err.message });
|
||||||
@@ -713,191 +912,7 @@ module.exports = {
|
|||||||
// RULE RUNNER (Background Job)
|
// RULE RUNNER (Background Job)
|
||||||
// =============================================
|
// =============================================
|
||||||
|
|
||||||
// Get current sensor value
|
// Rule Engine functions moved to global scope
|
||||||
function getSensorValue(channel) {
|
|
||||||
// channel format: "device:channel" e.g. "ac:controller:co2"
|
|
||||||
const lastColonIndex = channel.lastIndexOf(':');
|
|
||||||
if (lastColonIndex === -1) return null;
|
|
||||||
const device = channel.substring(0, lastColonIndex);
|
|
||||||
const ch = channel.substring(lastColonIndex + 1);
|
|
||||||
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
SELECT value FROM sensor_events
|
|
||||||
WHERE device = ? AND channel = ?
|
|
||||||
ORDER BY timestamp DESC LIMIT 1
|
|
||||||
`);
|
|
||||||
const row = stmt.get(device, ch);
|
|
||||||
return row ? row.value : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get current output value
|
|
||||||
function getOutputValue(channel) {
|
|
||||||
const stmt = db.prepare(`
|
|
||||||
SELECT value FROM output_events
|
|
||||||
WHERE channel = ?
|
|
||||||
ORDER BY timestamp DESC LIMIT 1
|
|
||||||
`);
|
|
||||||
const row = stmt.get(channel);
|
|
||||||
return row ? row.value : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write output value with RLE
|
|
||||||
function writeOutputValue(channel, value) {
|
|
||||||
const now = new Date().toISOString();
|
|
||||||
|
|
||||||
const lastStmt = db.prepare(`
|
|
||||||
SELECT id, value FROM output_events
|
|
||||||
WHERE channel = ?
|
|
||||||
ORDER BY timestamp DESC LIMIT 1
|
|
||||||
`);
|
|
||||||
const last = lastStmt.get(channel);
|
|
||||||
|
|
||||||
// Debug log removed
|
|
||||||
|
|
||||||
const valueChanged = !last || Math.abs(last.value - value) >= Number.EPSILON;
|
|
||||||
|
|
||||||
if (!valueChanged) {
|
|
||||||
// Same value - update the until timestamp (RLE)
|
|
||||||
const updateStmt = db.prepare('UPDATE output_events SET until = ? WHERE id = ?');
|
|
||||||
updateStmt.run(now, last.id);
|
|
||||||
} else {
|
|
||||||
// New value - insert new record
|
|
||||||
const insertStmt = db.prepare(`
|
|
||||||
INSERT INTO output_events (timestamp, until, channel, value, data_type)
|
|
||||||
VALUES (?, NULL, ?, ?, 'number')
|
|
||||||
`);
|
|
||||||
insertStmt.run(now, channel, value);
|
|
||||||
console.log(`[RuleRunner] Output changed: ${channel} = ${value}`);
|
|
||||||
|
|
||||||
// Send command to bound physical device
|
|
||||||
const binding = OUTPUT_BINDINGS[channel];
|
|
||||||
if (binding) {
|
|
||||||
let commandValue = value;
|
|
||||||
if (binding.type === 'switch') {
|
|
||||||
commandValue = value > 0 ? 1 : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`[RuleRunner] Binding for ${channel}: type=${binding.type}, val=${value}, cmdVal=${commandValue}`);
|
|
||||||
|
|
||||||
sendCommandToDevicePrefix(`${binding.device}:`, {
|
|
||||||
device: binding.channel,
|
|
||||||
action: 'set_state',
|
|
||||||
value: commandValue
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compare values with operator
|
|
||||||
function compareValues(actual, operator, target) {
|
|
||||||
if (actual === null || actual === undefined) return false;
|
|
||||||
switch (operator) {
|
|
||||||
case '=':
|
|
||||||
case '==': return actual === target;
|
|
||||||
case '!=': return actual !== target;
|
|
||||||
case '<': return actual < target;
|
|
||||||
case '>': return actual > target;
|
|
||||||
case '<=': return actual <= target;
|
|
||||||
case '>=': return actual >= target;
|
|
||||||
default: return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Evaluate a single condition
|
|
||||||
function evaluateCondition(condition) {
|
|
||||||
const { type, operator, value, channel } = condition;
|
|
||||||
|
|
||||||
// Handle AND/OR groups
|
|
||||||
if (operator === 'AND' || operator === 'OR') {
|
|
||||||
const results = (condition.conditions || []).map(c => evaluateCondition(c));
|
|
||||||
return operator === 'AND'
|
|
||||||
? results.every(r => r)
|
|
||||||
: results.some(r => r);
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (type) {
|
|
||||||
case 'time': {
|
|
||||||
const now = new Date();
|
|
||||||
const currentTime = now.getHours() * 60 + now.getMinutes(); // minutes since midnight
|
|
||||||
|
|
||||||
if (operator === 'between' && Array.isArray(value)) {
|
|
||||||
const [start, end] = value.map(t => {
|
|
||||||
const [h, m] = t.split(':').map(Number);
|
|
||||||
return h * 60 + m;
|
|
||||||
});
|
|
||||||
return currentTime >= start && currentTime <= end;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [h, m] = String(value).split(':').map(Number);
|
|
||||||
const targetTime = h * 60 + m;
|
|
||||||
return compareValues(currentTime, operator, targetTime);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'date': {
|
|
||||||
const now = new Date();
|
|
||||||
const today = now.toISOString().split('T')[0];
|
|
||||||
|
|
||||||
if (operator === 'between' && Array.isArray(value)) {
|
|
||||||
return today >= value[0] && today <= value[1];
|
|
||||||
}
|
|
||||||
if (operator === 'before') return today < value;
|
|
||||||
if (operator === 'after') return today > value;
|
|
||||||
return today === value;
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'sensor': {
|
|
||||||
const sensorValue = getSensorValue(channel);
|
|
||||||
return compareValues(sensorValue, operator, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
case 'output': {
|
|
||||||
const outputValue = getOutputValue(channel);
|
|
||||||
return compareValues(outputValue, operator, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
default:
|
|
||||||
console.warn(`[RuleRunner] Unknown condition type: ${type}`);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run all rules
|
|
||||||
function runRules() {
|
|
||||||
if (!db) return;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rules = db.prepare('SELECT * FROM rules WHERE enabled = 1 ORDER BY position ASC').all();
|
|
||||||
|
|
||||||
// Default all outputs to OFF (0) - if no rule sets them, they stay off
|
|
||||||
const desiredOutputs = {};
|
|
||||||
for (const ch of OUTPUT_CHANNELS) {
|
|
||||||
desiredOutputs[ch.channel] = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const rule of rules) {
|
|
||||||
try {
|
|
||||||
const conditions = JSON.parse(rule.conditions || '{}');
|
|
||||||
const action = JSON.parse(rule.action || '{}');
|
|
||||||
|
|
||||||
if (evaluateCondition(conditions)) {
|
|
||||||
// Rule matches - set output (later rules override)
|
|
||||||
if (action.channel && action.value !== undefined) {
|
|
||||||
desiredOutputs[action.channel] = action.value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error(`[RuleRunner] Error evaluating rule ${rule.id}:`, err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write output values
|
|
||||||
for (const [channel, value] of Object.entries(desiredOutputs)) {
|
|
||||||
writeOutputValue(channel, value);
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
console.error('[RuleRunner] Error running rules:', err.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Start rule runner (every 10 seconds)
|
// Start rule runner (every 10 seconds)
|
||||||
const ruleRunnerInterval = setInterval(runRules, 10000);
|
const ruleRunnerInterval = setInterval(runRules, 10000);
|
||||||
|
|||||||
Reference in New Issue
Block a user