Skip to content

Commit 6ca2849

Browse files
committed
feat(FanControl): add adjustable rule trigger order and fix single rule trigger bug
Implemented ability to adjust the trigger order of FanControl rules Fixed bug where only one rule would be triggered
1 parent 29d33ab commit 6ca2849

File tree

6 files changed

+114
-63
lines changed

6 files changed

+114
-63
lines changed

FanX/Components/Pages/FanControl.razor

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,22 @@
3333
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="@(() => OpenAddEditDialog(0))">@Localization.AddNewRule</MudButton>
3434
</ToolBarContent>
3535
<HeaderContent>
36+
<MudTh></MudTh> <!-- Sort order column -->
3637
<MudTh>@Localization.Enabled</MudTh>
3738
<MudTh>@Localization.RuleName</MudTh>
3839
<MudTh>@Localization.Conditions</MudTh>
3940
<MudTh>@Localization.TargetFanSpeed</MudTh>
4041
<MudTh>@Localization.Actions</MudTh>
4142
</HeaderContent>
4243
<RowTemplate>
44+
<MudTd DataLabel=""> <!-- Order controls -->
45+
<MudIconButton Icon="@Icons.Material.Filled.ArrowUpward"
46+
Disabled="@(context.SortOrder == _rules.First().SortOrder)"
47+
OnClick="@(async () => await MoveUp(context.Id))" Size="Size.Small" />
48+
<MudIconButton Icon="@Icons.Material.Filled.ArrowDownward"
49+
Disabled="@(context.SortOrder == _rules.Last().SortOrder)"
50+
OnClick="@(async () => await MoveDown(context.Id))" Size="Size.Small" />
51+
</MudTd>
4352
<MudTd DataLabel="@Localization.Enabled">
4453
<div class="form-check form-switch">
4554
<input class="form-check-input" type="checkbox" role="switch"
@@ -119,9 +128,52 @@
119128
private async Task LoadRules()
120129
{
121130
_rules = await FanControlService.GetRulesAsync();
122-
if (_rules.Any())
131+
// Ensure SortOrder values are unique and sequential if missing
132+
for (int i = 0; i < _rules.Count; i++)
133+
{
134+
if (_rules[i].SortOrder != i)
135+
{
136+
_rules[i].SortOrder = i;
137+
await FanControlService.SaveRuleAsync(_rules[i]);
138+
}
139+
}
140+
}
141+
private async Task MoveUp(int ruleId)
142+
{
143+
var index = _rules.FindIndex(r => r.Id == ruleId);
144+
if (index > 0)
145+
{
146+
var curr = _rules[index];
147+
var prev = _rules[index - 1];
148+
(curr.SortOrder, prev.SortOrder) = (prev.SortOrder, curr.SortOrder);
149+
await FanControlService.SaveRuleAsync(prev);
150+
await FanControlService.SaveRuleAsync(curr);
151+
await LoadRules();
152+
StateHasChanged();
153+
Snackbar.Add(Localization.RuleOrderUpdated, Severity.Success);
154+
}
155+
else
123156
{
124-
LoggerService.Info($"[FanControl] LoadRules finished. First rule (ID {_rules.First().Id}) has IsEnabled = {_rules.First().IsEnabled}.");
157+
Snackbar.Add(Localization.RuleOrderMoveFailed, Severity.Warning);
158+
}
159+
}
160+
private async Task MoveDown(int ruleId)
161+
{
162+
var index = _rules.FindIndex(r => r.Id == ruleId);
163+
if (index >= 0 && index < _rules.Count - 1)
164+
{
165+
var curr = _rules[index];
166+
var next = _rules[index + 1];
167+
(curr.SortOrder, next.SortOrder) = (next.SortOrder, curr.SortOrder);
168+
await FanControlService.SaveRuleAsync(next);
169+
await FanControlService.SaveRuleAsync(curr);
170+
await LoadRules();
171+
StateHasChanged();
172+
Snackbar.Add(Localization.RuleOrderUpdated, Severity.Success);
173+
}
174+
else
175+
{
176+
Snackbar.Add(Localization.RuleOrderMoveFailed, Severity.Warning);
125177
}
126178
}
127179

@@ -164,4 +216,4 @@
164216
{
165217
LocalizationService.OnLanguageChanged -= StateHasChanged;
166218
}
167-
}
219+
}

FanX/Models/FanControlRule.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ public class FanControlRule
1212

1313
public string Name { get; set; } = string.Empty;
1414

15+
// Field for custom sort order
16+
public int SortOrder { get; set; }
17+
1518
public int TargetFanSpeedPercent { get; set; }
1619

1720
public string TargetFanNamesJson { get; set; } = "[]";
@@ -21,4 +24,4 @@ public class FanControlRule
2124

2225
[SugarColumn(IsIgnore = true)]
2326
public List<FanControlCondition> Conditions { get; set; } = new();
24-
}
27+
}

FanX/Resources/Localization.Designer.cs

Lines changed: 18 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

FanX/Resources/Localization.resx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,14 @@
191191
<value>Warning</value>
192192
<comment>Warning text</comment>
193193
</data>
194+
<data name="RuleOrderUpdated" xml:space="preserve">
195+
<value>Rule order updated.</value>
196+
<comment>Snackbar message after updating rule order</comment>
197+
</data>
198+
<data name="RuleOrderMoveFailed" xml:space="preserve">
199+
<value>Cannot move rule further.</value>
200+
<comment>Snackbar warning when rule cannot be moved</comment>
201+
</data>
194202

195203
<!-- Language -->
196204
<data name="Language" xml:space="preserve">

FanX/Services/FanControlService.cs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ public FanControlService(DatabaseService dbService)
1414

1515
public async Task<List<FanControlRule>> GetRulesAsync()
1616
{
17-
var rules = await _dbService.Db.Queryable<FanControlRule>().ToListAsync();
17+
var rules = await _dbService.Db.Queryable<FanControlRule>()
18+
.OrderBy(r => r.SortOrder)
19+
.ToListAsync();
1820
if (rules.Count == 0) return rules;
1921
var ruleIds = rules.Select(r => r.Id).ToList();
2022
var allConditions = await _dbService.Db.Queryable<FanControlCondition>().Where(c => ruleIds.Contains(c.RuleId)).ToListAsync();
@@ -56,7 +58,7 @@ public async Task<bool> SaveRuleAsync(FanControlRule rule)
5658

5759
await _dbService.Db.Deleteable<FanControlCondition>().Where(c => c.RuleId == rule.Id).ExecuteCommandAsync();
5860

59-
if (rule.Conditions != null && rule.Conditions.Count != 0)
61+
if (rule.Conditions.Any())
6062
{
6163
rule.Conditions.ForEach(c => { c.RuleId = rule.Id; c.Id = 0; });
6264
await _dbService.Db.Insertable(rule.Conditions).ExecuteCommandAsync();
@@ -111,4 +113,4 @@ public async Task SetFanControlModeAsync(FanControlMode mode)
111113
await _dbService.Db.Storageable(setting).ExecuteCommandAsync();
112114
}
113115
}
114-
}
116+
}

FanX/Services/SensorLoggingService.cs

Lines changed: 24 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -69,74 +69,42 @@ private async Task AdjustFanSpeedBasedOnRules(FanControlService fanControlServic
6969
// If we reach here, the mode is Smart.
7070
var rules = (await fanControlService.GetRulesAsync())
7171
.Where(r => r.IsEnabled && r.Conditions.Any())
72-
.OrderByDescending(r => r.TargetFanSpeedPercent)
72+
.OrderBy(r => r.SortOrder)
7373
.ToList();
74-
75-
FanControlRule? triggeredRule = null;
74+
bool anyTriggered = false;
75+
// Switch to manual mode once before applying rules
76+
await ipmiService.SetManualFanControlAsync();
7677
foreach (var rule in rules)
7778
{
78-
bool result = false;
79+
bool match = false;
7980
for (int i = 0; i < rule.Conditions.Count; i++)
8081
{
81-
var condition = rule.Conditions[i];
82-
var sensor = sensorData.FirstOrDefault(s => s.SensorName != null && s.SensorName.Equals(condition.SensorName, StringComparison.OrdinalIgnoreCase));
83-
var current = sensor != null && IsConditionMet(sensor.Reading, condition.Threshold, condition.Operator);
82+
var cond = rule.Conditions[i];
83+
var sensor = sensorData.FirstOrDefault(s => s.SensorName != null && s.SensorName.Equals(cond.SensorName, StringComparison.OrdinalIgnoreCase));
84+
var current = sensor != null && IsConditionMet(sensor.Reading, cond.Threshold, cond.Operator);
8485
if (i == 0)
85-
{
86-
result = current;
87-
}
86+
match = current;
87+
else if (cond.Connector == ConditionLogicalOperator.And)
88+
match &= current;
8889
else
89-
{
90-
if (condition.Connector == ConditionLogicalOperator.And)
91-
result = result && current;
92-
else
93-
result = result || current;
94-
}
95-
}
96-
if (result)
97-
{
98-
triggeredRule = rule;
99-
break;
90+
match |= current;
10091
}
101-
}
102-
103-
if (triggeredRule != null)
104-
{
105-
var metConditionsDescriptions = new List<string>();
106-
foreach (var condition in triggeredRule.Conditions)
92+
if (match)
10793
{
108-
var sensor = sensorData.FirstOrDefault(s => s.SensorName != null && s.SensorName.Equals(condition.SensorName, StringComparison.OrdinalIgnoreCase));
109-
if (sensor != null)
94+
anyTriggered = true;
95+
foreach (var fanName in rule.TargetFanNames)
11096
{
111-
var opString = condition.Operator switch
97+
var fanSensor = sensorData.FirstOrDefault(s => s.SensorName != null && s.SensorName.Equals(fanName, StringComparison.OrdinalIgnoreCase));
98+
if (fanSensor?.SensorId != null)
11299
{
113-
TriggerOperator.GreaterThan => ">",
114-
TriggerOperator.LessThan => "<",
115-
TriggerOperator.EqualTo => "==",
116-
_ => "?"
117-
};
118-
metConditionsDescriptions.Add($"'{sensor.SensorName}' ({sensor.Reading:F1}{sensor.Unit}) {opString} {condition.Threshold}");
100+
LoggerService.Info($"Rule '{rule.Name}' triggered. Setting fan '{fanName}' to {rule.TargetFanSpeedPercent}%.");
101+
await ipmiService.SetIndividualFanSpeedAsync(fanSensor.SensorId, rule.TargetFanSpeedPercent);
102+
}
119103
}
120104
}
121-
122-
var logMessage = $"Fan control rule '{triggeredRule.Name}' triggered. Conditions met: [{string.Join(" AND ", metConditionsDescriptions)}]. " +
123-
$"Setting fans [{string.Join(", ", triggeredRule.TargetFanNames)}] to {triggeredRule.TargetFanSpeedPercent}%.";
124-
125-
LoggerService.Info(logMessage);
126-
127-
await ipmiService.SetManualFanControlAsync();
128-
129-
foreach (var fanName in triggeredRule.TargetFanNames)
130-
{
131-
var fanSensor = sensorData.FirstOrDefault(s => s.SensorName == fanName);
132-
if (fanSensor?.SensorId != null)
133-
{
134-
await ipmiService.SetIndividualFanSpeedAsync(fanSensor.SensorId, triggeredRule.TargetFanSpeedPercent);
135-
}
136-
}
137-
}
138-
else
139-
{
105+
}
106+
if (!anyTriggered)
107+
{
140108
LoggerService.Info("No fan control rule triggered. Setting fans to automatic.");
141109
await ipmiService.SetAutomaticFanControlAsync();
142110
}
@@ -165,4 +133,4 @@ public void Dispose()
165133
_timer?.Dispose();
166134
}
167135
}
168-
}
136+
}

0 commit comments

Comments
 (0)