Skip to content

Commit 655aa27

Browse files
committed
feat(notifications): add adjustable condition order and AND/OR connector support
Implemented ability to reorder notification rule conditions Added support for AND/OR connectors to require all or any conditions to trigger
1 parent 6ca2849 commit 655aa27

File tree

9 files changed

+184
-51
lines changed

9 files changed

+184
-51
lines changed

FanX/Components/Dialogs/AddEditConditionDialog.razor

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@
1919
}
2020
}
2121
</MudSelect>
22+
<MudSelect T="ConditionLogicalOperator" @bind-Value="@_condition.Connector" Label="Connector" Required="true" Class="mt-4">
23+
<MudSelectItem Value="ConditionLogicalOperator.And">AND</MudSelectItem>
24+
<MudSelectItem Value="ConditionLogicalOperator.Or">OR</MudSelectItem>
25+
</MudSelect>
2226
<MudSelect T="TriggerOperator" @bind-Value="@_condition.Operator" Label="@Localization.Operator" Required="true" Class="mt-4">
2327
@foreach (TriggerOperator op in Enum.GetValues(typeof(TriggerOperator)))
2428
{
@@ -91,4 +95,4 @@
9195
{
9296
LocalizationService.OnLanguageChanged -= StateHasChanged;
9397
}
94-
}
98+
}

FanX/Components/Dialogs/AddEditNotificationRuleDialog.razor

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,31 @@
3939
<MudButton Variant="Variant.Filled" Color="Color.Primary" OnClick="@(() => OpenConditionDialog())">@Localization.AddCondition</MudButton>
4040
</ToolBarContent>
4141
<HeaderContent>
42+
<MudTh></MudTh> <!-- Order controls -->
4243
<MudTh>@Localization.Sensor</MudTh>
4344
<MudTh>@Localization.Operator</MudTh>
45+
<MudTh>Connector</MudTh>
4446
<MudTh>@Localization.Threshold</MudTh>
4547
<MudTh>@Localization.Actions</MudTh>
4648
</HeaderContent>
4749
<RowTemplate>
50+
<MudTd DataLabel=""> <!-- Order controls -->
51+
<MudIconButton Icon="@Icons.Material.Filled.ArrowUpward"
52+
Disabled="@(_rule.Conditions.IndexOf(context) == 0)"
53+
OnClick="@(() => MoveConditionUp(context))" Size="Size.Small" />
54+
<MudIconButton Icon="@Icons.Material.Filled.ArrowDownward"
55+
Disabled="@(_rule.Conditions.IndexOf(context) == _rule.Conditions.Count - 1)"
56+
OnClick="@(() => MoveConditionDown(context))" Size="Size.Small" />
57+
</MudTd>
4858
<MudTd DataLabel="@Localization.Sensor">@context.SensorName</MudTd>
4959
<MudTd DataLabel="@Localization.Operator">@GetOperatorDisplayName(context.Operator)</MudTd>
60+
<MudTd DataLabel="Connector">@context.Connector</MudTd>
5061
<MudTd DataLabel="@Localization.Threshold">@context.Threshold</MudTd>
5162
<MudTd DataLabel="@Localization.Actions">
52-
<MudIconButton Icon="@Icons.Material.Filled.Edit" Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small" OnClick="@(() => OpenConditionDialog(context))"/>
53-
<MudIconButton Icon="@Icons.Material.Filled.Delete" Variant="Variant.Filled" Color="Color.Error" Size="Size.Small" OnClick="@(() => RemoveCondition(context))"/>
63+
<div style="display: flex; gap: 4px;">
64+
<MudIconButton Icon="@Icons.Material.Filled.Edit" Variant="Variant.Filled" Color="Color.Primary" Size="Size.Small" OnClick="@(() => OpenConditionDialog(context))"/>
65+
<MudIconButton Icon="@Icons.Material.Filled.Delete" Variant="Variant.Filled" Color="Color.Error" Size="Size.Small" OnClick="@(() => RemoveCondition(context))"/>
66+
</div>
5467
</MudTd>
5568
</RowTemplate>
5669
<NoRecordsContent>
@@ -152,8 +165,34 @@
152165
}
153166
}
154167

168+
private void MoveConditionUp(NotificationCondition condition)
169+
{
170+
var list = _rule?.Conditions;
171+
if (list == null) return;
172+
var index = list.IndexOf(condition);
173+
if (index > 0)
174+
{
175+
list.RemoveAt(index);
176+
list.Insert(index - 1, condition);
177+
StateHasChanged();
178+
}
179+
}
180+
181+
private void MoveConditionDown(NotificationCondition condition)
182+
{
183+
var list = _rule?.Conditions;
184+
if (list == null) return;
185+
var index = list.IndexOf(condition);
186+
if (index >= 0 && index < list.Count - 1)
187+
{
188+
list.RemoveAt(index);
189+
list.Insert(index + 1, condition);
190+
StateHasChanged();
191+
}
192+
}
193+
155194
public void Dispose()
156195
{
157196
LocalizationService.OnLanguageChanged -= StateHasChanged;
158197
}
159-
}
198+
}

FanX/Components/Pages/Notifications.razor

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,22 @@ else
127127
<MudButton Variant="Variant.Filled" Color="Color.Success" StartIcon="@Icons.Material.Filled.Add" OnClick="@(() => OpenAddEditDialog(0))">@Localization.AddRule</MudButton>
128128
</ToolBarContent>
129129
<HeaderContent>
130+
<MudTh></MudTh> <!-- Order column -->
130131
<MudTh>@Localization.RuleName</MudTh>
131132
<MudTh>@Localization.Conditions</MudTh>
132133
<MudTh>@Localization.CooldownMins</MudTh>
133134
<MudTh>@Localization.Enabled</MudTh>
134135
<MudTh>@Localization.Actions</MudTh>
135136
</HeaderContent>
136137
<RowTemplate>
138+
<MudTd DataLabel=""> <!-- Order controls -->
139+
<MudIconButton Icon="@Icons.Material.Filled.ArrowUpward"
140+
Disabled="@(context.SortOrder == _rules.First().SortOrder)"
141+
OnClick="@(async () => await MoveUp(context.Id))" Size="Size.Small" />
142+
<MudIconButton Icon="@Icons.Material.Filled.ArrowDownward"
143+
Disabled="@(context.SortOrder == _rules.Last().SortOrder)"
144+
OnClick="@(async () => await MoveDown(context.Id))" Size="Size.Small" />
145+
</MudTd>
137146
<MudTd DataLabel="@Localization.RuleName">@context.Name</MudTd>
138147
<MudTd DataLabel="@Localization.Conditions">@context.Conditions.Count</MudTd>
139148
<MudTd DataLabel="@Localization.CooldownMins">@context.FrequencyMinutes</MudTd>
@@ -307,6 +316,47 @@ else
307316
}
308317
}
309318

319+
private async Task MoveUp(int id)
320+
{
321+
await ServerReload(null, CancellationToken.None); // ensure _rules is loaded
322+
var index = _rules.FindIndex(r => r.Id == id);
323+
if (index > 0)
324+
{
325+
var curr = _rules[index];
326+
var prev = _rules[index - 1];
327+
(curr.SortOrder, prev.SortOrder) = (prev.SortOrder, curr.SortOrder);
328+
await NotificationService.SaveNotificationRuleAsync(prev);
329+
await NotificationService.SaveNotificationRuleAsync(curr);
330+
if (_table != null)
331+
await _table.ReloadServerData();
332+
Snackbar.Add(Localization.RuleOrderUpdated, Severity.Success);
333+
}
334+
else
335+
{
336+
Snackbar.Add(Localization.RuleOrderMoveFailed, Severity.Warning);
337+
}
338+
}
339+
private async Task MoveDown(int id)
340+
{
341+
await ServerReload(null, CancellationToken.None);
342+
var index = _rules.FindIndex(r => r.Id == id);
343+
if (index >= 0 && index < _rules.Count - 1)
344+
{
345+
var curr = _rules[index];
346+
var next = _rules[index + 1];
347+
(curr.SortOrder, next.SortOrder) = (next.SortOrder, curr.SortOrder);
348+
await NotificationService.SaveNotificationRuleAsync(next);
349+
await NotificationService.SaveNotificationRuleAsync(curr);
350+
if (_table != null)
351+
await _table.ReloadServerData();
352+
Snackbar.Add(Localization.RuleOrderUpdated, Severity.Success);
353+
}
354+
else
355+
{
356+
Snackbar.Add(Localization.RuleOrderMoveFailed, Severity.Warning);
357+
}
358+
}
359+
310360
public void Dispose()
311361
{
312362
LocalizationService.OnLanguageChanged -= StateHasChanged;
@@ -335,4 +385,4 @@ else
335385
position: relative;
336386
top: 0.15rem;
337387
}
338-
</style>
388+
</style>
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace FanX.Models;
2+
3+
public enum ConditionLogicalOperator
4+
{
5+
And,
6+
Or
7+
}

FanX/Models/FanControlCondition.cs

Lines changed: 13 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,21 @@
11
using SqlSugar;
22

3-
namespace FanX.Models
4-
{
5-
public enum ConditionLogicalOperator
6-
{
7-
And,
8-
Or
9-
}
3+
namespace FanX.Models;
104

11-
[SugarTable("FanControlConditions")]
12-
public class FanControlCondition
13-
{
14-
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
15-
public int Id { get; set; }
5+
[SugarTable("FanControlConditions")]
6+
public class FanControlCondition
7+
{
8+
[SugarColumn(IsPrimaryKey = true, IsIdentity = true)]
9+
public int Id { get; set; }
1610

17-
[SugarColumn(ColumnName = "RuleId")]
18-
public int RuleId { get; set; }
11+
[SugarColumn(ColumnName = "RuleId")]
12+
public int RuleId { get; set; }
1913

20-
public string? SensorName { get; set; }
14+
public string? SensorName { get; set; }
2115

22-
public TriggerOperator Operator { get; set; } = TriggerOperator.GreaterThan;
16+
public TriggerOperator Operator { get; set; } = TriggerOperator.GreaterThan;
2317

24-
public double Threshold { get; set; }
18+
public double Threshold { get; set; }
2519

26-
public ConditionLogicalOperator Connector { get; set; } = ConditionLogicalOperator.And;
27-
}
28-
}
20+
public ConditionLogicalOperator Connector { get; set; } = ConditionLogicalOperator.And;
21+
}

FanX/Models/NotificationCondition.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ public enum TriggerOperator
99
EqualTo
1010
}
1111

12+
1213
[SugarTable("NotificationConditions")]
1314
public class NotificationCondition
1415
{
@@ -23,4 +24,6 @@ public class NotificationCondition
2324
public TriggerOperator Operator { get; set; } = TriggerOperator.GreaterThan;
2425

2526
public double Threshold { get; set; }
26-
}
27+
28+
public ConditionLogicalOperator Connector { get; set; } = ConditionLogicalOperator.And;
29+
}

FanX/Models/NotificationRule.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ public class NotificationRule
1212

1313
public string Name { get; set; } = "New Rule";
1414

15+
public int SortOrder { get; set; } = 0;
16+
1517
// Cooldown in minutes
1618
public int FrequencyMinutes { get; set; } = 10;
1719

@@ -20,4 +22,4 @@ public class NotificationRule
2022
// Navigation property for conditions
2123
[SugarColumn(IsIgnore = true)]
2224
public List<NotificationCondition> Conditions { get; set; } = new();
23-
}
25+
}

FanX/Services/NotificationService.cs

Lines changed: 53 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,9 @@ public async Task<bool> SaveNotificationSettingAsync(NotificationSetting setting
2929

3030
public async Task<List<NotificationRule>> GetNotificationRulesAsync()
3131
{
32-
var rules = await _dbService.Db.Queryable<NotificationRule>().ToListAsync();
32+
var rules = await _dbService.Db.Queryable<NotificationRule>()
33+
.OrderBy(r => r.SortOrder)
34+
.ToListAsync();
3335
if (rules.Any())
3436
{
3537
var ruleIds = rules.Select(r => r.Id).ToList();
@@ -61,12 +63,14 @@ public async Task<bool> SaveNotificationRuleAsync(NotificationRule rule)
6163
// Explicitly handle Insert vs. Update for the rule for more reliability
6264
if (rule.Id == 0)
6365
{
64-
// It's a new rule, insert it and get the new ID
66+
// New rule: assign SortOrder as max existing + 1
67+
var maxSort = await _dbService.Db.Queryable<NotificationRule>().MaxAsync<int>(r => r.SortOrder);
68+
rule.SortOrder = maxSort + 1;
6569
rule.Id = await _dbService.Db.Insertable(rule).ExecuteReturnIdentityAsync();
6670
}
6771
else
6872
{
69-
// It's an existing rule, update it
73+
// Existing rule: update
7074
await _dbService.Db.Updateable(rule).ExecuteCommandAsync();
7175
}
7276

@@ -125,37 +129,62 @@ public async Task CheckAndSendNotificationsAsync(IEnumerable<SensorData> sensorR
125129

126130
foreach (var rule in rules.Where(r => r.IsEnabled && r.Conditions.Any()))
127131
{
128-
var allConditionsMet = true;
129-
var conditionsSummary = new StringBuilder();
130-
131-
foreach (var condition in rule.Conditions)
132+
var shouldTrigger = EvaluateRuleConditions(rule.Conditions, sensorReadingsList);
133+
134+
if (shouldTrigger.IsMet)
132135
{
133-
var sensorReading = sensorReadingsList.FirstOrDefault(s =>
134-
string.Equals(s.SensorName, condition.SensorName, StringComparison.OrdinalIgnoreCase));
136+
await HandleTriggeredRule(rule, settings, shouldTrigger.Summary);
137+
}
138+
}
139+
}
135140

136-
if (sensorReading == null)
137-
{
138-
// If a sensor in a condition is not found in the latest readings, the condition cannot be met.
139-
allConditionsMet = false;
140-
break;
141-
}
141+
private (bool IsMet, string Summary) EvaluateRuleConditions(List<NotificationCondition> conditions, List<SensorData> sensorReadings)
142+
{
143+
if (!conditions.Any()) return (false, "");
142144

143-
var conditionMetThisCycle = IsConditionMet(sensorReading.Reading, condition.Threshold, condition.Operator);
145+
var conditionsSummary = new StringBuilder();
146+
var results = new List<bool>();
144147

145-
if (!conditionMetThisCycle)
146-
{
147-
allConditionsMet = false;
148-
break;
149-
}
148+
// Evaluate each condition
149+
foreach (var condition in conditions)
150+
{
151+
var sensorReading = sensorReadings.FirstOrDefault(s =>
152+
string.Equals(s.SensorName, condition.SensorName, StringComparison.OrdinalIgnoreCase));
150153

154+
if (sensorReading == null)
155+
{
156+
// If sensor not found, condition is false
157+
results.Add(false);
158+
continue;
159+
}
160+
161+
var conditionMet = IsConditionMet(sensorReading.Reading, condition.Threshold, condition.Operator);
162+
results.Add(conditionMet);
163+
164+
if (conditionMet)
165+
{
151166
conditionsSummary.Append($"{sensorReading.SensorName} was {sensorReading.Reading:F1}{sensorReading.Unit}; ");
152167
}
168+
}
169+
170+
// Apply And/Or logic
171+
var finalResult = results[0]; // Start with first condition result
153172

154-
if (allConditionsMet)
173+
for (int i = 1; i < conditions.Count; i++)
174+
{
175+
var connector = conditions[i].Connector;
176+
177+
if (connector == ConditionLogicalOperator.And)
155178
{
156-
await HandleTriggeredRule(rule, settings, conditionsSummary.ToString().TrimEnd(' ', ';'));
179+
finalResult = finalResult && results[i];
180+
}
181+
else if (connector == ConditionLogicalOperator.Or)
182+
{
183+
finalResult = finalResult || results[i];
157184
}
158185
}
186+
187+
return (finalResult, conditionsSummary.ToString().TrimEnd(' ', ';'));
159188
}
160189

161190
private async Task HandleTriggeredRule(NotificationRule rule, NotificationSetting settings, string conditionsSummary)
@@ -370,4 +399,4 @@ private async Task<bool> SendWeComBotAsync(string key, string message)
370399
return false;
371400
}
372401
}
373-
}
402+
}

Models/NotificationCondition.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
public enum ConditionLogicalOperator
2+
{
3+
And,
4+
Or
5+
}
6+

0 commit comments

Comments
 (0)