mirror of
https://github.com/ThingsGateway/ThingsGateway.git
synced 2026-10-16 20:51:21 +08:00
feat: 添加变量异常过滤功能
This commit is contained in:
@@ -12,12 +12,12 @@
|
||||
<NoWarn>
|
||||
CS8002;NU1510;SYSLIB5007;CS8603;CS8618;CS1591;CS8625;CS8602;CS8604;CS8600;CS8601;CS8714;CS8619;CS8629;CS8765;CS8634;CS8621;CS8767;CS8633;CS8620;CS8610;CS8631;CS8605;CS8622;CS8613;NU5100;NU5104;NU1903;NU1902;CA1863;CA1812;CA1805;CA1515;CA1508;CA1819;CA1852;CA5394;CA1822;CA1815;CA1813;CA5358;CA5384;CA5400;CA5401;CA1814;CA1835;CA5392;CA5350;CA2100;CA1848;CA1810;CA1513;CA5351;CA1510;CA1512;CA1823;RCS1102;RCS1194;NETSDK1206
|
||||
</NoWarn>
|
||||
<TouchSocketVersion>4.1.10</TouchSocketVersion>
|
||||
<AdminVersion>12.5.2</AdminVersion>
|
||||
<FoundationVersion>12.5.5</FoundationVersion>
|
||||
<PluginVersion>12.5.6</PluginVersion>
|
||||
<TouchSocketVersion>4.1.12</TouchSocketVersion>
|
||||
<AdminVersion>12.5.3</AdminVersion>
|
||||
<FoundationVersion>12.5.6</FoundationVersion>
|
||||
<PluginVersion>12.5.7</PluginVersion>
|
||||
<ProPluginVersion>12.5.4</ProPluginVersion>
|
||||
<GatewayVersion>12.5.4</GatewayVersion>
|
||||
<GatewayVersion>12.5.5</GatewayVersion>
|
||||
<Version>$(GatewayVersion)</Version>
|
||||
|
||||
<ManagementVersion>12.5.1</ManagementVersion>
|
||||
|
||||
@@ -0,0 +1,264 @@
|
||||
# 变量异常值过滤功能说明
|
||||
|
||||
## 1. 功能概述
|
||||
|
||||
为 `Variable`(设备变量)新增 **异常值过滤(Outlier Filter)** 能力。当采集到的变量值发生变化时,系统会自动对新值进行异常检测,拦截不合理的数据,防止异常数据进入业务流程。
|
||||
|
||||
### 设计原则
|
||||
|
||||
- **仅在线时生效**:设备离线时不执行过滤,避免干扰离线状态处理。
|
||||
- **仅在值变化时检查**:值未发生变化时不会触发过滤逻辑,减少不必要的计算开销。
|
||||
- **多级过滤、顺序执行**:三种过滤模式按顺序依次检查,任一模式判定异常即标记为异常值。
|
||||
- **连续异常容错**:当异常值连续出现达到设定次数时,认定数据确实发生了变化,自动接受该值。
|
||||
|
||||
---
|
||||
|
||||
## 2. 三种过滤模式详解
|
||||
|
||||
### 2.1 合理范围过滤
|
||||
|
||||
检查新值是否在预设的合理范围 `[最小值, 最大值]` 内。超出范围的值被视为异常。
|
||||
|
||||
**典型场景**:温度传感器量程为 -40°C ~ 120°C,超出此范围的值显然是采集异常。
|
||||
|
||||
#### 配置属性
|
||||
|
||||
| 属性名 | 数据库列 | 类型 | 默认值 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `RangeFilterEnable` | `合理范围过滤使能` | `bool` | `false` | 是否启用合理范围过滤 |
|
||||
| `RangeFilterMinValue` | `最小值` | `decimal` | `0` | 允许的最小值(含) |
|
||||
| `RangeFilterMaxValue` | `最大值` | `decimal` | `100` | 允许的最大值(含) |
|
||||
|
||||
#### 判定逻辑
|
||||
|
||||
```
|
||||
如果 新值 < 最小值 或 新值 > 最大值:
|
||||
→ 标记为异常
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.2 变化幅度过滤
|
||||
|
||||
检查新值与上次值之间的变化幅度是否超过允许上限。支持 **绝对值** 和 **百分比** 两种判定方式,两者同时生效。
|
||||
|
||||
**典型场景**:压力变送器正常波动范围为 ±5 单位 或 ±10%,突然跳变 100 单位显然是采集异常。
|
||||
|
||||
#### 配置属性
|
||||
|
||||
| 属性名 | 数据库列 | 类型 | 默认值 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `AmplitudeFilterEnable` | `变化幅度过滤使能` | `bool` | `false` | 是否启用变化幅度过滤 |
|
||||
| `MaxAmplitude` | `最大变化幅度` | `decimal` | `100` | 允许的最大绝对变化量(**> 0 时生效**,设为 0 则跳过绝对值检查) |
|
||||
| `MaxAmplitudePercent` | `最大变化幅度%` | `decimal` | `100` | 允许的最大变化百分比(单位:%)(**> 0 时生效**,设为 0 则跳过百分比检查) |
|
||||
|
||||
#### 判定逻辑
|
||||
|
||||
```
|
||||
绝对变化量 = |新值 - 旧值|
|
||||
|
||||
步骤1 — 绝对值检查(仅 MaxAmplitude > 0 时执行):
|
||||
如果 绝对变化量 > MaxAmplitude:
|
||||
→ 标记为异常
|
||||
|
||||
步骤2 — 百分比检查(仅 MaxAmplitudePercent > 0 且 步骤1未触发 且 旧值≠0 时执行):
|
||||
变化百分比 = 绝对变化量 / |旧值| × 100
|
||||
如果 变化百分比 > MaxAmplitudePercent:
|
||||
→ 标记为异常
|
||||
```
|
||||
|
||||
> **注意**:
|
||||
> - 当 `MaxAmplitude` 设为 `0` 时,跳过绝对值检查,仅执行百分比检查。
|
||||
> - 当 `MaxAmplitudePercent` 设为 `0` 时,跳过百分比检查,仅执行绝对值检查。
|
||||
> - 两者同时设为 `0` 时,即使 `AmplitudeFilterEnable` 为 `true`,幅度过滤也不会触发任何拦截。
|
||||
> - 当旧值为 `0` 时,跳过百分比检查(避免除零错误)。
|
||||
|
||||
#### 两种方式的区别
|
||||
|
||||
| 对比维度 | 绝对值过滤 (`MaxAmplitude`) | 百分比过滤 (`MaxAmplitudePercent`) |
|
||||
|---|---|---|
|
||||
| 适用场景 | 变量值数量级固定 | 变量值数量级差异大 |
|
||||
| 示例 | 温度波动不超过 ±5°C | 流量变化不超过 ±20% |
|
||||
| 优势 | 简单直观 | 自适应不同数量级 |
|
||||
|
||||
---
|
||||
|
||||
### 2.3 连续异常过滤
|
||||
|
||||
当异常值 **连续出现** 达到设定次数后,系统认定数据确实发生了合理变化(如设备参数被人工调整),自动接受该值并清零计数器。
|
||||
|
||||
**典型场景**:温度设定值从 50°C 调整到 200°C,首次变化幅度超限被拦截,但如果连续 3 次都是 200°C,则说明确实发生了变化,应当接受。
|
||||
|
||||
#### 配置属性
|
||||
|
||||
| 属性名 | 数据库列 | 类型 | 默认值 | 说明 |
|
||||
|---|---|---|---|---|
|
||||
| `ContinuousFilterEnable` | `连续异常过滤使能` | `bool` | `false` | 是否启用连续异常过滤 |
|
||||
| `MaxContinuousCount` | `连续异常最大次数` | `int` | `3` | 连续异常值达到此次数后接受该值 |
|
||||
|
||||
#### 判定逻辑
|
||||
|
||||
```
|
||||
当值被判定为异常时:
|
||||
如果 连续异常过滤已启用:
|
||||
连续异常计数器++
|
||||
如果 计数器 >= MaxContinuousCount:
|
||||
→ 计数器清零,接受该值(返回 true)
|
||||
否则:
|
||||
→ 拒绝该值(返回 false)
|
||||
否则:
|
||||
→ 直接拒绝该值(返回 false)
|
||||
|
||||
当值正常(未被判定为异常)时:
|
||||
→ 计数器清零
|
||||
```
|
||||
|
||||
> **重要**:连续异常计数器仅在值实际变化(`changed == true`)时才递增,重复采集到相同的值不会增加计数。
|
||||
|
||||
---
|
||||
|
||||
## 3. 整体执行流程
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────┐
|
||||
│ Set() 方法 │
|
||||
│ │
|
||||
│ 1. 计算值是否变化 (changed) │
|
||||
│ 2. 检查条件: (changed || setChanged) && _isOnline │
|
||||
│ ├─ 条件不满足 → 跳过过滤,直接赋值 │
|
||||
│ └─ 条件满足 → 调用 ApplyOutlierFilter(data) │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ ApplyOutlierFilter(data) │
|
||||
│ │
|
||||
│ 1. 前置检查 │
|
||||
│ ├─ 无任何过滤启用 → 直接通过 │
|
||||
│ └─ 数据无法转为数值 → 直接通过(不过滤非数值类型) │
|
||||
│ │
|
||||
│ 2. 合理范围过滤 │
|
||||
│ └─ 新值不在 [Min, Max] → 标记异常 │
|
||||
│ │
|
||||
│ 3. 绝对幅度过滤(MaxAmplitude > 0 时) │
|
||||
│ └─ |新值 - 旧值| > MaxAmplitude → 标记异常 │
|
||||
│ │
|
||||
│ 4. 百分比幅度过滤(MaxAmplitudePercent > 0 且旧值≠0) │
|
||||
│ └─ 变化% > MaxAmplitudePercent → 标记异常 │
|
||||
│ │
|
||||
│ 5. 异常结果处理 │
|
||||
│ ├─ 未异常 → 计数器清零,返回 true(接受) │
|
||||
│ └─ 已异常 │
|
||||
│ ├─ 连续过滤启用 │
|
||||
│ │ ├─ 计数 >= 上限 → 清零,返回 true(接受) │
|
||||
│ │ └─ 计数 < 上限 → 返回 false(拒绝) │
|
||||
│ └─ 连续过滤未启用 → 返回 false(拒绝) │
|
||||
│ │
|
||||
├─────────────────────────────────────────────────────┤
|
||||
│ 返回到 Set() │
|
||||
│ │
|
||||
│ ├─ ApplyOutlierFilter 返回 true → 正常赋值流程 │
|
||||
│ └─ ApplyOutlierFilter 返回 false │
|
||||
│ → 触发 VariableCollectChange(更新采集时间) │
|
||||
│ → Set() 返回 false │
|
||||
│ → SetValue() 返回错误信息 │
|
||||
│ → 变量值不更新,保持上一次的有效值 │
|
||||
└─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. 涉及文件清单
|
||||
|
||||
### 4.1 后端代码
|
||||
|
||||
| 文件路径 | 变更说明 |
|
||||
|---|---|
|
||||
| `ThingsGateway.Gateway.Application/Entity/Variable.cs` | 新增 8 个数据库持久化属性(`[OrmColumn]`),分别对应三种过滤模式的配置 |
|
||||
| `ThingsGateway.Gateway.Application/Model/VariableRunTime.cs` | `Set()` 方法返回类型改为 `bool`;新增 `ApplyOutlierFilter()` 过滤逻辑方法;新增 `TryConvertToDecimal()` 辅助方法;新增 `_continuousAnomalyCount` 计数器字段 |
|
||||
|
||||
### 4.2 前端 UI
|
||||
|
||||
| 文件路径 | 变更说明 |
|
||||
|---|---|
|
||||
| `ThingsGateway.Gateway.Razor/Pages/.../VariableEditComponent.razor` | 变量编辑弹窗新增第三个 Tab 页 **"异常值过滤"**,包含三组过滤配置表单 |
|
||||
| `ThingsGateway.Management.Razor/Pages/.../VariableEditComponent.razor` | 管理端同步新增相同的 Tab 页 |
|
||||
|
||||
### 4.3 本地化
|
||||
|
||||
| 文件路径 | 新增 Key |
|
||||
|---|---|
|
||||
| `ThingsGateway.Gateway.Application/Locales/zh-CN.json` | `RangeFilterEnable`(合理范围过滤使能)、`RangeFilterMinValue`(范围最小值)、`RangeFilterMaxValue`(范围最大值)、`AmplitudeFilterEnable`(变化幅度过滤使能)、`MaxAmplitude`(最大变化幅度)、`MaxAmplitudePercent`(最大变化幅度%)、`ContinuousFilterEnable`(连续异常过滤使能)、`MaxContinuousCount`(连续异常最大次数) |
|
||||
| `ThingsGateway.Gateway.Application/Locales/en-US.json` | 同上(英文 Key) |
|
||||
| `ThingsGateway.Gateway.Razor/Locales/zh-CN.json` | `OutlierFilterInformation`(异常值过滤 — Tab 标题) |
|
||||
| `ThingsGateway.Gateway.Razor/Locales/en-US.json` | `OutlierFilterInformation`(OutlierFilter — Tab title) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 数据库变更
|
||||
|
||||
在 `variable` 表中新增以下 8 个列(SqlSugar ORM 自动迁移,`IsNullable = true`):
|
||||
|
||||
| 列名 | 类型 | 默认值 | 说明 |
|
||||
|---|---|---|---|
|
||||
| `RangeFilterEnable` | `bit` | `false` | 合理范围过滤使能 |
|
||||
| `RangeFilterMinValue` | `decimal` | `0` | 范围最小值 |
|
||||
| `RangeFilterMaxValue` | `decimal` | `100` | 范围最大值 |
|
||||
| `AmplitudeFilterEnable` | `bit` | `false` | 变化幅度过滤使能 |
|
||||
| `MaxAmplitude` | `decimal` | `100` | 最大变化幅度(绝对值),> 0 时生效 |
|
||||
| `MaxAmplitudePercent` | `decimal` | `100` | 最大变化幅度(百分比,%),> 0 时生效 |
|
||||
| `ContinuousFilterEnable` | `bit` | `false` | 连续异常过滤使能 |
|
||||
| `MaxContinuousCount` | `int` | `3` | 连续异常最大次数 |
|
||||
|
||||
---
|
||||
|
||||
## 6. UI 界面布局
|
||||
|
||||
变量编辑弹窗包含三个 Tab 页:
|
||||
|
||||
```
|
||||
┌──────────────┬──────────────┬──────────────┬──────────────┐
|
||||
│ 变量信息 │ 报警信息 │ 异常值过滤 │ 插件信息 │
|
||||
└──────────────┴──────────────┴──────────────┴──────────────┘
|
||||
```
|
||||
|
||||
**异常值过滤** Tab 页采用 3 列布局(`ItemsPerRow=3`),分三组展示:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 合理范围过滤使能 │ │ 范围最小值 │ │ 范围最大值 │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│─────────────────────────────────────────────────────────────│
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 变化幅度过滤使能 │ │ 最大变化幅度 │ │ 最大变化幅度% │ │
|
||||
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
|
||||
│─────────────────────────────────────────────────────────────│
|
||||
│ ┌─────────────────┐ ┌─────────────────┐ │
|
||||
│ │ 连续异常过滤使能 │ │ 连续异常最大次数 │ │
|
||||
│ └─────────────────┘ └─────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. 日志输出
|
||||
|
||||
过滤过程中的关键事件会通过 `LogMessage` 输出日志:
|
||||
|
||||
| 日志级别 | 事件 | 示例 |
|
||||
|---|---|---|
|
||||
| `Trace` | 范围过滤触发 | `变量A Outlier filter: value 250 out of range [0, 200]` |
|
||||
| `Trace` | 绝对幅度过滤触发 | `变量A Outlier filter: amplitude 150 exceeds max 100` |
|
||||
| `Trace` | 百分比幅度过滤触发 | `变量A Outlier filter: amplitude 85.50% exceeds max 50%` |
|
||||
| `Information` | 连续异常达上限,接受值 | `变量A Outlier filter: continuous anomaly count reached 3, accepting value 250` |
|
||||
| `Warning` | 值被拒绝(由 `SetValue` 输出) | `变量A Outlier filter rejected value` |
|
||||
|
||||
---
|
||||
|
||||
## 8. 注意事项
|
||||
|
||||
1. **非数值类型不过滤**:如果变量值无法转换为 `decimal`(如字符串、布尔值),则跳过所有过滤,直接接受该值。
|
||||
2. **首次赋值不过滤幅度**:变量的 `Value` 为 `null` 时(首次采集),幅度过滤不会执行(无旧值可比较),但范围过滤仍然生效。
|
||||
3. **设备离线时清零计数器**:设备离线时,`SetValue` 会清零连续异常计数器,确保重新上线后不会携带历史状态。
|
||||
4. **三种过滤独立启用**:每种过滤模式可独立开启或关闭,互不影响。
|
||||
5. **默认全部关闭**:所有过滤使能默认为 `false`,不影响现有变量的行为。
|
||||
6. **幅度阈值为 0 时跳过**:`MaxAmplitude` 或 `MaxAmplitudePercent` 设为 `0` 时,对应的检查项将被跳过,不会触发拦截。可以利用此特性只启用其中一种幅度检查方式。
|
||||
@@ -240,6 +240,65 @@ public class Variable : PrimaryKeyEntity, IValidatableObject
|
||||
[AutoGenerateColumn(Ignore = true)]
|
||||
public AlarmPropertys? AlarmPropertys { get; set; }
|
||||
|
||||
#region 异常值过滤
|
||||
|
||||
/// <summary>
|
||||
/// 合理范围过滤使能
|
||||
/// </summary>
|
||||
[OrmColumn(ColumnDescription = "合理范围过滤使能", IsNullable = true)]
|
||||
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
|
||||
public virtual bool RangeFilterEnable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最小值
|
||||
/// </summary>
|
||||
[OrmColumn(ColumnDescription = "最小值", IsNullable = true)]
|
||||
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
|
||||
public virtual decimal RangeFilterMinValue { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最大值
|
||||
/// </summary>
|
||||
[OrmColumn(ColumnDescription = "最大值", IsNullable = true)]
|
||||
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
|
||||
public virtual decimal RangeFilterMaxValue { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// 变化幅度过滤使能
|
||||
/// </summary>
|
||||
[OrmColumn(ColumnDescription = "变化幅度过滤使能", IsNullable = true)]
|
||||
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
|
||||
public virtual bool AmplitudeFilterEnable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最大变化幅度(与上次值的绝对差值上限)
|
||||
/// </summary>
|
||||
[OrmColumn(ColumnDescription = "最大变化幅度", IsNullable = true)]
|
||||
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
|
||||
public virtual decimal MaxAmplitude { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// 最大变化幅度百分比(与上次值的变化比例上限,单位%)
|
||||
/// </summary>
|
||||
[OrmColumn(ColumnDescription = "最大变化幅度%", IsNullable = true)]
|
||||
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
|
||||
public virtual decimal MaxAmplitudePercent { get; set; } = 100;
|
||||
|
||||
/// <summary>
|
||||
/// 连续异常过滤使能
|
||||
/// </summary>
|
||||
[OrmColumn(ColumnDescription = "连续异常过滤使能", IsNullable = true)]
|
||||
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
|
||||
public virtual bool ContinuousFilterEnable { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 连续异常最大次数,超过此次数后接受该值
|
||||
/// </summary>
|
||||
[OrmColumn(ColumnDescription = "连续异常最大次数", IsNullable = true)]
|
||||
[AutoGenerateColumn(Visible = false, Filterable = true, Sortable = true)]
|
||||
public virtual int MaxContinuousCount { get; set; } = 3;
|
||||
|
||||
#endregion 异常值过滤
|
||||
|
||||
#region 备用字段
|
||||
|
||||
|
||||
@@ -571,7 +571,15 @@
|
||||
"SaveVariable": "Add/Modify Variable",
|
||||
"Unit": "Unit",
|
||||
"VariableNotNull": "Variable name does not exist",
|
||||
"WriteExpressions": "WriteExpressions"
|
||||
"WriteExpressions": "WriteExpressions",
|
||||
"RangeFilterEnable": "RangeFilterEnable",
|
||||
"RangeFilterMinValue": "RangeFilterMinValue",
|
||||
"RangeFilterMaxValue": "RangeFilterMaxValue",
|
||||
"AmplitudeFilterEnable": "AmplitudeFilterEnable",
|
||||
"MaxAmplitude": "MaxAmplitude",
|
||||
"MaxAmplitudePercent": "MaxAmplitude%",
|
||||
"ContinuousFilterEnable": "ContinuousFilterEnable",
|
||||
"MaxContinuousCount": "MaxContinuousCount"
|
||||
},
|
||||
|
||||
"ThingsGateway.Gateway.Application.AlarmPropertys": {
|
||||
|
||||
@@ -597,7 +597,15 @@
|
||||
"SaveVariable": "添加/修改变量",
|
||||
"Unit": "单位",
|
||||
"VariableNotNull": "变量名称不存在",
|
||||
"WriteExpressions": "写入表达式"
|
||||
"WriteExpressions": "写入表达式",
|
||||
"RangeFilterEnable": "合理范围过滤使能",
|
||||
"RangeFilterMinValue": "范围最小值",
|
||||
"RangeFilterMaxValue": "范围最大值",
|
||||
"AmplitudeFilterEnable": "变化幅度过滤使能",
|
||||
"MaxAmplitude": "最大变化幅度",
|
||||
"MaxAmplitudePercent": "最大变化幅度%",
|
||||
"ContinuousFilterEnable": "连续异常过滤使能",
|
||||
"MaxContinuousCount": "连续异常最大次数"
|
||||
},
|
||||
|
||||
"ThingsGateway.Gateway.Application.AlarmPropertys": {
|
||||
|
||||
@@ -269,6 +269,11 @@ public partial class VariableRuntime : Variable
|
||||
#pragma warning disable CS0649
|
||||
protected string _lastErrorMessage;
|
||||
|
||||
/// <summary>
|
||||
/// 连续异常计数器
|
||||
/// </summary>
|
||||
private int _continuousAnomalyCount;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 设置变量值与时间/质量戳
|
||||
@@ -279,6 +284,7 @@ public partial class VariableRuntime : Variable
|
||||
RawValue = value;
|
||||
if (IsOnline == false)
|
||||
{
|
||||
_continuousAnomalyCount = 0;
|
||||
Set(value, dateTime, setChanged);
|
||||
return new();
|
||||
}
|
||||
@@ -287,7 +293,17 @@ public partial class VariableRuntime : Variable
|
||||
try
|
||||
{
|
||||
var data = ReadExpressions.GetExpressionsResult(RawValue, LogMessage);
|
||||
Set(data, dateTime, setChanged);
|
||||
if (!Set(data, dateTime, setChanged))
|
||||
{
|
||||
var oldMessage = _lastErrorMessage;
|
||||
_lastErrorMessage = $"{Name} Outlier filter rejected value";
|
||||
if (oldMessage != _lastErrorMessage)
|
||||
{
|
||||
LogMessage?.LogWarning(_lastErrorMessage);
|
||||
}
|
||||
|
||||
return new($"{Name} Outlier filter rejected value");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -312,11 +328,108 @@ public partial class VariableRuntime : Variable
|
||||
}
|
||||
else
|
||||
{
|
||||
Set(value, dateTime, setChanged);
|
||||
if (!Set(value, dateTime, setChanged))
|
||||
{
|
||||
return new($"{Name} Outlier filter rejected value");
|
||||
}
|
||||
}
|
||||
return new();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 应用异常值过滤,返回true表示值通过过滤,返回false表示值被拒绝
|
||||
/// </summary>
|
||||
private bool ApplyOutlierFilter(object data)
|
||||
{
|
||||
bool anyEnabled = RangeFilterEnable || AmplitudeFilterEnable || ContinuousFilterEnable;
|
||||
if (!anyEnabled || data == null)
|
||||
{
|
||||
_continuousAnomalyCount = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryConvertToDecimal(data, out var numericValue))
|
||||
{
|
||||
_continuousAnomalyCount = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool isAnomaly = false;
|
||||
|
||||
// 合理范围过滤
|
||||
if (RangeFilterEnable)
|
||||
{
|
||||
if (numericValue < RangeFilterMinValue || numericValue > RangeFilterMaxValue)
|
||||
{
|
||||
isAnomaly = true;
|
||||
LogMessage?.LogTrace($"{Name} Outlier filter: value {numericValue} out of range [{RangeFilterMinValue}, {RangeFilterMaxValue}]");
|
||||
}
|
||||
}
|
||||
|
||||
// 变化幅度过滤
|
||||
if (!isAnomaly && AmplitudeFilterEnable && Value != null)
|
||||
{
|
||||
if (TryConvertToDecimal(Value, out var lastNumeric))
|
||||
{
|
||||
var absoluteAmplitude = Math.Abs(numericValue - lastNumeric);
|
||||
if (MaxAmplitude > 0 && absoluteAmplitude > MaxAmplitude)
|
||||
{
|
||||
isAnomaly = true;
|
||||
LogMessage?.LogTrace($"{Name} Outlier filter: amplitude {absoluteAmplitude} exceeds max {MaxAmplitude}");
|
||||
}
|
||||
|
||||
if (!isAnomaly && lastNumeric != 0 && MaxAmplitudePercent > 0)
|
||||
{
|
||||
var amplitudePercent = absoluteAmplitude / Math.Abs(lastNumeric) * 100;
|
||||
if (amplitudePercent > MaxAmplitudePercent)
|
||||
{
|
||||
isAnomaly = true;
|
||||
LogMessage?.LogTrace($"{Name} Outlier filter: amplitude {amplitudePercent:F2}% exceeds max {MaxAmplitudePercent}%");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!isAnomaly)
|
||||
{
|
||||
_continuousAnomalyCount = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// 连续异常过滤
|
||||
if (ContinuousFilterEnable)
|
||||
{
|
||||
_continuousAnomalyCount++;
|
||||
if (_continuousAnomalyCount >= MaxContinuousCount)
|
||||
{
|
||||
// 连续异常次数已达上限,接受该值
|
||||
_continuousAnomalyCount = 0;
|
||||
LogMessage?.LogInformation($"{Name} Outlier filter: continuous anomaly count reached {MaxContinuousCount}, accepting value {data.ToSystemTextJsonString()}");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试将对象转换为decimal
|
||||
/// </summary>
|
||||
private static bool TryConvertToDecimal(object value, out decimal result)
|
||||
{
|
||||
result = 0;
|
||||
if (value == null) return false;
|
||||
try
|
||||
{
|
||||
result = Convert.ToDecimal(value);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 设置变量值与时间/质量戳
|
||||
/// </summary>
|
||||
@@ -335,7 +448,7 @@ public partial class VariableRuntime : Variable
|
||||
}
|
||||
}
|
||||
|
||||
protected void Set(object data, DateTime dateTime, bool setChanged)
|
||||
protected bool Set(object data, DateTime dateTime, bool setChanged)
|
||||
{
|
||||
DateTime time = dateTime != default ? dateTime : DateTime.Now;
|
||||
CollectTime = time;
|
||||
@@ -378,6 +491,14 @@ public partial class VariableRuntime : Variable
|
||||
changed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// 异常值过滤:仅在值变化且在线时检查
|
||||
if ((changed || setChanged) && _isOnline && !ApplyOutlierFilter(data))
|
||||
{
|
||||
GlobalData.VariableCollectChange(this);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (changed || _isOnlineChanged == true || setChanged)
|
||||
{
|
||||
ChangeTime = time;
|
||||
@@ -394,6 +515,7 @@ public partial class VariableRuntime : Variable
|
||||
GlobalData.VariableValueChange(this);
|
||||
}
|
||||
GlobalData.VariableCollectChange(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Init(DeviceRuntime deviceRuntime)
|
||||
|
||||
@@ -165,7 +165,11 @@
|
||||
"Unknown": "Unknown",
|
||||
"UpdateChannel": "Edit Channel",
|
||||
"UpdateDevice": "Edit Device",
|
||||
"VariableInformation": "Variable Information"
|
||||
"VariableInformation": "Variable Information",
|
||||
"OutlierFilterInformation": "Outlier Filter",
|
||||
"RangeFilter": "Range Filter",
|
||||
"AmplitudeFilter": "Amplitude Filter",
|
||||
"ContinuousFilter": "Continuous Anomaly Filter"
|
||||
},
|
||||
"ThingsGateway.Gateway.Razor.BackendLogPage": {
|
||||
"BackendLog": "Gateway Backend Log",
|
||||
|
||||
@@ -164,7 +164,11 @@
|
||||
"Unknown": "未知",
|
||||
"UpdateChannel": "编辑通道",
|
||||
"UpdateDevice": "编辑设备",
|
||||
"VariableInformation": "变量信息"
|
||||
"VariableInformation": "变量信息",
|
||||
"OutlierFilterInformation": "异常值过滤",
|
||||
"RangeFilter": "合理范围过滤",
|
||||
"AmplitudeFilter": "变化幅度过滤",
|
||||
"ContinuousFilter": "连续异常过滤"
|
||||
},
|
||||
"ThingsGateway.Gateway.Razor.BackendLogPage": {
|
||||
"BackendLog": "网关后台日志",
|
||||
|
||||
+38
-1
@@ -1,4 +1,4 @@
|
||||
@namespace ThingsGateway.Gateway.Razor
|
||||
@namespace ThingsGateway.Gateway.Razor
|
||||
@using ThingsGateway.Admin.Application
|
||||
@using ThingsGateway.Admin.Razor
|
||||
@using ThingsGateway.Foundation
|
||||
@@ -191,6 +191,43 @@
|
||||
|
||||
</ValidateForm>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Text=@GatewayLocalizer["OutlierFilterInformation"]>
|
||||
<EditorForm class="p-2" AutoGenerateAllItem="false" RowType=RowType.Inline ItemsPerRow=3 LabelWidth=250 Model="Model">
|
||||
<FieldItems>
|
||||
|
||||
<EditorItem TValue="string" TModel="Variable" @bind-Field="@context.Name">
|
||||
<EditTemplate Context="value">
|
||||
<div class="col-12">
|
||||
</div>
|
||||
</EditTemplate>
|
||||
</EditorItem>
|
||||
<EditorItem @bind-Field="@context.RangeFilterEnable" />
|
||||
<EditorItem @bind-Field="@context.RangeFilterMinValue" />
|
||||
<EditorItem @bind-Field="@context.RangeFilterMaxValue" />
|
||||
|
||||
<EditorItem TValue="string" TModel="Variable" @bind-Field="@context.Name">
|
||||
<EditTemplate Context="value">
|
||||
<div class="col-12">
|
||||
</div>
|
||||
</EditTemplate>
|
||||
</EditorItem>
|
||||
<EditorItem @bind-Field="@context.AmplitudeFilterEnable" />
|
||||
<EditorItem @bind-Field="@context.MaxAmplitude" />
|
||||
<EditorItem @bind-Field="@context.MaxAmplitudePercent" />
|
||||
|
||||
<EditorItem TValue="string" TModel="Variable" @bind-Field="@context.Name">
|
||||
<EditTemplate Context="value">
|
||||
<div class="col-12">
|
||||
</div>
|
||||
</EditTemplate>
|
||||
</EditorItem>
|
||||
<EditorItem @bind-Field="@context.ContinuousFilterEnable" />
|
||||
<EditorItem @bind-Field="@context.MaxContinuousCount" />
|
||||
|
||||
</FieldItems>
|
||||
</EditorForm>
|
||||
</TabItem>
|
||||
@if (!BatchEditEnable)
|
||||
{
|
||||
<TabItem Text=@GatewayLocalizer["PluginInformation"]>
|
||||
|
||||
@@ -571,7 +571,15 @@
|
||||
"SaveVariable": "Add/Modify Variable",
|
||||
"Unit": "Unit",
|
||||
"VariableNotNull": "Variable name does not exist",
|
||||
"WriteExpressions": "WriteExpressions"
|
||||
"WriteExpressions": "WriteExpressions",
|
||||
"RangeFilterEnable": "RangeFilterEnable",
|
||||
"RangeFilterMinValue": "RangeFilterMinValue",
|
||||
"RangeFilterMaxValue": "RangeFilterMaxValue",
|
||||
"AmplitudeFilterEnable": "AmplitudeFilterEnable",
|
||||
"MaxAmplitude": "MaxAmplitude",
|
||||
"MaxAmplitudePercent": "MaxAmplitude%",
|
||||
"ContinuousFilterEnable": "ContinuousFilterEnable",
|
||||
"MaxContinuousCount": "MaxContinuousCount"
|
||||
},
|
||||
|
||||
"ThingsGateway.Gateway.Application.AlarmPropertys": {
|
||||
|
||||
@@ -597,7 +597,15 @@
|
||||
"SaveVariable": "添加/修改变量",
|
||||
"Unit": "单位",
|
||||
"VariableNotNull": "变量名称不存在",
|
||||
"WriteExpressions": "写入表达式"
|
||||
"WriteExpressions": "写入表达式",
|
||||
"RangeFilterEnable": "合理范围过滤使能",
|
||||
"RangeFilterMinValue": "范围最小值",
|
||||
"RangeFilterMaxValue": "范围最大值",
|
||||
"AmplitudeFilterEnable": "变化幅度过滤使能",
|
||||
"MaxAmplitude": "最大变化幅度",
|
||||
"MaxAmplitudePercent": "最大变化幅度%",
|
||||
"ContinuousFilterEnable": "连续异常过滤使能",
|
||||
"MaxContinuousCount": "连续异常最大次数"
|
||||
},
|
||||
|
||||
"ThingsGateway.Gateway.Application.AlarmPropertys": {
|
||||
|
||||
@@ -165,7 +165,11 @@
|
||||
"Unknown": "Unknown",
|
||||
"UpdateChannel": "Edit Channel",
|
||||
"UpdateDevice": "Edit Device",
|
||||
"VariableInformation": "Variable Information"
|
||||
"VariableInformation": "Variable Information",
|
||||
"OutlierFilterInformation": "Outlier Filter",
|
||||
"RangeFilter": "Range Filter",
|
||||
"AmplitudeFilter": "Amplitude Filter",
|
||||
"ContinuousFilter": "Continuous Anomaly Filter"
|
||||
},
|
||||
"ThingsGateway.Gateway.Razor.BackendLogPage": {
|
||||
"BackendLog": "Gateway Backend Log",
|
||||
|
||||
@@ -164,7 +164,11 @@
|
||||
"Unknown": "未知",
|
||||
"UpdateChannel": "编辑通道",
|
||||
"UpdateDevice": "编辑设备",
|
||||
"VariableInformation": "变量信息"
|
||||
"VariableInformation": "变量信息",
|
||||
"OutlierFilterInformation": "异常值过滤",
|
||||
"RangeFilter": "合理范围过滤",
|
||||
"AmplitudeFilter": "变化幅度过滤",
|
||||
"ContinuousFilter": "连续异常过滤"
|
||||
},
|
||||
"ThingsGateway.Gateway.Razor.BackendLogPage": {
|
||||
"BackendLog": "网关后台日志",
|
||||
|
||||
+38
-1
@@ -1,4 +1,4 @@
|
||||
@namespace ThingsGateway.Gateway.Razor
|
||||
@namespace ThingsGateway.Gateway.Razor
|
||||
@using ThingsGateway.Admin.Application
|
||||
@using ThingsGateway.Admin.Razor
|
||||
@using ThingsGateway.Foundation
|
||||
@@ -191,6 +191,43 @@
|
||||
|
||||
</ValidateForm>
|
||||
</TabItem>
|
||||
|
||||
<TabItem Text=@GatewayLocalizer["OutlierFilterInformation"]>
|
||||
<EditorForm class="p-2" AutoGenerateAllItem="false" RowType=RowType.Inline ItemsPerRow=3 LabelWidth=250 Model="Model">
|
||||
<FieldItems>
|
||||
|
||||
<EditorItem TValue="string" TModel="Variable" @bind-Field="@context.Name">
|
||||
<EditTemplate Context="value">
|
||||
<div class="col-12">
|
||||
</div>
|
||||
</EditTemplate>
|
||||
</EditorItem>
|
||||
<EditorItem @bind-Field="@context.RangeFilterEnable" />
|
||||
<EditorItem @bind-Field="@context.RangeFilterMinValue" />
|
||||
<EditorItem @bind-Field="@context.RangeFilterMaxValue" />
|
||||
|
||||
<EditorItem TValue="string" TModel="Variable" @bind-Field="@context.Name">
|
||||
<EditTemplate Context="value">
|
||||
<div class="col-12">
|
||||
</div>
|
||||
</EditTemplate>
|
||||
</EditorItem>
|
||||
<EditorItem @bind-Field="@context.AmplitudeFilterEnable" />
|
||||
<EditorItem @bind-Field="@context.MaxAmplitude" />
|
||||
<EditorItem @bind-Field="@context.MaxAmplitudePercent" />
|
||||
|
||||
<EditorItem TValue="string" TModel="Variable" @bind-Field="@context.Name">
|
||||
<EditTemplate Context="value">
|
||||
<div class="col-12">
|
||||
</div>
|
||||
</EditTemplate>
|
||||
</EditorItem>
|
||||
<EditorItem @bind-Field="@context.ContinuousFilterEnable" />
|
||||
<EditorItem @bind-Field="@context.MaxContinuousCount" />
|
||||
|
||||
</FieldItems>
|
||||
</EditorForm>
|
||||
</TabItem>
|
||||
@if (!BatchEditEnable)
|
||||
{
|
||||
<TabItem Text=@GatewayLocalizer["PluginInformation"]>
|
||||
|
||||
Reference in New Issue
Block a user