一、核心场景与价值
在新能源光伏电站中,需实现「逆变器数据采集-实时监控-历史追溯-告警预警」全流程管理。本方案基于 C# + .NET 8 开发跨平台上位机,核心价值:
标准化对接:通过 OPC UA 工业协议(逆变器主流支持),兼容华为、阳光电源、固德威等主流品牌逆变器;全维度监控:采集逆变器输出电压/电流/功率、光伏组件辐照度、发电量(今日/累计)、设备状态等核心数据;可视化呈现:工业级 UI 展示实时数据、功率趋势曲线、发电量统计报表,支持多逆变器集群监控;高可靠运行:支持 OPC UA 断线重连、本地数据缓存、阈值告警(声光+日志),适配光伏电站 7×24 小时运行;跨平台部署:可运行在 Windows 工控机、Linux 边缘节点,支持单机/集群部署。
本文提供完整落地方案:从 OPC UA 逆变器对接、数据模型设计、UI 可视化到数据存储、告警、报表导出,附可直接复用的源码与配置,快速适配光伏电站监控需求。
二、整体架构设计
2.1 架构分层
光伏设备层 → 数据采集层 → 数据处理层 → 可视化层 → 应用层
| 层级 | 核心组件/技术 | 职责描述 |
|---|---|---|
| 光伏设备层 | 逆变器(OPC UA 服务器模式)、辐照度传感器 | 输出光伏发电核心数据(电压/电流/功率/发电量) |
| 数据采集层 | OPC UA 客户端(.NET 8) | 订阅/读取逆变器 OPC UA 节点数据,断线重连+缓存 |
| 数据处理层 | 数据模型、异常过滤、阈值判断 | 标准化数据格式,处理采集异常,触发告警逻辑 |
| 可视化层 | Avalonia UI + OxyPlot | 工业级监控界面(实时数据表格、趋势曲线、告警列表) |
| 应用层 | InfluxDB(时序存储)、SQLite(配置/告警)、报表导出 | 时序数据存储、历史查询、告警记录、Excel 报表 |
2.2 技术栈选型(工业级稳定组合)
| 模块 | 技术选型 | 核心优势 |
|---|---|---|
| 基础框架 | .NET 8(LTS) | 跨平台兼容,性能提升 30%+,支持工业场景优化 |
| OPC UA 通信 | OPCFoundation.NetStandard.Opc.Ua | 微软官方维护,兼容 OPC UA 1.04 标准,适配所有主流逆变器 |
| 工业 UI | Avalonia 11 | 跨 Windows/Linux,API 接近 WPF,工业界面适配性强 |
| 数据可视化 | OxyPlot.Avalonia | 轻量高效,支持实时趋势曲线、柱状图(发电量统计) |
| 时序数据存储 | InfluxDB 2.7(时序数据库) | 优化时序数据读写,查询速度比 MySQL 快 10 倍+ |
| 配置/告警存储 | SQLite | 轻量化无部署成本,适配嵌入式/工控机场景 |
| 报表导出 | EPPlus 5.0(Excel) | 支持复杂报表生成,兼容 Office 2016+ |
| 日志/告警 | Serilog | 工业级日志记录,支持文件轮转、告警分级 |
三、环境准备
3.1 硬件准备
核心设备:光伏逆变器(支持 OPC UA 服务器模式,如华为 SUN2000-10KTL-M1);辅助设备:辐照度传感器(可选,I2C 接口,用于环境数据补充);运行设备:工控机(Windows 10/11 x64 或 Ubuntu 22.04 x64),建议 4GB 内存+128GB 存储;网络设备:交换机(逆变器与工控机同局域网,确保 OPC UA 端口可通)。
3.2 软件准备
开发环境:Visual Studio 2022(17.10+)、.NET 8 SDK(下载);运行环境:InfluxDB 2.7(安装教程)、Docker(可选,快速部署 InfluxDB);依赖库安装(NuGet):
# OPC UA 核心通信库
Install-Package OPCFoundation.NetStandard.Opc.Ua -Version 1.4.370.103
# 工业 UI 框架
Install-Package Avalonia -Version 11.0.0
# 数据可视化
Install-Package OxyPlot.Avalonia -Version 2.1.0
# 时序数据库客户端
Install-Package InfluxDB.Client -Version 4.10.0
# SQLite 存储
Install-Package Microsoft.Data.Sqlite -Version 8.0.0
# Excel 报表导出
Install-Package EPPlus -Version 5.0.0
# 日志库
Install-Package Serilog.Sinks.Console -Version 5.0.0
Install-Package Serilog.Sinks.File -Version 5.0.0
3.3 逆变器 OPC UA 配置(关键前提)
登录逆变器 Web 管理界面(通过逆变器局域网 IP 访问);启用 OPC UA 服务器功能,记录以下参数:
OPC UA 服务器地址:如 (默认端口 4840);认证方式:匿名/用户名密码(多数逆变器支持匿名访问,工业场景建议启用用户名密码);核心数据节点(需从逆变器手册查询,示例如下):
opc.tcp://192.168.1.200:4840
| 监控项 | OPC UA 节点路径(示例) | 数据类型 | 单位 |
|---|---|---|---|
| 逆变器状态 | |
Int32 | 0=待机,1=运行,2=故障 |
| 输出电压(线电压) | |
Float | V |
| 输出电流 | |
Float | A |
| 输出功率 | |
Float | kW |
| 今日发电量 | |
Float | kWh |
| 累计发电量 | |
Float | MWh |
| 辐照度 | |
Float | W/m² |
四、Step1:项目搭建与核心数据模型
4.1 项目创建(Avalonia MVVM 架构)
安装 Avalonia 模板:;新建「Avalonia MVVM Application」,命名为
dotnet new install Avalonia.Templates,框架选择 .NET 8;项目结构设计(模块化适配光伏监控场景):
PVMonitorSystem
PVMonitorSystem/
├─ Config/ # 系统配置(OPC UA、数据库、告警阈值)
├─ Models/ # 数据模型(逆变器数据、告警、配置)
├─ Services/ # 核心服务(OPC UA 采集、存储、告警、报表)
├─ ViewModels/ # MVVM 视图模型(绑定 UI 数据)
├─ Views/ # UI 视图(主窗口、报表窗口、告警窗口)
└─ Utils/ # 工具类(日志、Excel 导出、数据转换)
4.2 核心数据模型(标准化数据格式)
// Models/InverterData.cs(逆变器实时数据模型)
using System;
namespace PVMonitorSystem.Models
{
/// <summary>
/// 逆变器实时采集数据
/// </summary>
public class InverterData
{
/// <summary>
/// 逆变器编号(唯一标识,如 INV-001)
/// </summary>
public string InverterId { get; set; }
/// <summary>
/// 采集时间戳(UTC+8)
/// </summary>
public DateTime CollectTime { get; set; } = DateTime.Now.ToUniversalTime().AddHours(8);
/// <summary>
/// 逆变器状态(0=待机,1=运行,2=故障)
/// </summary>
public int Status { get; set; }
/// <summary>
/// 输出线电压(V)
/// </summary>
public float OutputVoltage { get; set; }
/// <summary>
/// 输出电流(A)
/// </summary>
public float OutputCurrent { get; set; }
/// <summary>
/// 输出功率(kW)
/// </summary>
public float OutputPower { get; set; }
/// <summary>
/// 今日发电量(kWh)
/// </summary>
public float DailyEnergy { get; set; }
/// <summary>
/// 累计发电量(MWh)
/// </summary>
public float TotalEnergy { get; set; }
/// <summary>
/// 辐照度(W/m²)
/// </summary>
public float Irradiance { get; set; }
/// <summary>
/// 数据状态(0=正常,1=异常)
/// </summary>
public int DataStatus { get; set; } = 0;
}
/// <summary>
/// 告警信息模型
/// </summary>
public class AlarmInfo
{
/// <summary>
/// 告警ID(唯一标识)
/// </summary>
public string AlarmId { get; set; } = Guid.NewGuid().ToString("N");
/// <summary>
/// 关联逆变器ID
/// </summary>
public string InverterId { get; set; }
/// <summary>
/// 告警类型(电压异常/电流异常/功率异常/设备故障)
/// </summary>
public string AlarmType { get; set; }
/// <summary>
/// 告警描述
/// </summary>
public string AlarmDesc { get; set; }
/// <summary>
/// 告警级别(1=提示,2=警告,3=严重)
/// </summary>
public int AlarmLevel { get; set; }
/// <summary>
/// 告警时间
/// </summary>
public DateTime AlarmTime { get; set; } = DateTime.Now;
/// <summary>
/// 处理状态(0=未处理,1=已处理)
/// </summary>
public int HandleStatus { get; set; } = 0;
}
/// <summary>
/// OPC UA 配置模型
/// </summary>
public class OpcUaConfig
{
/// <summary>
/// OPC UA 服务器地址
/// </summary>
public string ServerUrl { get; set; } = "opc.tcp://192.168.1.200:4840";
/// <summary>
/// 用户名(匿名则为空)
/// </summary>
public string Username { get; set; } = "";
/// <summary>
/// 密码(匿名则为空)
/// </summary>
public string Password { get; set; } = "";
/// <summary>
/// 采集间隔(毫秒,默认 3000ms=3秒)
/// </summary>
public int CollectInterval { get; set; } = 3000;
/// <summary>
/// 重连间隔(毫秒,默认 5000ms=5秒)
/// </summary>
public int ReconnectInterval { get; set; } = 5000;
}
/// <summary>
/// 告警阈值配置
/// </summary>
public class AlarmThresholdConfig
{
/// <summary>
/// 输出电压上限(V)
/// </summary>
public float VoltageUpperLimit { get; set; } = 400.0f;
/// <summary>
/// 输出电压下限(V)
/// </summary>
public float VoltageLowerLimit { get; set; } = 200.0f;
/// <summary>
/// 输出功率上限(kW)
/// </summary>
public float PowerUpperLimit { get; set; } = 10.0f;
/// <summary>
/// 设备故障状态码(逆变器返回此状态则告警)
/// </summary>
public int FaultStatusCode { get; set; } = 2;
}
}
五、Step2:OPC UA 数据采集服务(核心对接逆变器)
基于 实现 OPC UA 客户端,负责连接逆变器、订阅实时数据、断线重连、数据缓存。
OPCFoundation.NetStandard.Opc.Ua
5.1 OPC UA 客户端封装(Services/OpcUaCollectorService.cs)
using OPCFoundation.NetStandard.Opc.Ua;
using OPCFoundation.NetStandard.Opc.Ua.Client;
using PVMonitorSystem.Models;
using Serilog;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace PVMonitorSystem.Services
{
public class OpcUaCollectorService : IDisposable
{
private readonly OpcUaConfig _opcConfig;
private readonly AlarmThresholdConfig _alarmThreshold;
private Session _opcSession; // OPC UA 会话
private Subscription _dataSubscription; // 数据订阅
private readonly CancellationTokenSource _cts = new();
private bool _isDisposed = false;
private List<InverterData> _cacheData = new(100); // 断网数据缓存
// 事件:数据采集完成(通知 UI 更新)
public event Action<InverterData> DataCollected;
// 事件:告警触发(通知告警服务)
public event Action<AlarmInfo> AlarmTriggered;
public OpcUaCollectorService(OpcUaConfig opcConfig, AlarmThresholdConfig alarmThreshold)
{
_opcConfig = opcConfig;
_alarmThreshold = alarmThreshold;
}
/// <summary>
/// 启动 OPC UA 采集服务
/// </summary>
public async Task StartAsync(string inverterId)
{
Log.Information("OPC UA 采集服务启动中,服务器地址:{0}", _opcConfig.ServerUrl);
try
{
// 1. 连接 OPC UA 服务器
await ConnectToServerAsync();
// 2. 创建数据订阅(实时推送)
await CreateSubscriptionAsync(inverterId);
// 3. 启动重连监控任务
_ = Task.Run(MonitorConnectionAsync, _cts.Token);
Log.Information("OPC UA 采集服务启动成功,采集间隔:{0}ms", _opcConfig.CollectInterval);
}
catch (Exception ex)
{
Log.Error($"OPC UA 采集服务启动失败:{ex.Message}");
throw;
}
}
/// <summary>
/// 连接 OPC UA 服务器
/// </summary>
private async Task ConnectToServerAsync()
{
// 配置 OPC UA 连接参数
var endpointUri = new Uri(_opcConfig.ServerUrl);
var endpoint = CoreClientUtils.SelectEndpoint(endpointUri.ToString(), useSecurity: false); // 工业场景建议启用安全策略
var config = new ApplicationConfiguration
{
ApplicationName = "PVMonitorSystem",
ApplicationType = ApplicationType.Client,
SecurityConfiguration = new SecurityConfiguration { DisableAllSecurity = true } // 匿名访问,需密码则配置身份认证
};
await config.Validate(ApplicationType.Client);
// 创建会话
var session = await Session.Create(config, new ConfiguredEndpoint(null, endpoint, EndpointConfiguration.Create(config)),
false, "PV Monitor Session", 60000,
new UserIdentity(_opcConfig.Username, _opcConfig.Password), null);
_opcSession = session;
Log.Information("OPC UA 服务器连接成功");
}
/// <summary>
/// 创建数据订阅(订阅逆变器核心节点)
/// </summary>
private async Task CreateSubscriptionAsync(string inverterId)
{
// 订阅参数:更新间隔=采集间隔
var subscriptionParams = new SubscriptionParameters
{
PublishingInterval = _opcConfig.CollectInterval,
KeepAliveCount = 3,
LifetimeCount = 10
};
_dataSubscription = new Subscription(_opcSession.DefaultSubscription)
{
DisplayName = "InverterDataSubscription",
Parameters = subscriptionParams
};
// 定义需要订阅的 OPC UA 节点(与逆变器节点路径对应)
var monitoredItems = new List<MonitoredItem>
{
CreateMonitoredItem("Device/Status", "Status"),
CreateMonitoredItem("Measurements/OutputVoltageLine", "OutputVoltage"),
CreateMonitoredItem("Measurements/OutputCurrent", "OutputCurrent"),
CreateMonitoredItem("Measurements/OutputPower", "OutputPower"),
CreateMonitoredItem("Measurements/DailyEnergy", "DailyEnergy"),
CreateMonitoredItem("Measurements/TotalEnergy", "TotalEnergy"),
CreateMonitoredItem("Measurements/Irradiance", "Irradiance")
};
// 绑定数据变化回调
foreach (var item in monitoredItems)
{
item.Notification += (s, e) =>
{
var monitoredItem = s as MonitoredItem;
if (monitoredItem != null && e.NotificationValue is DataValue dataValue && dataValue.Value != null)
{
// 缓存节点数据(用于组装完整 InverterData)
_nodeDataCache[monitoredItem.DisplayName] = dataValue.Value;
}
};
_dataSubscription.AddItem(item);
}
// 订阅周期回调(组装完整数据并触发事件)
_dataSubscription.PublishingCycle += (s, e) =>
{
try
{
var inverterData = AssembleInverterData(inverterId);
if (inverterData != null)
{
// 阈值判断,触发告警
CheckAlarmThreshold(inverterData);
// 触发数据采集完成事件(UI 更新)
DataCollected?.Invoke(inverterData);
// 缓存数据(断网时使用)
if (_opcSession != null && _opcSession.Connected)
{
if (_cacheData.Count > 0)
{
Log.Information("补发缓存数据 {0} 条", _cacheData.Count);
foreach (var cached in _cacheData) DataCollected?.Invoke(cached);
_cacheData.Clear();
}
}
else
{
_cacheData.Add(inverterData);
if (_cacheData.Count > 100) _cacheData.RemoveAt(0);
}
}
}
catch (Exception ex)
{
Log.Error($"数据组装失败:{ex.Message}");
}
};
// 添加订阅到会话
await _opcSession.AddSubscriptionAsync(_dataSubscription);
await _dataSubscription.ApplyChangesAsync();
}
// 节点数据缓存(临时存储单节点数据,周期内组装)
private readonly Dictionary<string, object> _nodeDataCache = new();
/// <summary>
/// 组装完整逆变器数据
/// </summary>
private InverterData AssembleInverterData(string inverterId)
{
try
{
return new InverterData
{
InverterId = inverterId,
Status = _nodeDataCache.TryGetValue("Status", out var status) ? Convert.ToInt32(status) : 0,
OutputVoltage = _nodeDataCache.TryGetValue("OutputVoltage", out var voltage) ? Convert.ToSingle(voltage) : 0,
OutputCurrent = _nodeDataCache.TryGetValue("OutputCurrent", out var current) ? Convert.ToSingle(current) : 0,
OutputPower = _nodeDataCache.TryGetValue("OutputPower", out var power) ? Convert.ToSingle(power) : 0,
DailyEnergy = _nodeDataCache.TryGetValue("DailyEnergy", out var dailyEnergy) ? Convert.ToSingle(dailyEnergy) : 0,
TotalEnergy = _nodeDataCache.TryGetValue("TotalEnergy", out var totalEnergy) ? Convert.ToSingle(totalEnergy) : 0,
Irradiance = _nodeDataCache.TryGetValue("Irradiance", out var irradiance) ? Convert.ToSingle(irradiance) : 0
};
}
catch (Exception ex)
{
Log.Error($"数据组装失败:{ex.Message}");
return null;
}
}
/// <summary>
/// 检查告警阈值,触发告警
/// </summary>
private void CheckAlarmThreshold(InverterData data)
{
var alarms = new List<AlarmInfo>();
// 电压异常告警
if (data.OutputVoltage > _alarmThreshold.VoltageUpperLimit)
{
alarms.Add(new AlarmInfo
{
InverterId = data.InverterId,
AlarmType = "电压异常",
AlarmDesc = $"输出电压超出上限:{data.OutputVoltage}V(阈值:{_alarmThreshold.VoltageUpperLimit}V)",
AlarmLevel = 3
});
}
else if (data.OutputVoltage < _alarmThreshold.VoltageLowerLimit && data.OutputVoltage > 0)
{
alarms.Add(new AlarmInfo
{
InverterId = data.InverterId,
AlarmType = "电压异常",
AlarmDesc = $"输出电压低于下限:{data.OutputVoltage}V(阈值:{_alarmThreshold.VoltageLowerLimit}V)",
AlarmLevel = 2
});
}
// 功率异常告警
if (data.OutputPower > _alarmThreshold.PowerUpperLimit)
{
alarms.Add(new AlarmInfo
{
InverterId = data.InverterId,
AlarmType = "功率异常",
AlarmDesc = $"输出功率超出上限:{data.OutputPower}kW(阈值:{_alarmThreshold.PowerUpperLimit}kW)",
AlarmLevel = 3
});
}
// 设备故障告警
if (data.Status == _alarmThreshold.FaultStatusCode)
{
alarms.Add(new AlarmInfo
{
InverterId = data.InverterId,
AlarmType = "设备故障",
AlarmDesc = $"逆变器状态异常(故障码:{data.Status})",
AlarmLevel = 3
});
}
// 触发告警事件
foreach (var alarm in alarms)
{
Log.Warning($"【{alarm.AlarmLevel}级告警】{alarm.AlarmDesc}");
AlarmTriggered?.Invoke(alarm);
}
}
/// <summary>
/// 监控连接状态,断线自动重连
/// </summary>
private async Task MonitorConnectionAsync()
{
while (!_cts.Token.IsCancellationRequested)
{
try
{
if (_opcSession == null || !_opcSession.Connected)
{
Log.Warning("OPC UA 连接断开,尝试重连...");
await ConnectToServerAsync();
await CreateSubscriptionAsync(_opcSession.SessionName); // 重连后重建订阅
}
}
catch (Exception ex)
{
Log.Error($"重连失败:{ex.Message}");
}
finally
{
await Task.Delay(_opcConfig.ReconnectInterval, _cts.Token);
}
}
}
/// <summary>
/// 创建监控项(绑定 OPC UA 节点)
/// </summary>
private MonitoredItem CreateMonitoredItem(string nodePath, string displayName)
{
var nodeId = new NodeId(nodePath, 2); // 命名空间索引=2(需与逆变器一致,从手册查询)
var monitoredItem = new MonitoredItem
{
NodeId = nodeId,
DisplayName = displayName,
MonitoringMode = MonitoringMode.Reporting,
QueueSize = 1,
SamplingInterval = _opcConfig.CollectInterval
};
monitoredItem.AttributeId = Attributes.Value;
return monitoredItem;
}
// 释放资源
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_isDisposed) return;
if (disposing)
{
_cts.Cancel();
_dataSubscription?.Dispose();
_opcSession?.CloseAsync();
_opcSession?.Dispose();
}
_isDisposed = true;
}
}
}
5.2 数据存储服务(时序+配置存储)
// Services/DataStorageService.cs
using InfluxDB.Client;
using InfluxDB.Client.Api.Domain;
using InfluxDB.Client.Writes;
using Microsoft.Data.Sqlite;
using PVMonitorSystem.Models;
using Serilog;
using System;
using System.Threading.Tasks;
namespace PVMonitorSystem.Services
{
public class DataStorageService : IDisposable
{
// InfluxDB 配置(时序数据存储)
private readonly string _influxUrl = "http://localhost:8086";
private readonly string _influxToken = "your-influx-token";
private readonly string _influxOrg = "pv-org";
private readonly string _influxBucket = "pv-data";
private readonly InfluxDBClient _influxClient;
// SQLite 配置(告警/配置存储)
private readonly string _sqliteDbPath = "pv_monitor.db";
private bool _isDisposed = false;
public DataStorageService()
{
// 初始化 InfluxDB 客户端
_influxClient = new InfluxDBClient(_influxUrl, _influxToken);
// 初始化 SQLite 表(告警表、配置表)
InitSqliteTables();
}
/// <summary>
/// 存储逆变器实时数据到 InfluxDB(时序数据)
/// </summary>
public async Task StoreInverterDataAsync(InverterData data)
{
try
{
using var writeApi = _influxClient.GetWriteApiAsync();
var point = PointData.Measurement("inverter_data")
.Tag("inverter_id", data.InverterId)
.Field("status", data.Status)
.Field("output_voltage", data.OutputVoltage)
.Field("output_current", data.OutputCurrent)
.Field("output_power", data.OutputPower)
.Field("daily_energy", data.DailyEnergy)
.Field("total_energy", data.TotalEnergy)
.Field("irradiance", data.Irradiance)
.Timestamp(data.CollectTime, WritePrecision.Ms);
await writeApi.WritePointAsync(point, _influxBucket, _influxOrg);
}
catch (Exception ex)
{
Log.Error($"InfluxDB 存储失败:{ex.Message}");
}
}
/// <summary>
/// 存储告警信息到 SQLite
/// </summary>
public void StoreAlarmInfo(AlarmInfo alarm)
{
try
{
using var connection = new SqliteConnection($"Data Source={_sqliteDbPath}");
connection.Open();
var insertCmd = connection.CreateCommand();
insertCmd.CommandText = @"
INSERT INTO AlarmInfo (AlarmId, InverterId, AlarmType, AlarmDesc, AlarmLevel, AlarmTime, HandleStatus)
VALUES (@AlarmId, @InverterId, @AlarmType, @AlarmDesc, @AlarmLevel, @AlarmTime, @HandleStatus);";
insertCmd.Parameters.AddWithValue("@AlarmId", alarm.AlarmId);
insertCmd.Parameters.AddWithValue("@InverterId", alarm.InverterId);
insertCmd.Parameters.AddWithValue("@AlarmType", alarm.AlarmType);
insertCmd.Parameters.AddWithValue("@AlarmDesc", alarm.AlarmDesc);
insertCmd.Parameters.AddWithValue("@AlarmLevel", alarm.AlarmLevel);
insertCmd.Parameters.AddWithValue("@AlarmTime", alarm.AlarmTime.ToString("yyyy-MM-dd HH:mm:ss"));
insertCmd.Parameters.AddWithValue("@HandleStatus", alarm.HandleStatus);
insertCmd.ExecuteNonQuery();
}
catch (Exception ex)
{
Log.Error($"SQLite 告警存储失败:{ex.Message}");
}
}
/// <summary>
/// 初始化 SQLite 表
/// </summary>
private void InitSqliteTables()
{
try
{
using var connection = new SqliteConnection($"Data Source={_sqliteDbPath}");
connection.Open();
// 创建告警表
var createAlarmTableCmd = connection.CreateCommand();
createAlarmTableCmd.CommandText = @"
CREATE TABLE IF NOT EXISTS AlarmInfo (
AlarmId TEXT PRIMARY KEY,
InverterId TEXT NOT NULL,
AlarmType TEXT NOT NULL,
AlarmDesc TEXT NOT NULL,
AlarmLevel INTEGER NOT NULL,
AlarmTime TEXT NOT NULL,
HandleStatus INTEGER NOT NULL
);";
createAlarmTableCmd.ExecuteNonQuery();
// 创建系统配置表
var createConfigTableCmd = connection.CreateCommand();
createConfigTableCmd.CommandText = @"
CREATE TABLE IF NOT EXISTS SystemConfig (
ConfigKey TEXT PRIMARY KEY,
ConfigValue TEXT NOT NULL,
Remark TEXT
);";
createConfigTableCmd.ExecuteNonQuery();
}
catch (Exception ex)
{
Log.Error($"SQLite 表初始化失败:{ex.Message}");
}
}
// 释放资源
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_isDisposed) return;
if (disposing)
{
_influxClient?.Dispose();
}
_isDisposed = true;
}
}
}
六、Step3:工业级 UI 可视化设计(Avalonia)
6.1 主窗口 UI 布局(Views/MainWindow.xaml)
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:oxy="clr-namespace:OxyPlot.Avalonia;assembly=OxyPlot.Avalonia"
xmlns:vm="clr-namespace:PVMonitorSystem.ViewModels"
xmlns:models="clr-namespace:PVMonitorSystem.Models"
x:Class="PVMonitorSystem.Views.MainWindow"
Title="新能源光伏监控系统" Width="1600" Height="900" Background="#F0F2F5">
<!-- 数据上下文绑定 -->
<Design.DataContext>
<vm:MainViewModel />
</Design.DataContext>
<Grid RowSpacing="15" ColumnSpacing="15" Padding="10">
<!-- 顶部:系统总览与状态 -->
<Grid Grid.Row="0" Background="White" Padding="15" CornerRadius="8" RowSpacing="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<!-- 系统标题与状态 -->
<Grid Grid.Row="0" ColumnSpacing="20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Label Text="新能源光伏监控系统" FontSize="24" FontAttributes="Bold" Foreground="#2D3748"/>
<Label Grid.Column="1" Text="系统状态:" FontSize="14" VerticalAlignment="Center"/>
<Label Grid.Column="2" Text="{Binding SystemStatus}" FontSize="14" VerticalAlignment="Center"
Foreground="{Binding SystemStatusColor}"/>
<Button Grid.Column="3" Content="导出报表" Width="100" Background="#4299E1" TextColor="White"
Click="ExportReport_Click"/>
</Grid>
<!-- 核心指标总览(卡片式) -->
<Grid Grid.Row="1" ColumnSpacing="20">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<!-- 总功率 -->
<Border Background="#E8F4F8" Padding="15" CornerRadius="8">
<VerticalStackLayout Spacing="5">
<Label Text="总输出功率" FontSize="14" Foreground="#718096"/>
<Label Text="{Binding TotalPower, StringFormat='F2'} kW" FontSize="28" FontAttributes="Bold" Foreground="#2D3748"/>
</VerticalStackLayout>
</Border>
<!-- 今日发电量 -->
<Border Background="#F0F8FB" Padding="15" CornerRadius="8">
<VerticalStackLayout Spacing="5">
<Label Text="今日发电量" FontSize="14" Foreground="#718096"/>
<Label Text="{Binding TotalDailyEnergy, StringFormat='F2'} kWh" FontSize="28" FontAttributes="Bold" Foreground="#2D3748"/>
</VerticalStackLayout>
</Border>
<!-- 累计发电量 -->
<Border Background="#F5Fafe" Padding="15" CornerRadius="8">
<VerticalStackLayout Spacing="5">
<Label Text="累计发电量" FontSize="14" Foreground="#718096"/>
<Label Text="{Binding TotalEnergy, StringFormat='F2'} MWh" FontSize="28" FontAttributes="Bold" Foreground="#2D3748"/>
</VerticalStackLayout>
</Border>
<!-- 运行时长 -->
<Border Background="#FDF2F8" Padding="15" CornerRadius="8">
<VerticalStackLayout Spacing="5">
<Label Text="系统运行时长" FontSize="14" Foreground="#718096"/>
<Label Text="{Binding RunDuration}" FontSize="28" FontAttributes="Bold" Foreground="#2D3748"/>
</VerticalStackLayout>
</Border>
</Grid>
</Grid>
<!-- 中间:实时数据与趋势图 -->
<Grid Grid.Row="1" ColumnSpacing="15">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="2*"/>
</Grid.ColumnDefinitions>
<!-- 左侧:逆变器实时数据表格 -->
<Border Grid.Column="0" Background="White" Padding="10" CornerRadius="8">
<VerticalStackLayout Spacing="10">
<Label Text="逆变器实时数据" FontSize="16" FontAttributes="Bold" Foreground="#2D3748"/>
<DataGrid Items="{Binding InverterDataList}" Height="500" ColumnSpacing="1" RowSpacing="1">
<DataGrid.Columns>
<DataGridTextColumn Header="逆变器ID" Binding="{Binding InverterId}" Width="Auto"/>
<DataGridTextColumn Header="采集时间" Binding="{Binding CollectTime, StringFormat='HH:mm:ss.fff'}" Width="Auto"/>
<DataGridTextColumn Header="状态" Binding="{Binding Status, Converter={StaticResource StatusConverter}}" Width="Auto"/>
<DataGridTextColumn Header="电压(V)" Binding="{Binding OutputVoltage, StringFormat='F2'}" Width="Auto"/>
<DataGridTextColumn Header="电流(A)" Binding="{Binding OutputCurrent, StringFormat='F2'}" Width="Auto"/>
<DataGridTextColumn Header="功率(kW)" Binding="{Binding OutputPower, StringFormat='F2'}" Width="Auto"/>
<DataGridTextColumn Header="今日发电量(kWh)" Binding="{Binding DailyEnergy, StringFormat='F2'}" Width="Auto"/>
</DataGrid.Columns>
</DataGrid>
</VerticalStackLayout>
</Border>
<!-- 右侧:功率趋势图 -->
<Border Grid.Column="1" Background="White" Padding="10" CornerRadius="8">
<VerticalStackLayout Spacing="10">
<Label Text="输出功率实时趋势" FontSize="16" FontAttributes="Bold" Foreground="#2D3748"/>
<oxy:PlotView Model="{Binding PowerTrendModel}" Height="500"/>
</VerticalStackLayout>
</Border>
</Grid>
<!-- 底部:告警列表 -->
<Border Grid.Row="2" Background="White" Padding="10" CornerRadius="8">
<VerticalStackLayout Spacing="10">
<Label Text="告警信息" FontSize="16" FontAttributes="Bold" Foreground="#2D3748"/>
<DataGrid Items="{Binding AlarmList}" Height="200" ColumnSpacing="1" RowSpacing="1">
<DataGrid.Columns>
<DataGridTextColumn Header="告警ID" Binding="{Binding AlarmId}" Width="Auto"/>
<DataGridTextColumn Header="逆变器ID" Binding="{Binding InverterId}" Width="Auto"/>
<DataGridTextColumn Header="告警类型" Binding="{Binding AlarmType}" Width="Auto"/>
<DataGridTextColumn Header="告警描述" Binding="{Binding AlarmDesc}" Width="*"/>
<DataGridTextColumn Header="告警级别" Binding="{Binding AlarmLevel, Converter={StaticResource AlarmLevelConverter}}" Width="Auto"/>
<DataGridTextColumn Header="告警时间" Binding="{Binding AlarmTime, StringFormat='yyyy-MM-dd HH:mm:ss'}" Width="Auto"/>
<DataGridTextColumn Header="处理状态" Binding="{Binding HandleStatus, Converter={StaticResource HandleStatusConverter}}" Width="Auto"/>
</DataGrid.Columns>
</DataGrid>
</VerticalStackLayout>
</Border>
</Grid>
</Window>
6.2 视图模型(ViewModel)绑定 UI 数据
// ViewModels/MainViewModel.cs
using Avalonia;
using Avalonia.Media;
using OxyPlot;
using OxyPlot.Axes;
using OxyPlot.Series;
using PVMonitorSystem.Models;
using PVMonitorSystem.Services;
using ReactiveUI;
using Serilog;
using System;
using System.Collections.ObjectModel;
using System.Reactive.Linq;
namespace PVMonitorSystem.ViewModels
{
public class MainViewModel : ViewModelBase
{
// 核心服务
private readonly OpcUaCollectorService _opcCollector;
private readonly DataStorageService _dataStorage;
private readonly ReportService _reportService;
// 系统状态
private string _systemStatus = "未运行";
public string SystemStatus
{
get => _systemStatus;
set => this.RaiseAndSetIfChanged(ref _systemStatus, value);
}
private Brush _systemStatusColor = Brushes.Red;
public Brush SystemStatusColor
{
get => _systemStatusColor;
set => this.RaiseAndSetIfChanged(ref _systemStatusColor, value);
}
// 总览指标
private float _totalPower;
public float TotalPower
{
get => _totalPower;
set => this.RaiseAndSetIfChanged(ref _totalPower, value);
}
private float _totalDailyEnergy;
public float TotalDailyEnergy
{
get => _totalDailyEnergy;
set => this.RaiseAndSetIfChanged(ref _totalDailyEnergy, value);
}
private float _totalEnergy;
public float TotalEnergy
{
get => _totalEnergy;
set => this.RaiseAndSetIfChanged(ref _totalEnergy, value);
}
private string _runDuration = "00:00:00";
public string RunDuration
{
get => _runDuration;
set => this.RaiseAndSetIfChanged(ref _runDuration, value);
}
// 数据集合(绑定 UI 表格)
public ObservableCollection<InverterData> InverterDataList { get; } = new();
public ObservableCollection<AlarmInfo> AlarmList { get; } = new();
// 趋势图模型
private PlotModel _powerTrendModel;
public PlotModel PowerTrendModel
{
get => _powerTrendModel;
set => this.RaiseAndSetIfChanged(ref _powerTrendModel, value);
}
// 系统启动时间
private DateTime _startTime;
public MainViewModel()
{
// 初始化配置
var opcConfig = new OpcUaConfig();
var alarmThreshold = new AlarmThresholdConfig();
// 初始化服务
_opcCollector = new OpcUaCollectorService(opcConfig, alarmThreshold);
_dataStorage = new DataStorageService();
_reportService = new ReportService(_dataStorage);
// 绑定 OPC UA 事件
_opcCollector.DataCollected += OnDataCollected;
_opcCollector.AlarmTriggered += OnAlarmTriggered;
// 初始化趋势图
InitPowerTrendModel();
// 启动采集服务
_ = StartCollectorAsync();
// 启动运行时长计时
_startTime = DateTime.Now;
Observable.Interval(TimeSpan.FromSeconds(1))
.Subscribe(_ => UpdateRunDuration());
}
/// <summary>
/// 启动 OPC UA 采集服务
/// </summary>
private async Task StartCollectorAsync()
{
try
{
await _opcCollector.StartAsync("INV-001"); // 逆变器ID
SystemStatus = "运行中";
SystemStatusColor = Brushes.Green;
}
catch (Exception ex)
{
SystemStatus = "启动失败";
SystemStatusColor = Brushes.Red;
Log.Error($"采集服务启动失败:{ex.Message}");
}
}
/// <summary>
/// 数据采集完成回调(更新 UI)
/// </summary>
private void OnDataCollected(InverterData data)
{
// 更新实时数据表格
Application.Current.Dispatcher.Post(() =>
{
InverterDataList.Add(data);
if (InverterDataList.Count > 50) InverterDataList.RemoveAt(0);
// 更新总览指标
TotalPower = data.OutputPower;
TotalDailyEnergy = data.DailyEnergy;
TotalEnergy = data.TotalEnergy;
// 更新趋势图
UpdatePowerTrend(data);
// 存储数据
_ = _dataStorage.StoreInverterDataAsync(data);
});
}
/// <summary>
/// 告警触发回调(更新告警列表)
/// </summary>
private void OnAlarmTriggered(AlarmInfo alarm)
{
Application.Current.Dispatcher.Post(() =>
{
AlarmList.Insert(0, alarm);
if (AlarmList.Count > 100) AlarmList.RemoveAt(AlarmList.Count - 1);
// 存储告警
_dataStorage.StoreAlarmInfo(alarm);
// 播放告警音(可选)
// PlayAlarmSound();
});
}
/// <summary>
/// 初始化功率趋势图
/// </summary>
private void InitPowerTrendModel()
{
_powerTrendModel = new PlotModel
{
Title = "输出功率趋势(kW)",
TitleFontSize = 14,
Background = OxyColors.White
};
// X轴:时间轴
var xAxis = new DateTimeAxis
{
Position = AxisPosition.Bottom,
Title = "时间",
StringFormat = "HH:mm:ss",
IntervalType = DateTimeIntervalType.Seconds,
Interval = 10
};
_powerTrendModel.Axes.Add(xAxis);
// Y轴:功率
var yAxis = new LinearAxis
{
Position = AxisPosition.Left,
Title = "功率(kW)",
Minimum = 0
};
_powerTrendModel.Axes.Add(yAxis);
// 功率曲线
var powerSeries = new LineSeries
{
Title = "实时功率",
Color = OxyColors.Blue,
StrokeThickness = 2,
MarkerSize = 3,
MarkerType = MarkerType.Circle
};
_powerTrendModel.Series.Add(powerSeries);
}
/// <summary>
/// 更新功率趋势图
/// </summary>
private void UpdatePowerTrend(InverterData data)
{
var series = _powerTrendModel.Series[0] as LineSeries;
series.Points.Add(new DataPoint(DateTimeAxis.ToDouble(data.CollectTime), data.OutputPower));
// 只保留最近 100 个数据点
if (series.Points.Count > 100)
{
series.Points.RemoveAt(0);
}
_powerTrendModel.InvalidatePlot(true);
}
/// <summary>
/// 更新系统运行时长
/// </summary>
private void UpdateRunDuration()
{
var duration = DateTime.Now - _startTime;
RunDuration = $"{duration.Hours:D2}:{duration.Minutes:D2}:{duration.Seconds:D2}";
}
}
}
七、Step4:报表导出与告警功能
7.1 报表导出服务(Excel 格式)
// Services/ReportService.cs
using EPPlus;
using InfluxDB.Client;
using InfluxDB.Client.Api.Domain;
using PVMonitorSystem.Models;
using Serilog;
using System;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
namespace PVMonitorSystem.Services
{
public class ReportService
{
private readonly DataStorageService _dataStorage;
private readonly string _influxUrl = "http://localhost:8086";
private readonly string _influxToken = "your-influx-token";
private readonly string _influxOrg = "pv-org";
private readonly string _influxBucket = "pv-data";
public ReportService(DataStorageService dataStorage)
{
_dataStorage = dataStorage;
}
/// <summary>
/// 导出今日发电量报表(Excel)
/// </summary>
public async Task<string> ExportDailyReportAsync(string inverterId)
{
try
{
// 1. 查询今日数据(InfluxDB)
var start = DateTime.Now.Date;
var end = DateTime.Now;
var dataList = await QueryInverterDataAsync(inverterId, start, end);
// 2. 创建 Excel 文档
using var package = new ExcelPackage();
var worksheet = package.Workbook.Worksheets.Add("今日发电量报表");
// 3. 设置表头
worksheet.Cells["A1"].Value = "逆变器ID";
worksheet.Cells["B1"].Value = "采集时间";
worksheet.Cells["C1"].Value = "输出电压(V)";
worksheet.Cells["D1"].Value = "输出电流(A)";
worksheet.Cells["E1"].Value = "输出功率(kW)";
worksheet.Cells["F1"].Value = "今日发电量(kWh)";
worksheet.Cells["G1"].Value = "累计发电量(MWh)";
// 4. 填充数据
for (int i = 0; i < dataList.Count; i++)
{
var data = dataList[i];
worksheet.Cells[$"A{i+2}"].Value = data.InverterId;
worksheet.Cells[$"B{i+2}"].Value = data.CollectTime.ToString("yyyy-MM-dd HH:mm:ss");
worksheet.Cells[$"C{i+2}"].Value = data.OutputVoltage;
worksheet.Cells[$"D{i+2}"].Value = data.OutputCurrent;
worksheet.Cells[$"E{i+2}"].Value = data.OutputPower;
worksheet.Cells[$"F{i+2}"].Value = data.DailyEnergy;
worksheet.Cells[$"G{i+2}"].Value = data.TotalEnergy;
}
// 5. 格式化表格
worksheet.Cells.AutoFitColumns();
worksheet.Cells["A1:G1"].Style.Font.Bold = true;
worksheet.Cells["A1:G1"].Style.Fill.PatternType = ExcelFillStyle.Solid;
worksheet.Cells["A1:G1"].Style.Fill.BackgroundColor.SetColor(System.Drawing.Color.LightBlue);
// 6. 保存文件
var reportPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
$"光伏今日报表_{DateTime.Now:yyyyMMddHHmmss}.xlsx");
using var stream = new FileStream(reportPath, FileMode.Create);
package.SaveAs(stream);
Log.Information($"报表导出成功:{reportPath}");
return reportPath;
}
catch (Exception ex)
{
Log.Error($"报表导出失败:{ex.Message}");
return null;
}
}
/// <summary>
/// 从 InfluxDB 查询逆变器数据
/// </summary>
private async Task<List<InverterData>> QueryInverterDataAsync(string inverterId, DateTime start, DateTime end)
{
using var client = new InfluxDBClient(_influxUrl, _influxToken);
var queryApi = client.GetQueryApi();
var query = $@"
from(bucket: ""{_influxBucket}"")
|> range(start: {start:o}, stop: {end:o})
|> filter(fn: (r) => r._measurement == ""inverter_data"" and r.inverter_id == ""{inverterId}"")
|> pivot(rowKey: [""_time""], columnKey: [""_field""], valueColumn: ""_value"")
|> sort(columns: [""_time""])
";
var tables = await queryApi.QueryAsync(query, _influxOrg);
var dataList = new List<InverterData>();
foreach (var table in tables)
{
foreach (var record in table.Records)
{
dataList.Add(new InverterData
{
InverterId = inverterId,
CollectTime = DateTime.Parse(record.GetValueByKey("_time").ToString()),
OutputVoltage = Convert.ToSingle(record.GetValueByKey("output_voltage") ?? 0),
OutputCurrent = Convert.ToSingle(record.GetValueByKey("output_current") ?? 0),
OutputPower = Convert.ToSingle(record.GetValueByKey("output_power") ?? 0),
DailyEnergy = Convert.ToSingle(record.GetValueByKey("daily_energy") ?? 0),
TotalEnergy = Convert.ToSingle(record.GetValueByKey("total_energy") ?? 0)
});
}
}
return dataList;
}
}
}
7.2 告警声光提示(Utils/AlarmHelper.cs)
using System.Media;
using System.Threading;
namespace PVMonitorSystem.Utils
{
public static class AlarmHelper
{
/// <summary>
/// 播放告警提示音
/// </summary>
public static void PlayAlarmSound()
{
// 简单提示音(可替换为自定义音频文件)
new Thread(() =>
{
for (int i = 0; i < 3; i++)
{
SystemSounds.Beep.Play();
Thread.Sleep(500);
}
}).Start();
}
}
}
八、Step5:部署与测试
8.1 部署流程
InfluxDB 部署:
本地部署:安装 InfluxDB 2.7,创建组织 、桶
pv-org,生成访问 Token;Docker 部署(推荐):
pv-data
docker run -d --name influxdb -p 8086:8086
-v influxdb-data:/var/lib/influxdb2
-e DOCKER_INFLUXDB_INIT_MODE=setup
-e DOCKER_INFLUXDB_INIT_USERNAME=admin
-e DOCKER_INFLUXDB_INIT_PASSWORD=pv123456
-e DOCKER_INFLUXDB_INIT_ORG=pv-org
-e DOCKER_INFLUXDB_INIT_BUCKET=pv-data
influxdb:2.7-alpine
上位机部署:
发布应用(Windows 工控机):
dotnet publish -c Release -r win-x64 --self-contained true -o ./publish/win
复制发布文件到工控机,双击 运行;配置 OPC UA 服务器地址、InfluxDB Token(修改
PVMonitorSystem.exe 中的配置)。
DataStorageService.cs
8.2 测试验证(核心用例)
| 测试项 | 操作步骤 | 预期结果 |
|---|---|---|
| OPC UA 连接测试 | 启动上位机,输入逆变器 OPC UA 地址 | 系统状态显示「运行中」,实时数据表格更新 |
| 实时数据监控 | 逆变器正常发电(或启动模拟器) | 功率趋势图动态滚动,总览指标实时更新 |
| 告警触发测试 | 调整告警阈值(如电压上限设为 300V) | 告警列表显示红色提示,播放告警音 |
| 报表导出测试 | 点击「导出报表」按钮 | 桌面生成 Excel 报表,数据与实时数据一致 |
| 断线重连测试 | 断开逆变器网络,等待 5 秒后恢复 | 上位机自动重连,断网期间数据缓存并补发 |
| 稳定性测试 | 连续运行 24 小时 | 无崩溃、无数据丢包,CPU 占用 < 10% |
九、工业场景优化与扩展
9.1 性能优化
InfluxDB 索引优化:为 、
inverter_id 字段创建索引,提升历史数据查询速度;数据采样优化:根据光伏发电特性,白天(辐照度>0)采集间隔 3 秒,夜间(辐照度=0)采集间隔 60 秒,降低资源占用;UI 渲染优化:趋势图数据点超过 1000 个时自动采样,避免 UI 卡顿。
_measurement
9.2 功能扩展
多逆变器集群监控:支持配置多个逆变器 OPC UA 节点,按逆变器 ID 分组显示数据;云平台对接:集成 MQTT 客户端,将数据上传到阿里云 IoT/华为云 IoT,实现远程监控;AI 故障预测:集成 ML.NET 训练逆变器故障预测模型,基于电压/电流波动识别潜在故障;权限管理:添加用户登录、角色权限控制(如管理员可修改阈值,操作员仅可查看);移动端适配:开发 Avalonia 移动版 UI,支持手机端查看监控数据与告警。
9.3 国产化适配
适配国产工控机(如研华、华北工控)和国产操作系统(麒麟、统信 UOS);替换 InfluxDB 为国产时序数据库(如 TDengine),提升国产化兼容度。
十、总结
本方案基于 C# + .NET 8 + OPC UA 实现新能源光伏监控系统,核心优势在于「标准化对接逆变器、工业级可视化、高可靠运行」,完美适配光伏电站 7×24 小时监控需求。通过 OPC UA 协议兼容主流逆变器品牌,InfluxDB 优化时序数据存储,Avalonia 实现跨平台部署,可快速落地单机/集群光伏电站监控场景。
关键落地要点:
逆变器 OPC UA 节点需与手册严格对应,命名空间索引、节点路径不可出错;时序数据优先选择 InfluxDB,避免使用关系型数据库导致查询性能瓶颈;工业场景必须实现断线重连、数据缓存、异常重试,确保数据不丢失;UI 设计符合工业监控习惯,核心指标突出,告警信息直观可见。
该系统可直接应用于分布式光伏电站、集中式光伏电站的本地监控,通过扩展云对接功能,可实现多站点集中管理,降低光伏电站运维成本。