From 10556cb698890d643f8658795792a8ae97289354 Mon Sep 17 00:00:00 2001 From: sebseb7 Date: Thu, 25 Dec 2025 06:03:46 +0100 Subject: [PATCH] u --- uiserver/src/components/RuleEditor.js | 248 ++++++++++++++++++++++---- uiserver/webpack.config.js | 21 ++- 2 files changed, 228 insertions(+), 41 deletions(-) diff --git a/uiserver/src/components/RuleEditor.js b/uiserver/src/components/RuleEditor.js index 42ed8fb..7f96ab1 100644 --- a/uiserver/src/components/RuleEditor.js +++ b/uiserver/src/components/RuleEditor.js @@ -419,18 +419,85 @@ class RuleEditor extends Component { /> ) : ( - 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' ? ( + + {/* Dynamic Target Toggle */} + + { + 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 }} + /> + + + {cond.value?.type === 'dynamic' ? ( + <> + + * + this.updateCondition(condPath, { value: { ...cond.value, factor: parseFloat(e.target.value) || 0 } })} + disabled={!this.isAdmin()} + sx={{ width: 70 }} + /> + + + this.updateCondition(condPath, { value: { ...cond.value, offset: parseFloat(e.target.value) || 0 } })} + disabled={!this.isAdmin()} + sx={{ width: 70 }} + /> + + ) : ( + this.updateCondition(condPath, { value: parseFloat(e.target.value) || 0 })} + disabled={!this.isAdmin()} + sx={{ width: 140 }} + /> + )} + + ) : ( + 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)} - 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 + } } @@ -609,33 +686,126 @@ class RuleEditor extends Component { + + Action (Then) - - Set - - to - 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 }} - /> + + {/* Value Type Toggle */} + + Value Type: + this.setState({ + action: { + ...action, + value: action.value?.type === 'calculated' + ? 0 // Reset to static + : { type: 'calculated', sensorA: '', sensorB: '', factor: 1, offset: 0 } + } + })} + sx={{ cursor: 'pointer' }} + /> + + + + Set + + = + + {action.value?.type === 'calculated' ? ( + + ( + + - + + ) + * + this.setState({ + action: { + ...action, + value: { ...action.value, factor: parseFloat(e.target.value) || 0 } + } + })} + sx={{ width: 80 }} + /> + + + this.setState({ + action: { + ...action, + value: { ...action.value, offset: parseFloat(e.target.value) || 0 } + } + })} + sx={{ width: 80 }} + /> + + ) : ( + 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 }} + /> + )} + diff --git a/uiserver/webpack.config.js b/uiserver/webpack.config.js index fe53c85..c51c316 100644 --- a/uiserver/webpack.config.js +++ b/uiserver/webpack.config.js @@ -470,7 +470,14 @@ function evaluateCondition(condition) { case 'sensor': { 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': { @@ -505,7 +512,17 @@ function runRules() { if (evaluateCondition(conditions)) { // Rule matches - set output (later rules override) 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) {