u
This commit is contained in:
@@ -43,6 +43,7 @@ class ViewManager extends Component {
|
||||
this.state = {
|
||||
views: [],
|
||||
rules: [],
|
||||
activeRuleIds: [],
|
||||
outputValues: {},
|
||||
open: false,
|
||||
colorPickerOpen: false,
|
||||
@@ -77,7 +78,8 @@ class ViewManager extends Component {
|
||||
this.rulesInterval = setInterval(() => {
|
||||
this.loadRules();
|
||||
this.loadOutputValues();
|
||||
}, 30000);
|
||||
this.loadRuleStatus();
|
||||
}, 5000);
|
||||
if (this.isAdmin()) {
|
||||
fetch('/api/devices')
|
||||
.then(res => res.json())
|
||||
@@ -128,6 +130,13 @@ class ViewManager extends Component {
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
loadRuleStatus = () => {
|
||||
fetch('/api/rules/status')
|
||||
.then(res => res.json())
|
||||
.then(data => this.setState({ activeRuleIds: data.activeIds || [] }))
|
||||
.catch(console.error);
|
||||
};
|
||||
|
||||
parseViewData(view) {
|
||||
let channels = [];
|
||||
let axes = { left: { min: '', max: '' }, right: { min: '', max: '' } };
|
||||
@@ -197,7 +206,10 @@ class ViewManager extends Component {
|
||||
}
|
||||
return `📅 ${operator} ${value}`;
|
||||
case 'sensor':
|
||||
// Show device:channel for clarity (e.g. "ac:controller:humidity")
|
||||
// Show device:channel for clarity
|
||||
if (value && value.type === 'dynamic') {
|
||||
return `📡 ${channel} ${op} (${value.channel} * ${value.factor} + ${value.offset})`;
|
||||
}
|
||||
return `📡 ${channel} ${op} ${value}`;
|
||||
case 'output':
|
||||
return `⚙️ ${channel} ${op} ${value}`;
|
||||
@@ -216,6 +228,11 @@ class ViewManager extends Component {
|
||||
'TentExhaust': '💨 Tent Exhaust Fan'
|
||||
};
|
||||
const name = channelNames[action.channel] || action.channel;
|
||||
|
||||
if (action.value && action.value.type === 'calculated') {
|
||||
return `${name} = (${action.value.sensorA} - ${action.value.sensorB || '0'}) * ${action.value.factor} + ${action.value.offset}`;
|
||||
}
|
||||
|
||||
return `${name} = ${action.value}`;
|
||||
};
|
||||
|
||||
@@ -508,35 +525,38 @@ class ViewManager extends Component {
|
||||
<Paper sx={{ p: 2, mt: 4 }}>
|
||||
<Typography variant="h5" sx={{ mb: 2 }}>🤖 Active Rules</Typography>
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', gap: 1 }}>
|
||||
{this.state.rules.filter(r => r.enabled).map((rule, idx) => (
|
||||
<Box
|
||||
key={rule.id}
|
||||
sx={{
|
||||
p: 1.5,
|
||||
bgcolor: 'background.paper',
|
||||
borderRadius: 1,
|
||||
border: '1px solid #504945',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: '1.2em' }}>
|
||||
{this.getRuleEmoji(rule)}
|
||||
</Typography>
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
|
||||
{rule.name}
|
||||
{this.state.rules.filter(r => r.enabled).map((rule, idx) => {
|
||||
const isActive = this.state.activeRuleIds.includes(rule.id);
|
||||
return (
|
||||
<Box
|
||||
key={rule.id}
|
||||
sx={{
|
||||
p: 1.5,
|
||||
bgcolor: isActive ? 'rgba(76, 175, 80, 0.15)' : 'background.paper',
|
||||
borderRadius: 1,
|
||||
border: isActive ? '1px solid #4caf50' : '1px solid #504945',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: 2
|
||||
}}
|
||||
>
|
||||
<Typography sx={{ fontSize: '1.2em' }}>
|
||||
{this.getRuleEmoji(rule)}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{this.formatRuleConditions(rule.conditions)} → {this.formatRuleAction(rule.action)}
|
||||
<Box sx={{ flex: 1 }}>
|
||||
<Typography variant="subtitle1" sx={{ fontWeight: 'bold' }}>
|
||||
{rule.name}
|
||||
</Typography>
|
||||
<Typography variant="body2" color="text.secondary">
|
||||
{this.formatRuleConditions(rule.conditions)} → {this.formatRuleAction(rule.action)}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography sx={{ fontSize: '0.85em', color: 'text.secondary' }}>
|
||||
#{idx + 1}
|
||||
</Typography>
|
||||
</Box>
|
||||
<Typography sx={{ fontSize: '0.85em', color: 'text.secondary' }}>
|
||||
#{idx + 1}
|
||||
</Typography>
|
||||
</Box>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
</Box>
|
||||
<Typography variant="body2" color="text.secondary" sx={{ mt: 2, textAlign: 'center' }}>
|
||||
📊 Current outputs: {Object.entries(this.state.outputValues).filter(([k, v]) => v > 0).map(([k, v]) => `${k}=${v}`).join(', ') || 'all off'}
|
||||
|
||||
@@ -491,6 +491,9 @@ function evaluateCondition(condition) {
|
||||
}
|
||||
}
|
||||
|
||||
// Global set to track currently active rule IDs
|
||||
const activeRuleIds = new Set();
|
||||
|
||||
// Run all rules
|
||||
function runRules() {
|
||||
if (!db) return;
|
||||
@@ -498,6 +501,9 @@ function runRules() {
|
||||
try {
|
||||
const rules = db.prepare('SELECT * FROM rules WHERE enabled = 1 ORDER BY position ASC').all();
|
||||
|
||||
// Clear active rules list at start of run
|
||||
activeRuleIds.clear();
|
||||
|
||||
// Default all outputs to OFF (0) - if no rule sets them, they stay off
|
||||
const desiredOutputs = {};
|
||||
for (const ch of OUTPUT_CHANNELS) {
|
||||
@@ -510,6 +516,9 @@ function runRules() {
|
||||
const action = JSON.parse(rule.action || '{}');
|
||||
|
||||
if (evaluateCondition(conditions)) {
|
||||
// Rule matches - add to active list
|
||||
activeRuleIds.add(rule.id);
|
||||
|
||||
// Rule matches - set output (later rules override)
|
||||
if (action.channel && action.value !== undefined) {
|
||||
let finalValue = action.value;
|
||||
@@ -818,6 +827,11 @@ module.exports = {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/rules/status - Get currently active rule IDs
|
||||
app.get('/api/rules/status', (req, res) => {
|
||||
res.json({ activeIds: Array.from(activeRuleIds) });
|
||||
});
|
||||
|
||||
// GET /api/rules - List all rules
|
||||
app.get('/api/rules', (req, res) => {
|
||||
try {
|
||||
@@ -835,6 +849,8 @@ module.exports = {
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// POST /api/rules - Create rule (admin only)
|
||||
app.post('/api/rules', requireAdmin, (req, res) => {
|
||||
const { name, type = 'static', enabled = 1, conditions, action } = req.body;
|
||||
|
||||
Reference in New Issue
Block a user