This commit is contained in:
sebseb7
2025-12-25 06:03:46 +01:00
parent 22701a2614
commit 10556cb698
2 changed files with 228 additions and 41 deletions

View File

@@ -418,19 +418,86 @@ class RuleEditor extends Component {
sx={{ width: 140 }} sx={{ width: 140 }}
/> />
</> </>
) : (
cond.type === 'sensor' ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
{/* Dynamic Target Toggle */}
<Tooltip title="Compare to Value or Another Sensor">
<Chip
label={cond.value?.type === 'dynamic' ? 'Sensor' : 'Value'}
size="small"
color={cond.value?.type === 'dynamic' ? 'secondary' : 'default'}
onClick={this.isAdmin() ? () => {
const isDynamic = cond.value?.type === 'dynamic';
this.updateCondition(condPath, {
value: isDynamic
? 0 // Switch to static
: { type: 'dynamic', channel: '', factor: 1, offset: 0 } // Switch to dynamic
});
} : undefined}
sx={{ cursor: this.isAdmin() ? 'pointer' : 'default', minWidth: 60 }}
/>
</Tooltip>
{cond.value?.type === 'dynamic' ? (
<>
<Select
size="small"
value={cond.value.channel || ''}
onChange={e => this.updateCondition(condPath, { value: { ...cond.value, channel: e.target.value } })}
disabled={!this.isAdmin()}
displayEmpty
sx={{ minWidth: 150 }}
>
<MenuItem value=""><em>Target Sensor</em></MenuItem>
{sensorChannels.map(ch => <MenuItem key={ch} value={ch}>{ch}</MenuItem>)}
</Select>
<Typography>*</Typography>
<TextField
size="small"
label="Factor"
type="number"
value={cond.value.factor}
onChange={e => this.updateCondition(condPath, { value: { ...cond.value, factor: parseFloat(e.target.value) || 0 } })}
disabled={!this.isAdmin()}
sx={{ width: 70 }}
/>
<Typography>+</Typography>
<TextField
size="small"
label="Offset"
type="number"
value={cond.value.offset}
onChange={e => this.updateCondition(condPath, { value: { ...cond.value, offset: parseFloat(e.target.value) || 0 } })}
disabled={!this.isAdmin()}
sx={{ width: 70 }}
/>
</>
) : (
<TextField
size="small"
type="number"
value={cond.value ?? ''}
onChange={e => this.updateCondition(condPath, { value: parseFloat(e.target.value) || 0 })}
disabled={!this.isAdmin()}
sx={{ width: 140 }}
/>
)}
</Box>
) : ( ) : (
<TextField <TextField
size="small" size="small"
type={cond.type === 'time' ? 'time' : (cond.type === 'date' ? 'date' : 'number')} type={cond.type === 'time' ? 'time' : (cond.type === 'date' ? 'date' : 'number')}
value={cond.value ?? ''} value={cond.value ?? ''}
onChange={e => this.updateCondition(condPath, { onChange={e => this.updateCondition(condPath, {
value: cond.type === 'sensor' || cond.type === 'output' value: cond.type === 'output'
? parseFloat(e.target.value) || 0 ? parseFloat(e.target.value) || 0
: e.target.value : e.target.value
})} })}
disabled={!this.isAdmin()} disabled={!this.isAdmin()}
sx={{ width: 140 }} sx={{ width: 140 }}
/> />
)
)} )}
{this.isAdmin() && ( {this.isAdmin() && (
@@ -468,6 +535,12 @@ class RuleEditor extends Component {
: `date ${operator} ${value}`; : `date ${operator} ${value}`;
break; break;
case 'sensor': case 'sensor':
if (value && value.type === 'dynamic') {
formatted = `${channel} ${operator} (${value.channel} * ${value.factor} + ${value.offset})`;
} else {
formatted = `${channel || '?'} ${operator} ${value}`;
}
break;
case 'output': case 'output':
formatted = `${channel || '?'} ${operator} ${value}`; formatted = `${channel || '?'} ${operator} ${value}`;
break; break;
@@ -547,7 +620,11 @@ class RuleEditor extends Component {
When: {this.formatConditionSummary(rule.conditions)} When: {this.formatConditionSummary(rule.conditions)}
</Typography> </Typography>
<Typography variant="body2" color="text.secondary"> <Typography variant="body2" color="text.secondary">
Then: Set {rule.action?.channel} = {rule.action?.value} Then: Set {rule.action?.channel} = {
rule.action?.value?.type === 'calculated'
? `(${rule.action.value.sensorA} - ${rule.action.value.sensorB || '0'}) * ${rule.action.value.factor} + ${rule.action.value.offset}`
: rule.action?.value
}
</Typography> </Typography>
</Box> </Box>
} }
@@ -609,8 +686,29 @@ class RuleEditor extends Component {
<Divider sx={{ my: 2 }} /> <Divider sx={{ my: 2 }} />
<Divider sx={{ my: 2 }} />
<Typography variant="subtitle1" gutterBottom>Action (Then)</Typography> <Typography variant="subtitle1" gutterBottom>Action (Then)</Typography>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}> <Box sx={{ display: 'flex', flexDirection: 'column', gap: 2 }}>
{/* Value Type Toggle */}
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1 }}>
<Typography variant="body2">Value Type:</Typography>
<Chip
label={action.value?.type === 'calculated' ? 'Calculated' : 'Static'}
color={action.value?.type === 'calculated' ? 'secondary' : 'default'}
onClick={() => this.setState({
action: {
...action,
value: action.value?.type === 'calculated'
? 0 // Reset to static
: { type: 'calculated', sensorA: '', sensorB: '', factor: 1, offset: 0 }
}
})}
sx={{ cursor: 'pointer' }}
/>
</Box>
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center', flexWrap: 'wrap' }}>
<Typography>Set</Typography> <Typography>Set</Typography>
<Select <Select
size="small" size="small"
@@ -624,7 +722,77 @@ class RuleEditor extends Component {
</MenuItem> </MenuItem>
))} ))}
</Select> </Select>
<Typography>to</Typography> <Typography>=</Typography>
{action.value?.type === 'calculated' ? (
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, p: 1, border: '1px solid #444', borderRadius: 1 }}>
<Typography>(</Typography>
<Select
size="small"
value={action.value.sensorA || ''}
onChange={e => this.setState({
action: {
...action,
value: { ...action.value, sensorA: e.target.value }
}
})}
displayEmpty
sx={{ minWidth: 150 }}
>
<MenuItem value=""><em>Sensor A</em></MenuItem>
{this.state.devices.map(d => `${d.device}:${d.channel}`).map(ch => (
<MenuItem key={ch} value={ch}>{ch}</MenuItem>
))}
</Select>
<Typography>-</Typography>
<Select
size="small"
value={action.value.sensorB || ''}
onChange={e => this.setState({
action: {
...action,
value: { ...action.value, sensorB: e.target.value }
}
})}
displayEmpty
sx={{ minWidth: 150 }}
>
<MenuItem value=""><em>Sensor B (0)</em></MenuItem>
{this.state.devices.map(d => `${d.device}:${d.channel}`).map(ch => (
<MenuItem key={ch} value={ch}>{ch}</MenuItem>
))}
</Select>
<Typography>)</Typography>
<Typography>*</Typography>
<TextField
size="small"
type="number"
label="Factor"
value={action.value.factor}
onChange={e => this.setState({
action: {
...action,
value: { ...action.value, factor: parseFloat(e.target.value) || 0 }
}
})}
sx={{ width: 80 }}
/>
<Typography>+</Typography>
<TextField
size="small"
type="number"
label="Offset"
value={action.value.offset}
onChange={e => this.setState({
action: {
...action,
value: { ...action.value, offset: parseFloat(e.target.value) || 0 }
}
})}
sx={{ width: 80 }}
/>
</Box>
) : (
<TextField <TextField
size="small" size="small"
type="number" type="number"
@@ -636,6 +804,8 @@ class RuleEditor extends Component {
}} }}
sx={{ width: 100 }} sx={{ width: 100 }}
/> />
)}
</Box>
</Box> </Box>
</DialogContent> </DialogContent>
<DialogActions> <DialogActions>

View File

@@ -470,7 +470,14 @@ function evaluateCondition(condition) {
case 'sensor': { case 'sensor': {
const sensorValue = getSensorValue(channel); const sensorValue = getSensorValue(channel);
return compareValues(sensorValue, operator, value); let target = value;
if (value && typeof value === 'object' && value.type === 'dynamic') {
const targetSensorVal = getSensorValue(value.channel) || 0;
target = (targetSensorVal * (value.factor || 1)) + (value.offset || 0);
}
return compareValues(sensorValue, operator, target);
} }
case 'output': { case 'output': {
@@ -505,7 +512,17 @@ function runRules() {
if (evaluateCondition(conditions)) { if (evaluateCondition(conditions)) {
// Rule matches - set output (later rules override) // Rule matches - set output (later rules override)
if (action.channel && action.value !== undefined) { if (action.channel && action.value !== undefined) {
desiredOutputs[action.channel] = action.value; let finalValue = action.value;
// Handle calculated value
if (action.value && typeof action.value === 'object' && action.value.type === 'calculated') {
const valA = getSensorValue(action.value.sensorA) || 0;
const valB = action.value.sensorB ? (getSensorValue(action.value.sensorB) || 0) : 0;
const diff = valA - valB;
finalValue = (diff * (action.value.factor || 1)) + (action.value.offset || 0);
}
desiredOutputs[action.channel] = finalValue;
} }
} }
} catch (err) { } catch (err) {