u
This commit is contained in:
@@ -419,18 +419,85 @@ class RuleEditor extends Component {
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<TextField
|
||||
size="small"
|
||||
type={cond.type === 'time' ? 'time' : (cond.type === 'date' ? 'date' : 'number')}
|
||||
value={cond.value ?? ''}
|
||||
onChange={e => this.updateCondition(condPath, {
|
||||
value: cond.type === 'sensor' || cond.type === 'output'
|
||||
? parseFloat(e.target.value) || 0
|
||||
: e.target.value
|
||||
})}
|
||||
disabled={!this.isAdmin()}
|
||||
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
|
||||
size="small"
|
||||
type={cond.type === 'time' ? 'time' : (cond.type === 'date' ? 'date' : 'number')}
|
||||
value={cond.value ?? ''}
|
||||
onChange={e => this.updateCondition(condPath, {
|
||||
value: cond.type === 'output'
|
||||
? parseFloat(e.target.value) || 0
|
||||
: e.target.value
|
||||
})}
|
||||
disabled={!this.isAdmin()}
|
||||
sx={{ width: 140 }}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
|
||||
{this.isAdmin() && (
|
||||
@@ -468,6 +535,12 @@ class RuleEditor extends Component {
|
||||
: `date ${operator} ${value}`;
|
||||
break;
|
||||
case 'sensor':
|
||||
if (value && value.type === 'dynamic') {
|
||||
formatted = `${channel} ${operator} (${value.channel} * ${value.factor} + ${value.offset})`;
|
||||
} else {
|
||||
formatted = `${channel || '?'} ${operator} ${value}`;
|
||||
}
|
||||
break;
|
||||
case 'output':
|
||||
formatted = `${channel || '?'} ${operator} ${value}`;
|
||||
break;
|
||||
@@ -547,7 +620,11 @@ class RuleEditor extends Component {
|
||||
When: {this.formatConditionSummary(rule.conditions)}
|
||||
</Typography>
|
||||
<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>
|
||||
</Box>
|
||||
}
|
||||
@@ -609,33 +686,126 @@ class RuleEditor extends Component {
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<Typography variant="subtitle1" gutterBottom>Action (Then)</Typography>
|
||||
<Box sx={{ display: 'flex', gap: 2, alignItems: 'center' }}>
|
||||
<Typography>Set</Typography>
|
||||
<Select
|
||||
size="small"
|
||||
value={action.channel}
|
||||
onChange={e => this.setState({ action: { ...action, channel: e.target.value } })}
|
||||
sx={{ minWidth: 200 }}
|
||||
>
|
||||
{outputChannels.map(ch => (
|
||||
<MenuItem key={ch.channel} value={ch.channel}>
|
||||
{ch.description} ({ch.channel})
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<Typography>to</Typography>
|
||||
<TextField
|
||||
size="small"
|
||||
type="number"
|
||||
value={action.value}
|
||||
onChange={e => this.setState({ action: { ...action, value: parseFloat(e.target.value) || 0 } })}
|
||||
inputProps={{
|
||||
min: outputChannels.find(c => c.channel === action.channel)?.min || 0,
|
||||
max: outputChannels.find(c => c.channel === action.channel)?.max || 10
|
||||
}}
|
||||
sx={{ width: 100 }}
|
||||
/>
|
||||
<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>
|
||||
<Select
|
||||
size="small"
|
||||
value={action.channel}
|
||||
onChange={e => this.setState({ action: { ...action, channel: e.target.value } })}
|
||||
sx={{ minWidth: 200 }}
|
||||
>
|
||||
{outputChannels.map(ch => (
|
||||
<MenuItem key={ch.channel} value={ch.channel}>
|
||||
{ch.description} ({ch.channel})
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
<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
|
||||
size="small"
|
||||
type="number"
|
||||
value={action.value}
|
||||
onChange={e => this.setState({ action: { ...action, value: parseFloat(e.target.value) || 0 } })}
|
||||
inputProps={{
|
||||
min: outputChannels.find(c => c.channel === action.channel)?.min || 0,
|
||||
max: outputChannels.find(c => c.channel === action.channel)?.max || 10
|
||||
}}
|
||||
sx={{ width: 100 }}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
|
||||
Reference in New Issue
Block a user