3532 lines
128 KiB
ObjectPascal
3532 lines
128 KiB
ObjectPascal
unit uMain;
|
|
|
|
interface
|
|
|
|
uses
|
|
Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
|
|
Vcl.Controls, Vcl.Forms, Vcl.Dialogs, UMQTTClient, Vcl.StdCtrls, System.JSON, System.StrUtils,
|
|
Vcl.ExtCtrls, Vcl.Grids, Vcl.ComCtrls, Vcl.Mask, System.Win.ComObj, system.Generics.Collections,
|
|
system.SyncObjs, VCLTee.TeEngine, VCLTee.Series, VCLTee.TeeProcs, VCLTee.Chart,
|
|
System.Math, System.UITypes,
|
|
FireDAC.Comp.Client,
|
|
System.IniFiles,
|
|
uNILMTypes, uNILMManager;
|
|
|
|
const
|
|
CLR_BG = TColor($002A170F); // #0F172A
|
|
CLR_PANEL = TColor($0045342D); // #1E293B (alpha overlay over #0F172A)
|
|
CLR_PANEL2 = TColor($0045342D); // consistent panel color
|
|
CLR_GREEN = TColor($0050AF4C); // #4CAF50
|
|
CLR_RED = TColor($005252FF); // #FF5252
|
|
CLR_WHITE = TColor($00FFFFFF);
|
|
CLR_GRAY = TColor($00B8A394); // #94A3B8
|
|
|
|
ITEMMAX = 20;
|
|
///////////////////////////////////////////// OPC 임시 코드 _ 20260514
|
|
VAL_CTRL_SPACE = 8;
|
|
// VAL_CTRL_TOP = 192;
|
|
VAL_CTRL_TOP = 40;
|
|
VAL_CTRL_LEFT = 16;
|
|
VAL_ITEMNAME_HEIGHT = 24;
|
|
// VAL_ITEMNAME_WIDTH = 88;
|
|
VAL_ITEMNAME_WIDTH = 200;
|
|
VAL_VALUE_HEIGHT = 24;
|
|
VAL_VALUE_WIDTH = 48;
|
|
VAL_TIME_HEIGHT = 24;
|
|
VAL_TIME_WIDTH = 128;
|
|
VAL_QUALITY_HEIGHT = 24;
|
|
VAL_QUALITY_WIDTH = 40;
|
|
VAL_TYPE_HEIGHT = 24;
|
|
VAL_TYPE_WIDTH = 56; // VarType 표시 칸
|
|
///////////////////////////////////////////// OPC 임시 코드 _ 20260514
|
|
|
|
type
|
|
TNILMItem = class
|
|
public
|
|
id : string;
|
|
seq : string;
|
|
PreSeq : string;
|
|
COMM : string;
|
|
pfA : string;
|
|
voltageA : string;
|
|
currentA : string;
|
|
Var_A : string;
|
|
Va_A : string;
|
|
pfB : string;
|
|
voltageB : string;
|
|
currentB : string;
|
|
Var_B : string;
|
|
Va_B : string;
|
|
mqttTopic : string;
|
|
lastupdatetime : string;
|
|
delaycount : string;
|
|
nilmstatus : string;
|
|
statusCheck_off : string;
|
|
statusCheck_wait : string;
|
|
statusCheck_drive : string;
|
|
end;
|
|
|
|
type
|
|
TfMain = class(TForm)
|
|
pcMain: TPageControl;
|
|
tsSetting: TTabSheet;
|
|
tsMonitoring: TTabSheet;
|
|
tsAnalyze: TTabSheet;
|
|
tsLog: TTabSheet;
|
|
Panel2: TPanel;
|
|
pcSetting: TPageControl;
|
|
tsSettingMQTT: TTabSheet;
|
|
tsSettingDB: TTabSheet;
|
|
tsSettingOPC: TTabSheet;
|
|
tsSettingNILM: TTabSheet;
|
|
grdMQTTMonitor: TStringGrid;
|
|
panMainTop: TPanel;
|
|
btnMainStart: TButton;
|
|
btnMainStop: TButton;
|
|
btPublish: TButton;
|
|
lbDateTime: TLabel;
|
|
tmrTime: TTimer;
|
|
cbAutoStart: TCheckBox;
|
|
edtMQTTHost: TLabeledEdit;
|
|
edtMQTTPort: TLabeledEdit;
|
|
edtMQTTId: TLabeledEdit;
|
|
edtMQTTUId: TLabeledEdit;
|
|
edtMQTTUPass: TLabeledEdit;
|
|
btnMqttConnect: TButton;
|
|
btnMqttDisConnect: TButton;
|
|
pnlMqttStatusBox: TPanel;
|
|
lblMQTTConnectionStatus: TLabel;
|
|
edtDBHost: TLabeledEdit;
|
|
edtDBPort: TLabeledEdit;
|
|
edtDBId: TLabeledEdit;
|
|
edtDBPass: TLabeledEdit;
|
|
edtDBName: TLabeledEdit;
|
|
BtnDBConnect: TButton;
|
|
BtnDBDisConnect: TButton;
|
|
pnlDBStatusBox: TPanel;
|
|
lblDBConnectionStatus: TLabel;
|
|
reMainLog: TRichEdit;
|
|
etdMaxRow: TLabeledEdit;
|
|
etdMaxLength: TLabeledEdit;
|
|
cbFullLengthSet: TCheckBox;
|
|
edtOPCHost: TLabeledEdit;
|
|
edtOPCServerName: TLabeledEdit;
|
|
edtOPCUpdateRate: TLabeledEdit;
|
|
btnOPCConnect: TButton;
|
|
btnOPCRead: TButton;
|
|
pnlOPCStatusBox: TPanel;
|
|
lblOPCConnectionStatus: TLabel;
|
|
btnOPCWrite: TButton;
|
|
reMQTTLog: TRichEdit;
|
|
reDBLog: TRichEdit;
|
|
btnOPCDisConnect: TButton;
|
|
Panel1: TPanel;
|
|
sbOPCMainEtc: TScrollBox;
|
|
Label5: TLabel;
|
|
Label7: TLabel;
|
|
Label8: TLabel;
|
|
Label9: TLabel;
|
|
Label10: TLabel;
|
|
reOPCLog: TRichEdit;
|
|
btnSubscribe: TButton;
|
|
tmrdelaystop: TTimer;
|
|
edtDBHostEtc: TLabeledEdit;
|
|
edtDBPortEtc: TLabeledEdit;
|
|
edtDBIdEtc: TLabeledEdit;
|
|
edtDBPassEtc: TLabeledEdit;
|
|
edtDBNameEtc: TLabeledEdit;
|
|
Label1: TLabel;
|
|
Label2: TLabel;
|
|
pnlDBStatusBoxEtc: TPanel;
|
|
lblDBConnectionStatusEtc: TLabel;
|
|
BtnDBConnectEtc: TButton;
|
|
BtnDBDisConnectEtc: TButton;
|
|
pnlNILMTop: TPanel;
|
|
btnNILMRefresh: TButton;
|
|
btnNILMAdd: TButton;
|
|
btnNILMEdit: TButton;
|
|
btnNILMDelete: TButton;
|
|
btnNILMExportJSON: TButton;
|
|
btnNILMImportJSON: TButton;
|
|
chkAutoSaveDB: TCheckBox;
|
|
grdNILMDevice: TStringGrid;
|
|
pcHistory: TPageControl;
|
|
tsHistData: TTabSheet;
|
|
pnlHistFilter: TPanel;
|
|
Label3: TLabel;
|
|
Label4: TLabel;
|
|
Label6: TLabel;
|
|
Label11: TLabel;
|
|
lblHistCount: TLabel;
|
|
cmbHistDevice: TComboBox;
|
|
dtpHistFrom: TDateTimePicker;
|
|
dtpHistTo: TDateTimePicker;
|
|
cmbHistStatus: TComboBox;
|
|
btnHistSearch: TButton;
|
|
btnHistExportCSV: TButton;
|
|
grdHistory: TStringGrid;
|
|
tsEventLog: TTabSheet;
|
|
pnlEvtFilter: TPanel;
|
|
Label13: TLabel;
|
|
Label14: TLabel;
|
|
Label15: TLabel;
|
|
Label16: TLabel;
|
|
lblEvtCount: TLabel;
|
|
cmbEvtDevice: TComboBox;
|
|
dtpEvtFrom: TDateTimePicker;
|
|
dtpEvtTo: TDateTimePicker;
|
|
cmbEvtType: TComboBox;
|
|
btnEvtSearch: TButton;
|
|
grdEventLog: TStringGrid;
|
|
tsTrendView: TTabSheet;
|
|
Panel3: TPanel;
|
|
btnNILMLoad: TButton;
|
|
GroupBox2: TGroupBox;
|
|
cbpf: TCheckBox;
|
|
cbvoltage: TCheckBox;
|
|
cbcurrent: TCheckBox;
|
|
cbVar: TCheckBox;
|
|
cbVa: TCheckBox;
|
|
cbw: TCheckBox;
|
|
Chart_Main: TChart;
|
|
Series1: TLineSeries;
|
|
Series2: TLineSeries;
|
|
Series3: TLineSeries;
|
|
Series4: TLineSeries;
|
|
Series5: TLineSeries;
|
|
Series6: TLineSeries;
|
|
Label17: TLabel;
|
|
Label18: TLabel;
|
|
Label19: TLabel;
|
|
cmbTrendDevice: TComboBox;
|
|
dtpTrendFrom: TDateTimePicker;
|
|
dtpTrendTo: TDateTimePicker;
|
|
lblTrendCount: TLabel;
|
|
edtHistMaxRows: TEdit;
|
|
Label12: TLabel;
|
|
rgCHSelect: TRadioGroup;
|
|
reTrendCheckLog: TRichEdit;
|
|
Splitter1: TSplitter;
|
|
Panel4: TPanel;
|
|
btnOPCWriteONE: TButton;
|
|
Edit1: TEdit;
|
|
Edit2: TEdit;
|
|
cbLogView: TCheckBox;
|
|
cbLogSave: TCheckBox;
|
|
btnNILMInitDB: TButton;
|
|
cbMqttLogVIew: TCheckBox;
|
|
cbDbLogVIew: TCheckBox;
|
|
cbOPCLogVIew: TCheckBox;
|
|
cbDataLogVIew: TCheckBox;
|
|
procedure btPublishClick(Sender: TObject);
|
|
procedure FormDestroy(Sender: TObject);
|
|
procedure tmrTimeTimer(Sender: TObject);
|
|
procedure btnMqttConnectClick(Sender: TObject);
|
|
procedure btnMqttDisConnectClick(Sender: TObject);
|
|
procedure BtnDBConnectClick(Sender: TObject);
|
|
procedure BtnDBDisConnectClick(Sender: TObject);
|
|
procedure btnOPCConnectClick(Sender: TObject);
|
|
procedure btnOPCDisConnectClick(Sender: TObject);
|
|
procedure btnOPCReadClick(Sender: TObject);
|
|
procedure FormCreate(Sender: TObject);
|
|
procedure btnOPCWriteClick(Sender: TObject);
|
|
procedure btnSubscribeClick(Sender: TObject);
|
|
procedure btnMainStartClick(Sender: TObject);
|
|
procedure tmrdelaystopTimer(Sender: TObject);
|
|
procedure btnMainStopClick(Sender: TObject);
|
|
procedure BtnDBConnectEtcClick(Sender: TObject);
|
|
procedure BtnDBDisConnectEtcClick(Sender: TObject);
|
|
procedure btnNILMLoadClick(Sender: TObject);
|
|
procedure btnNILMInitDBClick(Sender: TObject);
|
|
procedure btnNILMRefreshClick(Sender: TObject);
|
|
procedure btnNILMAddClick(Sender: TObject);
|
|
procedure btnNILMEditClick(Sender: TObject);
|
|
procedure btnNILMDeleteClick(Sender: TObject);
|
|
procedure btnNILMExportJSONClick(Sender: TObject);
|
|
procedure btnNILMImportJSONClick(Sender: TObject);
|
|
procedure btnHistSearchClick(Sender: TObject);
|
|
procedure btnHistExportCSVClick(Sender: TObject);
|
|
procedure btnEvtSearchClick(Sender: TObject);
|
|
procedure Chart_MainClickSeries(Sender: TCustomChart; Series: TChartSeries;
|
|
ValueIndex: Integer; Button: TMouseButton; Shift: TShiftState; X,
|
|
Y: Integer);
|
|
procedure btnOPCWriteONEClick(Sender: TObject);
|
|
|
|
private
|
|
txtItemName: array[0..ITEMMAX-1] of TEdit;
|
|
txtValue: array[0..ITEMMAX-1] of TEdit;
|
|
txtTime: array[0..ITEMMAX-1] of TEdit;
|
|
txtQuality: array[0..ITEMMAX-1] of TEdit;
|
|
txtType: array[0..ITEMMAX-1] of TEdit; // VarType 표시 칸
|
|
|
|
OPCServer: OLEVariant;
|
|
OPCGroup: OLEVariant;
|
|
OPCItemObjects: array[1..ITEMMAX] of OLEVariant;
|
|
sH: array[1..ITEMMAX] of Integer;
|
|
FItemVarTypes: array[1..ITEMMAX] of Integer; // Read 시 저장한 각 아이템의 실제 VarType
|
|
|
|
bConnect: Boolean;
|
|
|
|
FMQTTClient: TMQTTClient;
|
|
|
|
FNILMManager : TNILMManager;
|
|
FDeviceList : TList<TNILMDevice>;
|
|
FHistoryQuery : TFDQuery; // 조회 결과 캐시 유지
|
|
FEventLogQuery : TFDQuery;
|
|
|
|
FPreviousOPCValues: TDictionary<string, string>;
|
|
FDynamicOPCItems: TDictionary<string, OLEVariant>;
|
|
FLastOPCRetryTime: TDateTime;
|
|
FLastEnergyUpdate: TDictionary<Integer, TDateTime>;
|
|
FDayEnergyValue: TDictionary<Integer, Double>;
|
|
FLastDailyEnergySyncDate: TDate;
|
|
FLastRecvTime: TDictionary<Integer, TDateTime>;
|
|
FDeviceTimeoutHandled: TDictionary<Integer, Boolean>;
|
|
procedure CheckDeviceTimeouts;
|
|
procedure HandleDeviceTimeout(ADeviceID: Integer; const ADeviceName: string);
|
|
function WriteOPCValueIfChanged(const ATagName: string; const AValue: string): Integer;
|
|
procedure ClearFailedOPCTags;
|
|
procedure LoadSettings;
|
|
procedure SaveSettings;
|
|
procedure NILMErrorHandler(const AMsg: string);
|
|
|
|
procedure OnMQTTMessage(const ATopic, APayload: string);
|
|
procedure OnMQTTStatus(AConnected: Boolean);
|
|
procedure ProcessIncomingMQTTMessage(const ATopic, APayload: string);
|
|
procedure ProcessGatewayMessage(const ATopic, APayload: string);
|
|
procedure ProcessNodeStatusMessage(const ATopic, APayload: string);
|
|
procedure ProcessEventMessage(const ATopic, APayload: string);
|
|
|
|
// NILM 설정 헬퍼
|
|
procedure NILM_InitGrid;
|
|
procedure NILM_LoadGrid;
|
|
procedure NILM_ClearDeviceList;
|
|
function NILM_SelectedDeviceID: Integer;
|
|
// 히스토리 헬퍼
|
|
procedure Hist_InitGrid;
|
|
procedure Hist_LoadDeviceCombo(ACombo: TComboBox; TotalUse:Boolean = True);
|
|
procedure NILM_SaveMQTTData(const ATopic, APayload: string);
|
|
procedure Hist_InitEventGrid;
|
|
procedure Hist_ExportGridToCSV(AGrid: TStringGrid; const ATitle: string);
|
|
|
|
public
|
|
BaseList_NILMItem: TObjectList<TNILMItem>;
|
|
NILMItem: TNILMItem;
|
|
|
|
// Helper Functions
|
|
procedure Log(Target: TRichEdit; LogType: string; Msg: string);
|
|
procedure addSubscribe(topic:string);
|
|
procedure SetupUI;
|
|
function gridmonitorRefresh_main(ADeviceID, ASeq, APreSeq, ACommStatus: Integer;
|
|
const APayloadJSON, AOpStatus: string; AData: TJSONObject; ASensorList: TNILMSensorConfigList): Boolean;
|
|
|
|
procedure LoadLatestMonitorDataFromDB;
|
|
function PG_Connect(x : Integer):Boolean;
|
|
|
|
procedure chartrefresh;
|
|
|
|
// function ConvertOPCValue(const S: string; VarType: Integer): Variant;
|
|
end;
|
|
|
|
var
|
|
fMain: TfMain;
|
|
|
|
implementation
|
|
|
|
{$R *.dfm}
|
|
|
|
uses U_DM, uLogManagerThread, uNILMDeviceForm, Vcl.FileCtrl;
|
|
|
|
procedure TfMain.LoadSettings;
|
|
var
|
|
Ini: TIniFile;
|
|
i: Integer;
|
|
begin
|
|
Ini := TIniFile.Create(ExtractFilePath(ParamStr(0)) + 'settings.ini');
|
|
try
|
|
edtMQTTHost.Text := Ini.ReadString('MQTT', 'Host', edtMQTTHost.Text);
|
|
edtMQTTPort.Text := Ini.ReadString('MQTT', 'Port', edtMQTTPort.Text);
|
|
edtMQTTId.Text := Ini.ReadString('MQTT', 'ID', edtMQTTId.Text);
|
|
edtMQTTUId.Text := Ini.ReadString('MQTT', 'UserID', edtMQTTUId.Text);
|
|
edtMQTTUPass.Text := Ini.ReadString('MQTT', 'Password', edtMQTTUPass.Text);
|
|
|
|
edtDBHost.Text := Ini.ReadString('DB_NILM', 'Host', edtDBHost.Text);
|
|
edtDBPort.Text := Ini.ReadString('DB_NILM', 'Port', edtDBPort.Text);
|
|
edtDBId.Text := Ini.ReadString('DB_NILM', 'ID', edtDBId.Text);
|
|
edtDBPass.Text := Ini.ReadString('DB_NILM', 'Password', edtDBPass.Text);
|
|
edtDBName.Text := Ini.ReadString('DB_NILM', 'Name', edtDBName.Text);
|
|
|
|
edtDBHostEtc.Text := Ini.ReadString('DB_ETC', 'Host', edtDBHostEtc.Text);
|
|
edtDBPortEtc.Text := Ini.ReadString('DB_ETC', 'Port', edtDBPortEtc.Text);
|
|
edtDBIdEtc.Text := Ini.ReadString('DB_ETC', 'ID', edtDBIdEtc.Text);
|
|
edtDBPassEtc.Text := Ini.ReadString('DB_ETC', 'Password', edtDBPassEtc.Text);
|
|
edtDBNameEtc.Text := Ini.ReadString('DB_ETC', 'Name', edtDBNameEtc.Text);
|
|
|
|
edtOPCHost.Text := Ini.ReadString('OPC', 'Host', edtOPCHost.Text);
|
|
edtOPCServerName.Text := Ini.ReadString('OPC', 'ServerName', edtOPCServerName.Text);
|
|
edtOPCUpdateRate.Text := Ini.ReadString('OPC', 'UpdateRate', edtOPCUpdateRate.Text);
|
|
for i := 0 to ITEMMAX - 1 do
|
|
begin
|
|
if Assigned(txtItemName[i]) then
|
|
txtItemName[i].Text := Ini.ReadString('OPC_ITEMS', 'Item' + IntToStr(i), txtItemName[i].Text);
|
|
end;
|
|
|
|
cbLogView.Checked := Ini.ReadBool('SETTINGS', 'LogView', cbLogView.Checked);
|
|
cbLogSave.Checked := Ini.ReadBool('SETTINGS', 'LogSave', cbLogSave.Checked);
|
|
cbAutoStart.Checked := Ini.ReadBool('SETTINGS', 'AutoStart', cbAutoStart.Checked);
|
|
cbFullLengthSet.Checked := Ini.ReadBool('SETTINGS', 'FullLengthSet', cbFullLengthSet.Checked);
|
|
etdMaxRow.Text := Ini.ReadString('SETTINGS', 'MaxRow', etdMaxRow.Text);
|
|
etdMaxLength.Text := Ini.ReadString('SETTINGS', 'MaxLength', etdMaxLength.Text);
|
|
|
|
cbpf.Checked := Ini.ReadBool('DISPLAY', 'pf', cbpf.Checked);
|
|
cbvoltage.Checked := Ini.ReadBool('DISPLAY', 'voltage', cbvoltage.Checked);
|
|
cbcurrent.Checked := Ini.ReadBool('DISPLAY', 'current', cbcurrent.Checked);
|
|
cbVar.Checked := Ini.ReadBool('DISPLAY', 'Var', cbVar.Checked);
|
|
cbVa.Checked := Ini.ReadBool('DISPLAY', 'Va', cbVa.Checked);
|
|
cbw.Checked := Ini.ReadBool('DISPLAY', 'W', cbw.Checked);
|
|
|
|
chkAutoSaveDB.Checked := Ini.ReadBool('NILM', 'AutoSaveDB', chkAutoSaveDB.Checked);
|
|
edtHistMaxRows.Text := Ini.ReadString('NILM', 'HistMaxRows', edtHistMaxRows.Text);
|
|
finally
|
|
Ini.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.SaveSettings;
|
|
var
|
|
Ini: TIniFile;
|
|
i: Integer;
|
|
begin
|
|
Ini := TIniFile.Create(ExtractFilePath(ParamStr(0)) + 'settings.ini');
|
|
try
|
|
Ini.WriteString('MQTT', 'Host', edtMQTTHost.Text);
|
|
Ini.WriteString('MQTT', 'Port', edtMQTTPort.Text);
|
|
Ini.WriteString('MQTT', 'ID', edtMQTTId.Text);
|
|
Ini.WriteString('MQTT', 'UserID', edtMQTTUId.Text);
|
|
Ini.WriteString('MQTT', 'Password', edtMQTTUPass.Text);
|
|
|
|
Ini.WriteString('DB_NILM', 'Host', edtDBHost.Text);
|
|
Ini.WriteString('DB_NILM', 'Port', edtDBPort.Text);
|
|
Ini.WriteString('DB_NILM', 'ID', edtDBId.Text);
|
|
Ini.WriteString('DB_NILM', 'Password', edtDBPass.Text);
|
|
Ini.WriteString('DB_NILM', 'Name', edtDBName.Text);
|
|
|
|
Ini.WriteString('DB_ETC', 'Host', edtDBHostEtc.Text);
|
|
Ini.WriteString('DB_ETC', 'Port', edtDBPortEtc.Text);
|
|
Ini.WriteString('DB_ETC', 'ID', edtDBIdEtc.Text);
|
|
Ini.WriteString('DB_ETC', 'Password', edtDBPassEtc.Text);
|
|
Ini.WriteString('DB_ETC', 'Name', edtDBNameEtc.Text);
|
|
|
|
Ini.WriteString('OPC', 'Host', edtOPCHost.Text);
|
|
Ini.WriteString('OPC', 'ServerName', edtOPCServerName.Text);
|
|
Ini.WriteString('OPC', 'UpdateRate', edtOPCUpdateRate.Text);
|
|
for i := 0 to ITEMMAX - 1 do
|
|
begin
|
|
if Assigned(txtItemName[i]) then
|
|
Ini.WriteString('OPC_ITEMS', 'Item' + IntToStr(i), txtItemName[i].Text);
|
|
end;
|
|
|
|
Ini.WriteBool('SETTINGS', 'LogView', cbLogView.Checked);
|
|
Ini.WriteBool('SETTINGS', 'LogSave', cbLogSave.Checked);
|
|
Ini.WriteBool('SETTINGS', 'AutoStart', cbAutoStart.Checked);
|
|
Ini.WriteBool('SETTINGS', 'FullLengthSet', cbFullLengthSet.Checked);
|
|
Ini.WriteString('SETTINGS', 'MaxRow', etdMaxRow.Text);
|
|
Ini.WriteString('SETTINGS', 'MaxLength', etdMaxLength.Text);
|
|
|
|
Ini.WriteBool('DISPLAY', 'pf', cbpf.Checked);
|
|
Ini.WriteBool('DISPLAY', 'voltage', cbvoltage.Checked);
|
|
Ini.WriteBool('DISPLAY', 'current', cbcurrent.Checked);
|
|
Ini.WriteBool('DISPLAY', 'Var', cbVar.Checked);
|
|
Ini.WriteBool('DISPLAY', 'Va', cbVa.Checked);
|
|
Ini.WriteBool('DISPLAY', 'W', cbw.Checked);
|
|
|
|
Ini.WriteBool('NILM', 'AutoSaveDB', chkAutoSaveDB.Checked);
|
|
Ini.WriteString('NILM', 'HistMaxRows', edtHistMaxRows.Text);
|
|
finally
|
|
Ini.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.SetupUI;
|
|
var
|
|
i: Integer;
|
|
begin
|
|
for i := 0 to ITEMMAX - 1 do
|
|
begin
|
|
txtItemName[i] := TEdit.Create(Self);
|
|
txtItemName[i].Parent := sbOPCMainEtc;
|
|
txtItemName[i].Top := VAL_CTRL_TOP + (VAL_ITEMNAME_HEIGHT + VAL_CTRL_SPACE) * i;
|
|
txtItemName[i].Left := VAL_CTRL_LEFT;
|
|
txtItemName[i].Width := VAL_ITEMNAME_WIDTH;
|
|
txtItemName[i].Height := VAL_ITEMNAME_HEIGHT;
|
|
if i <= 7 then begin
|
|
txtItemName[i].Text := 'SYSTEM.Tag'+ RightStr('000'+IntToStr(i+2),3);
|
|
end
|
|
else begin
|
|
txtItemName[i].Text := '';
|
|
end;
|
|
|
|
txtValue[i] := TEdit.Create(Self);
|
|
txtValue[i].Parent := sbOPCMainEtc;
|
|
txtValue[i].Top := VAL_CTRL_TOP + (VAL_VALUE_HEIGHT + VAL_CTRL_SPACE) * i;
|
|
txtValue[i].Left := VAL_CTRL_LEFT + VAL_ITEMNAME_WIDTH + VAL_CTRL_SPACE;
|
|
txtValue[i].Width := VAL_VALUE_WIDTH;
|
|
txtValue[i].Height := VAL_VALUE_HEIGHT;
|
|
|
|
txtTime[i] := TEdit.Create(Self);
|
|
txtTime[i].Parent := sbOPCMainEtc;
|
|
txtTime[i].Top := VAL_CTRL_TOP + (VAL_TIME_HEIGHT + VAL_CTRL_SPACE) * i;
|
|
txtTime[i].Left := txtValue[i].Left + VAL_VALUE_WIDTH + VAL_CTRL_SPACE;
|
|
txtTime[i].Width := VAL_TIME_WIDTH;
|
|
txtTime[i].Height := VAL_TIME_HEIGHT;
|
|
|
|
txtQuality[i] := TEdit.Create(Self);
|
|
txtQuality[i].Parent := sbOPCMainEtc;
|
|
txtQuality[i].Top := VAL_CTRL_TOP + (VAL_QUALITY_HEIGHT + VAL_CTRL_SPACE) * i;
|
|
txtQuality[i].Left := txtTime[i].Left + VAL_TIME_WIDTH + VAL_CTRL_SPACE;
|
|
txtQuality[i].Width := VAL_QUALITY_WIDTH;
|
|
txtQuality[i].Height := VAL_QUALITY_HEIGHT;
|
|
txtQuality[i].ReadOnly := True;
|
|
|
|
txtType[i] := TEdit.Create(Self);
|
|
txtType[i].Parent := sbOPCMainEtc;
|
|
txtType[i].Top := VAL_CTRL_TOP + (VAL_TYPE_HEIGHT + VAL_CTRL_SPACE) * i;
|
|
txtType[i].Left := txtQuality[i].Left + VAL_QUALITY_WIDTH + VAL_CTRL_SPACE;
|
|
txtType[i].Width := VAL_TYPE_WIDTH;
|
|
txtType[i].Height := VAL_TYPE_HEIGHT;
|
|
txtType[i].ReadOnly := True;
|
|
txtType[i].Color := $00F0F0F0; // 연회색 배경
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.btPublishClick(Sender: TObject);
|
|
var Topic, Payload: string;
|
|
begin
|
|
Topic := 'QST/SHAREDEV/GWTP/AMC/1';
|
|
Payload := '{"DO_' + Format('%.2d', [1]) + '":"' + '1' + '"}';
|
|
if FMQTTClient <> nil then FMQTTClient.Publish(Topic, Payload);
|
|
end;
|
|
|
|
procedure TfMain.btnOPCWriteONEClick(Sender: TObject);
|
|
begin
|
|
|
|
WriteOPCValueIfChanged('SYSTEM.'+edit1.Text,edit2.Text);
|
|
|
|
end;
|
|
|
|
procedure TfMain.addSubscribe(topic:string);
|
|
begin
|
|
if FMQTTClient <> nil then begin
|
|
FMQTTClient.Subscribe(topic);
|
|
Log(reMQTTLog,'process','Subscribe('+topic+')');
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.btnSubscribeClick(Sender: TObject);
|
|
var
|
|
Q: TFDQuery;
|
|
Topic, BaseGWTopic: string;
|
|
SubscribedGWs: TList<string>;
|
|
GWIdx: Integer;
|
|
begin
|
|
grdMQTTMonitor.RowCount := 2;
|
|
grdMQTTMonitor.ColCount := 28;
|
|
grdMQTTMonitor.Cells[0, 0] := 'id';
|
|
grdMQTTMonitor.Cells[1, 0] := 'LastUpdate';
|
|
grdMQTTMonitor.Cells[2, 0] := 'seq';
|
|
grdMQTTMonitor.Cells[3, 0] := 'wire';
|
|
grdMQTTMonitor.Cells[4, 0] := 'Status';
|
|
grdMQTTMonitor.Cells[5, 0] := 'RSSI';
|
|
grdMQTTMonitor.Cells[6, 0] := 'Total_W';
|
|
grdMQTTMonitor.Cells[7, 0] := 'Total_PF';
|
|
grdMQTTMonitor.Cells[8, 0] := 'pf_a';
|
|
grdMQTTMonitor.Cells[9, 0] := 'vrms_a';
|
|
grdMQTTMonitor.Cells[10, 0] := 'irms_a';
|
|
grdMQTTMonitor.Cells[11, 0] := 'w_a';
|
|
grdMQTTMonitor.Cells[12, 0] := 'var_a';
|
|
grdMQTTMonitor.Cells[13, 0] := 'va_a';
|
|
grdMQTTMonitor.Cells[14, 0] := 'pf_b';
|
|
grdMQTTMonitor.Cells[15, 0] := 'vrms_b';
|
|
grdMQTTMonitor.Cells[16, 0] := 'irms_b';
|
|
grdMQTTMonitor.Cells[17, 0] := 'w_b';
|
|
grdMQTTMonitor.Cells[18, 0] := 'var_b';
|
|
grdMQTTMonitor.Cells[19, 0] := 'va_b';
|
|
grdMQTTMonitor.Cells[20, 0] := 'pf_c';
|
|
grdMQTTMonitor.Cells[21, 0] := 'vrms_c';
|
|
grdMQTTMonitor.Cells[22, 0] := 'irms_c';
|
|
grdMQTTMonitor.Cells[23, 0] := 'w_c';
|
|
grdMQTTMonitor.Cells[24, 0] := 'var_c';
|
|
grdMQTTMonitor.Cells[25, 0] := 'va_c';
|
|
grdMQTTMonitor.Cells[26, 0] := '누적전력량(wh)';
|
|
grdMQTTMonitor.Cells[27, 0] := 'Energy_Day(Wh)';
|
|
|
|
if not Assigned(FNILMManager) then FNILMManager := TNILMManager.Create;
|
|
|
|
SubscribedGWs := TList<string>.Create;
|
|
try
|
|
Q := FNILMManager.LoadDevicesSimple;
|
|
try
|
|
while not Q.EOF do
|
|
begin
|
|
Topic := Trim(Q.FieldByName('mqtt_topic').AsString);
|
|
if Topic <> '' then
|
|
begin
|
|
// 측정값 토픽 구독
|
|
addSubscribe(Topic);
|
|
// 노드 상태 토픽 구독 (/{NDID}/status)
|
|
addSubscribe(Topic + '/status');
|
|
// 장비 이벤트 토픽 구독 (/{NDID}/event)
|
|
addSubscribe(Topic + '/event');
|
|
|
|
// 게이트웨이 자체 상태 토픽 구독 (/{GWID}/gateway)
|
|
GWIdx := LastDelimiter('/', Topic);
|
|
if GWIdx > 0 then
|
|
begin
|
|
BaseGWTopic := Copy(Topic, 1, GWIdx - 1) + '/gateway';
|
|
if SubscribedGWs.IndexOf(BaseGWTopic) < 0 then
|
|
begin
|
|
SubscribedGWs.Add(BaseGWTopic);
|
|
addSubscribe(BaseGWTopic);
|
|
end;
|
|
end;
|
|
end;
|
|
Q.Next;
|
|
end;
|
|
finally
|
|
Q.Free;
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
Log(reDBLog, 'error', 'DB Subscribe load error: ' + E.Message);
|
|
end;
|
|
SubscribedGWs.Free;
|
|
|
|
// DB에 저장된 마지막 최신 측정 데이터를 그리드에 즉시 로드
|
|
LoadLatestMonitorDataFromDB;
|
|
end;
|
|
|
|
procedure TfMain.LoadLatestMonitorDataFromDB;
|
|
var
|
|
Q: TFDQuery;
|
|
did, seq: Integer;
|
|
wireVal, statusStr, opStatus: string;
|
|
row_ck: Integer;
|
|
is3P3W: Boolean;
|
|
begin
|
|
if not Assigned(DM) or not DM.fdConnNilm.Connected then Exit;
|
|
|
|
Q := TFDQuery.Create(nil);
|
|
try
|
|
Q.Connection := DM.fdConnNilm;
|
|
Q.SQL.Text :=
|
|
'SELECT device_id, seq, comm_status, received_at, op_status, wire, rssi, wh, ' +
|
|
' total_p, total_pf, ' +
|
|
' power_factor_a, vrms_a, irms_a, active_power_a, reactive_power_a, apparent_power_a, ' +
|
|
' power_factor_b, vrms_b, irms_b, active_power_b, reactive_power_b, apparent_power_b, ' +
|
|
' power_factor_c, vrms_c, irms_c, active_power_c, reactive_power_c, apparent_power_c ' +
|
|
'FROM nilm_data ORDER BY device_id';
|
|
try
|
|
Q.Open;
|
|
except
|
|
Exit;
|
|
end;
|
|
|
|
while not Q.EOF do
|
|
begin
|
|
did := Q.FieldByName('device_id').AsInteger;
|
|
seq := Q.FieldByName('seq').AsInteger;
|
|
wireVal := Q.FieldByName('wire').AsString;
|
|
if wireVal = '' then wireVal := PHASE_3P4W;
|
|
is3P3W := SameText(wireVal, PHASE_3P3W);
|
|
opStatus:= Q.FieldByName('op_status').AsString;
|
|
|
|
var recvTime := Q.FieldByName('received_at').AsDateTime;
|
|
if Assigned(FLastRecvTime) then
|
|
FLastRecvTime.AddOrSetValue(did, recvTime);
|
|
if Assigned(FDeviceTimeoutHandled) then
|
|
FDeviceTimeoutHandled.AddOrSetValue(did, False);
|
|
|
|
// row 찾기
|
|
row_ck := 0;
|
|
for var r := 1 to grdMQTTMonitor.RowCount - 1 do
|
|
begin
|
|
if grdMQTTMonitor.Cells[0, r] = did.ToString then
|
|
begin
|
|
row_ck := r;
|
|
Break;
|
|
end;
|
|
end;
|
|
|
|
// 없으면 신규 row 추가
|
|
if row_ck = 0 then
|
|
begin
|
|
if Trim(grdMQTTMonitor.Cells[0, 1]) = '' then
|
|
row_ck := 1
|
|
else
|
|
begin
|
|
row_ck := grdMQTTMonitor.RowCount;
|
|
grdMQTTMonitor.RowCount := grdMQTTMonitor.RowCount + 1;
|
|
end;
|
|
end;
|
|
|
|
grdMQTTMonitor.Cells[ 0, row_ck] := did.ToString;
|
|
grdMQTTMonitor.Cells[ 1, row_ck] := FormatDateTime('yyyymmdd hhnnss', Q.FieldByName('received_at').AsDateTime);
|
|
grdMQTTMonitor.Cells[ 2, row_ck] := seq.ToString;
|
|
grdMQTTMonitor.Cells[ 3, row_ck] := wireVal;
|
|
|
|
if opStatus = '2' then statusStr := '가동(2)'
|
|
else if opStatus = '1' then statusStr := '미가동(1)'
|
|
else statusStr := '전원꺼짐(0)';
|
|
grdMQTTMonitor.Cells[ 4, row_ck] := statusStr;
|
|
|
|
grdMQTTMonitor.Cells[ 5, row_ck] := Format('%ddBm', [Q.FieldByName('rssi').AsInteger]);
|
|
grdMQTTMonitor.Cells[ 6, row_ck] := FormatFloat('#,##0.0', Q.FieldByName('total_p').AsFloat);
|
|
grdMQTTMonitor.Cells[ 7, row_ck] := FormatFloat('0.000', Q.FieldByName('total_pf').AsFloat);
|
|
|
|
// A상
|
|
grdMQTTMonitor.Cells[ 8, row_ck] := FormatFloat('0.000', Q.FieldByName('power_factor_a').AsFloat);
|
|
grdMQTTMonitor.Cells[ 9, row_ck] := FormatFloat('0.0', Q.FieldByName('vrms_a').AsFloat);
|
|
grdMQTTMonitor.Cells[10, row_ck] := FormatFloat('0.00', Q.FieldByName('irms_a').AsFloat);
|
|
grdMQTTMonitor.Cells[11, row_ck] := FormatFloat('0.0', Q.FieldByName('active_power_a').AsFloat);
|
|
grdMQTTMonitor.Cells[12, row_ck] := FormatFloat('0.0', Q.FieldByName('reactive_power_a').AsFloat);
|
|
grdMQTTMonitor.Cells[13, row_ck] := FormatFloat('0.0', Q.FieldByName('apparent_power_a').AsFloat);
|
|
|
|
// B상
|
|
if is3P3W then
|
|
begin
|
|
grdMQTTMonitor.Cells[14, row_ck] := '-';
|
|
grdMQTTMonitor.Cells[15, row_ck] := '-';
|
|
grdMQTTMonitor.Cells[16, row_ck] := '-';
|
|
grdMQTTMonitor.Cells[17, row_ck] := '-';
|
|
grdMQTTMonitor.Cells[18, row_ck] := '-';
|
|
grdMQTTMonitor.Cells[19, row_ck] := '-';
|
|
end
|
|
else
|
|
begin
|
|
grdMQTTMonitor.Cells[14, row_ck] := FormatFloat('0.000', Q.FieldByName('power_factor_b').AsFloat);
|
|
grdMQTTMonitor.Cells[15, row_ck] := FormatFloat('0.0', Q.FieldByName('vrms_b').AsFloat);
|
|
grdMQTTMonitor.Cells[16, row_ck] := FormatFloat('0.00', Q.FieldByName('irms_b').AsFloat);
|
|
grdMQTTMonitor.Cells[17, row_ck] := FormatFloat('0.0', Q.FieldByName('active_power_b').AsFloat);
|
|
grdMQTTMonitor.Cells[18, row_ck] := FormatFloat('0.0', Q.FieldByName('reactive_power_b').AsFloat);
|
|
grdMQTTMonitor.Cells[19, row_ck] := FormatFloat('0.0', Q.FieldByName('apparent_power_b').AsFloat);
|
|
end;
|
|
|
|
// C상
|
|
grdMQTTMonitor.Cells[20, row_ck] := FormatFloat('0.000', Q.FieldByName('power_factor_c').AsFloat);
|
|
grdMQTTMonitor.Cells[21, row_ck] := FormatFloat('0.0', Q.FieldByName('vrms_c').AsFloat);
|
|
grdMQTTMonitor.Cells[22, row_ck] := FormatFloat('0.00', Q.FieldByName('irms_c').AsFloat);
|
|
grdMQTTMonitor.Cells[23, row_ck] := FormatFloat('0.0', Q.FieldByName('active_power_c').AsFloat);
|
|
grdMQTTMonitor.Cells[24, row_ck] := FormatFloat('0.0', Q.FieldByName('reactive_power_c').AsFloat);
|
|
grdMQTTMonitor.Cells[25, row_ck] := FormatFloat('0.0', Q.FieldByName('apparent_power_c').AsFloat);
|
|
|
|
// Wh
|
|
grdMQTTMonitor.Cells[26, row_ck] := FormatFloat('#,##0', Q.FieldByName('wh').AsLargeInt);
|
|
grdMQTTMonitor.Cells[27, row_ck] := '-';
|
|
|
|
Q.Next;
|
|
end;
|
|
finally
|
|
Q.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.BtnDBConnectClick(Sender: TObject);
|
|
begin
|
|
if not PG_Connect(0) then Exit;
|
|
end;
|
|
|
|
procedure TfMain.BtnDBConnectEtcClick(Sender: TObject);
|
|
begin
|
|
if not PG_Connect(1) then Exit;
|
|
end;
|
|
|
|
function TfMain.PG_Connect(x : Integer):Boolean;
|
|
begin
|
|
Result := False;
|
|
DM.FDPhysPgDriverLink.VendorLib := '.\libpq-10.dll';
|
|
|
|
case x of
|
|
0 : begin
|
|
DM.fdConnNilm.Connected := False;
|
|
|
|
DM.fdConnNilm.Params.Clear;
|
|
DM.fdConnNilm.Params.DriverID := 'PG';
|
|
DM.fdConnNilm.Params.Database := Trim(edtDBName.Text);
|
|
DM.fdConnNilm.Params.UserName := Trim(edtDBId.Text);
|
|
DM.fdConnNilm.Params.Password := Trim(edtDBPass.Text);
|
|
DM.fdConnNilm.Params.Add('Server=' + Trim(edtDBHost.Text));
|
|
DM.fdConnNilm.Params.Add('Port=' + Trim(edtDBPort.Text));
|
|
DM.fdConnNilm.Params.Add('CharacterSet=UTF8');
|
|
|
|
try
|
|
DM.fdConnNilm.Connected := True;
|
|
Result := True;
|
|
Log(reDBLog,'process','DB (NILM) Connected');
|
|
|
|
lblDBConnectionStatus.Caption := 'Connected';
|
|
lblDBConnectionStatus.Font.Color := CLR_GREEN;
|
|
lblDBConnectionStatus.Tag := 1;
|
|
|
|
btnNILMInitDB.Enabled := True;
|
|
LoadLatestMonitorDataFromDB;
|
|
except
|
|
on e: Exception do begin
|
|
Log(reDBLog,'error','DB (NILM) Connect fail :'+e.Message);
|
|
Result := False;
|
|
end;
|
|
end;
|
|
end;
|
|
1 : begin
|
|
DM.fdConnEtc.Connected := False;
|
|
|
|
DM.fdConnEtc.Params.Clear;
|
|
DM.fdConnEtc.Params.DriverID := 'PG';
|
|
DM.fdConnEtc.Params.Database := Trim(edtDBName.Text);
|
|
DM.fdConnEtc.Params.UserName := Trim(edtDBId.Text);
|
|
DM.fdConnEtc.Params.Password := Trim(edtDBPass.Text);
|
|
DM.fdConnEtc.Params.Add('Server=' + Trim(edtDBHost.Text));
|
|
DM.fdConnEtc.Params.Add('Port=' + Trim(edtDBPort.Text));
|
|
DM.fdConnEtc.Params.Add('CharacterSet=UTF8');
|
|
|
|
try
|
|
DM.fdConnEtc.Connected := True;
|
|
Result := True;
|
|
Log(reDBLog,'process','DB (ETC) Connected');
|
|
|
|
lblDBConnectionStatusEtc.Caption := 'Connected';
|
|
lblDBConnectionStatusEtc.Font.Color := CLR_GREEN;
|
|
lblDBConnectionStatusEtc.Tag := 1;
|
|
except
|
|
on e: Exception do begin
|
|
Log(reDBLog,'error','DB (ETC) Connect fail :'+e.Message);
|
|
Result := False;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.BtnDBDisConnectClick(Sender: TObject);
|
|
begin
|
|
if DM.fdConnNilm.Connected then begin
|
|
DM.fdConnNilm.Connected := False;
|
|
Log(reDBLog,'process','DB (NILM) DisConnected');
|
|
|
|
lblDBConnectionStatus.Caption := 'DisConnected';
|
|
lblDBConnectionStatus.Font.Color := CLR_RED;
|
|
lblDBConnectionStatus.Tag := 2;
|
|
end;
|
|
if not btnMainStart.Enabled then
|
|
btnMainStart.Enabled := true;
|
|
end;
|
|
|
|
procedure TfMain.BtnDBDisConnectEtcClick(Sender: TObject);
|
|
begin
|
|
if DM.fdConnEtc.Connected then begin
|
|
DM.fdConnEtc.Connected := False;
|
|
Log(reDBLog,'process','DB (ETC) DisConnected');
|
|
|
|
lblDBConnectionStatusEtc.Caption := 'DisConnected';
|
|
lblDBConnectionStatusEtc.Font.Color := CLR_RED;
|
|
lblDBConnectionStatusEtc.Tag := 2;
|
|
end;
|
|
if not btnMainStart.Enabled then
|
|
btnMainStart.Enabled := true;
|
|
end;
|
|
|
|
procedure TfMain.btnEvtSearchClick(Sender: TObject);
|
|
var
|
|
DevID : Integer;
|
|
EvtType: string;
|
|
MaxR : Integer;
|
|
Row : Integer;
|
|
Q : TFDQuery;
|
|
begin
|
|
if not Assigned(FNILMManager) then FNILMManager := TNILMManager.Create;
|
|
|
|
DevID := -1;
|
|
if Assigned(cmbEvtDevice) and (cmbEvtDevice.ItemIndex > 0) then
|
|
begin
|
|
var S := cmbEvtDevice.Items[cmbEvtDevice.ItemIndex];
|
|
var P1 := Pos('[', S);
|
|
var P2 := Pos(']', S);
|
|
if (P1 > 0) and (P2 > P1) then
|
|
DevID := StrToIntDef(Copy(S, P1 + 1, P2 - P1 - 1), -1);
|
|
end;
|
|
|
|
EvtType := '';
|
|
if Assigned(cmbEvtType) and (cmbEvtType.ItemIndex > 0) then
|
|
EvtType := cmbEvtType.Items[cmbEvtType.ItemIndex];
|
|
|
|
MaxR := 500;
|
|
|
|
FreeAndNil(FEventLogQuery);
|
|
try
|
|
Hist_InitEventGrid;
|
|
Q := FNILMManager.QueryEventLog(
|
|
DevID, dtpEvtFrom.DateTime, dtpEvtTo.DateTime + 1, EvtType, MaxR);
|
|
FEventLogQuery := Q;
|
|
|
|
grdEventLog.RowCount := Max(2, Q.RecordCount + 1);
|
|
Row := 1;
|
|
while not Q.EOF do
|
|
begin
|
|
grdEventLog.Cells[0, Row] := FormatDateTime('yyyy-mm-dd hh:nn:ss', Q.FieldByName('event_time').AsDateTime);
|
|
grdEventLog.Cells[1, Row] := Q.FieldByName('event_type').AsString;
|
|
grdEventLog.Cells[2, Row] := Q.FieldByName('device_id').AsString;
|
|
grdEventLog.Cells[3, Row] := Q.FieldByName('message').AsString;
|
|
grdEventLog.Cells[4, Row] := Q.FieldByName('detail').AsString;
|
|
Inc(Row);
|
|
Q.Next;
|
|
end;
|
|
if Assigned(lblEvtCount) then
|
|
lblEvtCount.Caption := Format('조회 결과: %d 건', [Q.RecordCount]);
|
|
except
|
|
on E: Exception do
|
|
Log(reDBLog, 'error', '[이벤트로그] 조회 오류: ' + E.Message);
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.btnHistExportCSVClick(Sender: TObject);
|
|
begin
|
|
if Assigned(grdHistory) then
|
|
Hist_ExportGridToCSV(grdHistory, 'NILM_History');
|
|
end;
|
|
|
|
procedure TfMain.btnHistSearchClick(Sender: TObject);
|
|
var
|
|
DevID : Integer;
|
|
CommFlt: Integer;
|
|
MaxR : Integer;
|
|
Row : Integer;
|
|
wireVal: string;
|
|
Q : TFDQuery;
|
|
begin
|
|
if not Assigned(FNILMManager) then FNILMManager := TNILMManager.Create;
|
|
|
|
// 장비 ID
|
|
DevID := -1;
|
|
if Assigned(cmbHistDevice) and (cmbHistDevice.ItemIndex > 0) then
|
|
begin
|
|
var S := cmbHistDevice.Items[cmbHistDevice.ItemIndex];
|
|
var P1 := Pos('[', S);
|
|
var P2 := Pos(']', S);
|
|
if (P1 > 0) and (P2 > P1) then
|
|
DevID := StrToIntDef(Copy(S, P1 + 1, P2 - P1 - 1), -1);
|
|
end;
|
|
|
|
// COMM 상태 필터
|
|
CommFlt := -1;
|
|
if Assigned(cmbHistStatus) and (cmbHistStatus.ItemIndex > 0) then
|
|
CommFlt := cmbHistStatus.ItemIndex - 1;
|
|
|
|
MaxR := StrToIntDef(edtHistMaxRows.Text, 1000);
|
|
|
|
FreeAndNil(FHistoryQuery);
|
|
try
|
|
Hist_InitGrid;
|
|
Q := FNILMManager.QueryNILMHistory(
|
|
DevID, dtpHistFrom.DateTime, dtpHistTo.DateTime + 1, CommFlt, MaxR);
|
|
FHistoryQuery := Q;
|
|
|
|
grdHistory.RowCount := Max(2, Q.RecordCount + 1);
|
|
Row := 1;
|
|
while not Q.EOF do
|
|
begin
|
|
grdHistory.Cells[0, Row] := FormatDateTime('yyyy-mm-dd hh:nn:ss', Q.FieldByName('received_at').AsDateTime);
|
|
grdHistory.Cells[1, Row] := Q.FieldByName('device_id').AsString;
|
|
grdHistory.Cells[2, Row] := Q.FieldByName('seq').AsString;
|
|
grdHistory.Cells[3, Row] := Q.FieldByName('pre_seq').AsString;
|
|
grdHistory.Cells[4, Row] := Q.FieldByName('comm_status').AsString;
|
|
|
|
wireVal := Q.FieldByName('wire').AsString;
|
|
if wireVal = '' then wireVal := PHASE_3P4W;
|
|
grdHistory.Cells[5, Row] := wireVal;
|
|
|
|
grdHistory.Cells[6, Row] := Q.FieldByName('rssi').AsString;
|
|
grdHistory.Cells[7, Row] := FormatFloat('#,##0.0', Q.FieldByName('total_p').AsFloat);
|
|
grdHistory.Cells[8, Row] := FormatFloat('0.000', Q.FieldByName('total_pf').AsFloat);
|
|
|
|
// A상
|
|
grdHistory.Cells[9, Row] := FormatFloat('0.000', Q.FieldByName('power_factor_a').AsFloat);
|
|
grdHistory.Cells[10, Row] := FormatFloat('0.0', Q.FieldByName('vrms_a').AsFloat);
|
|
grdHistory.Cells[11, Row] := FormatFloat('0.00', Q.FieldByName('irms_a').AsFloat);
|
|
grdHistory.Cells[12, Row] := FormatFloat('0.0', Q.FieldByName('active_power_a').AsFloat);
|
|
grdHistory.Cells[13, Row] := FormatFloat('0.0', Q.FieldByName('reactive_power_a').AsFloat);
|
|
grdHistory.Cells[14, Row] := FormatFloat('0.0', Q.FieldByName('apparent_power_a').AsFloat);
|
|
|
|
// B상 (3P3W일 경우 미사용 표시)
|
|
if SameText(wireVal, PHASE_3P3W) then
|
|
begin
|
|
grdHistory.Cells[15, Row] := '-';
|
|
grdHistory.Cells[16, Row] := '-';
|
|
grdHistory.Cells[17, Row] := '-';
|
|
grdHistory.Cells[18, Row] := '-';
|
|
grdHistory.Cells[19, Row] := '-';
|
|
grdHistory.Cells[20, Row] := '-';
|
|
end
|
|
else
|
|
begin
|
|
grdHistory.Cells[15, Row] := FormatFloat('0.000', Q.FieldByName('power_factor_b').AsFloat);
|
|
grdHistory.Cells[16, Row] := FormatFloat('0.0', Q.FieldByName('vrms_b').AsFloat);
|
|
grdHistory.Cells[17, Row] := FormatFloat('0.00', Q.FieldByName('irms_b').AsFloat);
|
|
grdHistory.Cells[18, Row] := FormatFloat('0.0', Q.FieldByName('active_power_b').AsFloat);
|
|
grdHistory.Cells[19, Row] := FormatFloat('0.0', Q.FieldByName('reactive_power_b').AsFloat);
|
|
grdHistory.Cells[20, Row] := FormatFloat('0.0', Q.FieldByName('apparent_power_b').AsFloat);
|
|
end;
|
|
|
|
// C상
|
|
grdHistory.Cells[21, Row] := FormatFloat('0.000', Q.FieldByName('power_factor_c').AsFloat);
|
|
grdHistory.Cells[22, Row] := FormatFloat('0.0', Q.FieldByName('vrms_c').AsFloat);
|
|
grdHistory.Cells[23, Row] := FormatFloat('0.00', Q.FieldByName('irms_c').AsFloat);
|
|
grdHistory.Cells[24, Row] := FormatFloat('0.0', Q.FieldByName('active_power_c').AsFloat);
|
|
grdHistory.Cells[25, Row] := FormatFloat('0.0', Q.FieldByName('reactive_power_c').AsFloat);
|
|
grdHistory.Cells[26, Row] := FormatFloat('0.0', Q.FieldByName('apparent_power_c').AsFloat);
|
|
|
|
// 누적전력량 및 기타 센서값
|
|
grdHistory.Cells[27, Row] := FormatFloat('#,##0', Q.FieldByName('wh').AsLargeInt);
|
|
grdHistory.Cells[28, Row] := FormatFloat('0.0', Q.FieldByName('temperature').AsFloat);
|
|
grdHistory.Cells[29, Row] := FormatFloat('0.0', Q.FieldByName('temp_ext').AsFloat);
|
|
grdHistory.Cells[30, Row] := FormatFloat('0.0', Q.FieldByName('vibration').AsFloat);
|
|
grdHistory.Cells[31, Row] := FormatFloat('0.0', Q.FieldByName('tilt_angle').AsFloat);
|
|
|
|
Inc(Row);
|
|
Q.Next;
|
|
end;
|
|
|
|
if Assigned(lblHistCount) then
|
|
lblHistCount.Caption := Format('조회 결과: %d 건', [Q.RecordCount]);
|
|
Log(reDBLog, 'complete', Format('[히스토리] %d 건 조회 완료', [Q.RecordCount]));
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
Log(reDBLog, 'error', '[히스토리] 조회 오류: ' + E.Message);
|
|
if Assigned(lblHistCount) then
|
|
lblHistCount.Caption := '조회 실패';
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.btnMainStartClick(Sender: TObject);
|
|
begin
|
|
btnMqttConnectClick(Sender);
|
|
BtnDBConnectClick(Sender);
|
|
BtnDBConnectEtcClick(Sender);
|
|
btnOPCConnectClick(Sender);
|
|
btnNILMRefreshClick(Sender);
|
|
|
|
btnMainStart.Enabled := False;
|
|
end;
|
|
|
|
procedure TfMain.btnMainStopClick(Sender: TObject);
|
|
begin
|
|
Log(reOPCLog, 'process', '--- STOP CLICKED ---');
|
|
|
|
Log(reOPCLog, 'process', '1. MQTT Disconnect Start');
|
|
btnMqttDisConnectClick(Sender);
|
|
Log(reOPCLog, 'process', '1. MQTT Disconnect End');
|
|
|
|
Log(reOPCLog, 'process', '2. DB Disconnect Start');
|
|
BtnDBDisConnectClick(Sender);
|
|
Log(reOPCLog, 'process', '2. DB Disconnect End');
|
|
|
|
Log(reOPCLog, 'process', '3. DB ETC Disconnect Start');
|
|
BtnDBDisConnectEtcClick(Sender);
|
|
Log(reOPCLog, 'process', '3. DB ETC Disconnect End');
|
|
|
|
Log(reOPCLog, 'process', '4. OPC Disconnect Start');
|
|
btnOPCDisConnectClick(Sender);
|
|
Log(reOPCLog, 'process', '4. OPC Disconnect End');
|
|
|
|
btnMainStart.Enabled := true;
|
|
Log(reOPCLog, 'process', '--- STOP FINISHED ---');
|
|
end;
|
|
|
|
procedure TfMain.btnMqttConnectClick(Sender: TObject);
|
|
begin
|
|
// Self.Enabled := False;
|
|
if FMQTTClient <> nil then begin
|
|
FMQTTClient.OnMessage := nil;
|
|
FMQTTClient.OnStatus := nil;
|
|
FMQTTClient.Disconnect;
|
|
FreeAndNil(FMQTTClient);
|
|
end;
|
|
Log(reMQTTLog,'process','MqttConnectClick('+edtMQTTHost.Text +' - '+ edtMQTTPort.Text +' - '+ edtMQTTId.Text+')');
|
|
FMQTTClient := TMQTTClient.Create(edtMQTTHost.Text, StrToInt(edtMQTTPort.Text), edtMQTTId.Text, edtMQTTUId.Text, edtMQTTUPass.Text);
|
|
FMQTTClient.OnMessage := OnMQTTMessage;
|
|
FMQTTClient.OnStatus := OnMQTTStatus;
|
|
FMQTTClient.Connect;
|
|
|
|
// tmrdelaystop.Enabled := True;
|
|
end;
|
|
|
|
procedure TfMain.btnMqttDisConnectClick(Sender: TObject);
|
|
begin
|
|
Log(reMQTTLog,'process','MqttDisConnectClick');
|
|
if FMQTTClient <> nil then begin
|
|
FMQTTClient.OnMessage := nil;
|
|
FMQTTClient.OnStatus := nil;
|
|
FMQTTClient.Disconnect;
|
|
FreeAndNil(FMQTTClient);
|
|
|
|
Log(reMQTTLog,'process','MQTT DisConnected.');
|
|
lblMQTTConnectionStatus.Caption := 'DisConnected';
|
|
lblMQTTConnectionStatus.Font.Color := CLR_RED;
|
|
lblMQTTConnectionStatus.Tag := 2;
|
|
end;
|
|
|
|
if not btnMainStart.Enabled then
|
|
btnMainStart.Enabled := true;
|
|
end;
|
|
|
|
// ─── 장비 추가 ────────────────────────────────────────────────────────────
|
|
procedure TfMain.btnNILMAddClick(Sender: TObject);
|
|
var
|
|
Result : TNILMDevice;
|
|
Dlg : TfNILMDevice;
|
|
begin
|
|
if not Assigned(FNILMManager) then FNILMManager := TNILMManager.Create;
|
|
|
|
Dlg := TfNILMDevice.Create(Self);
|
|
try
|
|
Dlg.InitForAdd;
|
|
if Dlg.Execute(Result) then
|
|
begin
|
|
try
|
|
if FNILMManager.SaveDevice(Result) then begin
|
|
Log(reDBLog, 'complete', Format('[NILM] 장비 %d (%s) 추가 완료', [Result.DeviceID, Result.DeviceName]));
|
|
NILM_LoadGrid;
|
|
end
|
|
else
|
|
Log(reDBLog, 'error', Format('[NILM] 장비 %d (%s) 추가 실패', [Result.DeviceID, Result.DeviceName]));
|
|
except
|
|
on E: Exception do
|
|
Log(reDBLog, 'error', '[NILM] 장비 추가 오류: ' + E.Message);
|
|
end;
|
|
if Assigned(Result.SensorList) then FreeAndNil(Result.SensorList);
|
|
end;
|
|
finally
|
|
Dlg.Free;
|
|
end;
|
|
end;
|
|
|
|
// ─── 장비 삭제 ────────────────────────────────────────────────────────────
|
|
procedure TfMain.btnNILMDeleteClick(Sender: TObject);
|
|
var
|
|
DevID: Integer;
|
|
Row : Integer;
|
|
begin
|
|
DevID := NILM_SelectedDeviceID;
|
|
if DevID < 0 then
|
|
begin
|
|
MessageDlg('삭제할 장비를 선택해주세요.', mtInformation, [mbOK], 0);
|
|
Exit;
|
|
end;
|
|
|
|
Row := grdNILMDevice.Row;
|
|
if MessageDlg(Format('장비 번호 %d [%s] 를 삭제하시겠습니까?' + sLineBreak +
|
|
'연관된 센서 설정도 함께 삭제됩니다.',
|
|
[DevID, grdNILMDevice.Cells[1, Row]]),
|
|
mtConfirmation, [mbYes, mbNo], 0) = mrYes then
|
|
begin
|
|
if not Assigned(FNILMManager) then FNILMManager := TNILMManager.Create;
|
|
try
|
|
FNILMManager.DeleteDevice(DevID);
|
|
Log(reDBLog, 'complete', Format('[NILM] 장비 %d 삭제 완료', [DevID]));
|
|
NILM_LoadGrid;
|
|
except
|
|
on E: Exception do
|
|
Log(reDBLog, 'error', '[NILM] 장비 삭제 오류: ' + E.Message);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
// ─── 장비 수정 ────────────────────────────────────────────────────────────
|
|
procedure TfMain.btnNILMEditClick(Sender: TObject);
|
|
var
|
|
DevID : Integer;
|
|
DevRec : TNILMDevice;
|
|
EditRes: TNILMDevice;
|
|
Dlg : TfNILMDevice;
|
|
begin
|
|
DevID := NILM_SelectedDeviceID;
|
|
if DevID < 0 then
|
|
begin
|
|
MessageDlg('수정할 장비를 선택해주세요.', mtInformation, [mbOK], 0);
|
|
Exit;
|
|
end;
|
|
|
|
if not Assigned(FNILMManager) then FNILMManager := TNILMManager.Create;
|
|
|
|
try
|
|
DevRec := FNILMManager.LoadDevice(DevID);
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
Log(reDBLog, 'error', '[NILM] 장비 로드 오류: ' + E.Message);
|
|
Exit;
|
|
end;
|
|
end;
|
|
|
|
try
|
|
Dlg := TfNILMDevice.Create(Self);
|
|
try
|
|
Dlg.InitForEdit(DevRec);
|
|
if Dlg.Execute(EditRes) then
|
|
begin
|
|
try
|
|
if FNILMManager.SaveDevice(EditRes) then begin
|
|
Log(reDBLog, 'complete', Format('[NILM] 장비 %d 수정 완료', [EditRes.DeviceID]));
|
|
NILM_LoadGrid;
|
|
|
|
if DevRec.MqttTopic <> EditRes.MqttTopic then
|
|
begin
|
|
if Assigned(FMQTTClient) and FMQTTClient.Connected then
|
|
begin
|
|
if DevRec.MqttTopic <> '' then
|
|
begin
|
|
FMQTTClient.Unsubscribe(DevRec.MqttTopic);
|
|
Log(reMQTTLog, 'process', 'Unsubscribe(' + DevRec.MqttTopic + ')');
|
|
end;
|
|
if EditRes.MqttTopic <> '' then
|
|
addSubscribe(EditRes.MqttTopic);
|
|
end;
|
|
end;
|
|
end
|
|
else
|
|
Log(reDBLog, 'error', Format('[NILM] 장비 %d 수정 실패', [EditRes.DeviceID]));
|
|
except
|
|
on E: Exception do
|
|
Log(reDBLog, 'error', '[NILM] 장비 수정 오류: ' + E.Message);
|
|
end;
|
|
if Assigned(EditRes.SensorList) then FreeAndNil(EditRes.SensorList);
|
|
end;
|
|
finally
|
|
Dlg.Free;
|
|
end;
|
|
finally
|
|
if Assigned(DevRec.SensorList) then FreeAndNil(DevRec.SensorList);
|
|
end;
|
|
end;
|
|
|
|
// ─── JSON 내보내기 ────────────────────────────────────────────────────────
|
|
procedure TfMain.btnNILMExportJSONClick(Sender: TObject);
|
|
var
|
|
Dlg : TSaveDialog;
|
|
FilePath: string;
|
|
begin
|
|
if not Assigned(FDeviceList) or (FDeviceList.Count = 0) then
|
|
begin
|
|
MessageDlg('내보낼 장비 정보가 없습니다. 먼저 목록을 새로고침해주세요.', mtInformation, [mbOK], 0);
|
|
Exit;
|
|
end;
|
|
|
|
Dlg := TSaveDialog.Create(nil);
|
|
try
|
|
Dlg.Title := 'NILM 설정 내보내기';
|
|
Dlg.Filter := 'JSON 파일 (*.json)|*.json';
|
|
Dlg.DefaultExt := 'json';
|
|
Dlg.FileName := 'nilm_config_' + FormatDateTime('yyyymmdd_hhnn', Now) + '.json';
|
|
if Dlg.Execute then
|
|
begin
|
|
FilePath := Dlg.FileName;
|
|
if not Assigned(FNILMManager) then FNILMManager := TNILMManager.Create;
|
|
try
|
|
FNILMManager.ExportToJSON(FilePath, FDeviceList);
|
|
Log(reDBLog, 'complete', '[NILM] JSON 내보내기 완료: ' + FilePath);
|
|
MessageDlg('JSON 내보내기가 완료되었습니다.' + sLineBreak + FilePath, mtInformation, [mbOK], 0);
|
|
except
|
|
on E: Exception do
|
|
Log(reDBLog, 'error', '[NILM] JSON 내보내기 오류: ' + E.Message);
|
|
end;
|
|
end;
|
|
finally
|
|
Dlg.Free;
|
|
end;
|
|
end;
|
|
|
|
// ─── JSON 가져오기 ────────────────────────────────────────────────────────
|
|
procedure TfMain.btnNILMImportJSONClick(Sender: TObject);
|
|
var
|
|
Dlg : TOpenDialog;
|
|
FilePath : string;
|
|
ImportList : TList<TNILMDevice>;
|
|
D : TNILMDevice;
|
|
OkCnt, ErrCnt: Integer;
|
|
begin
|
|
Dlg := TOpenDialog.Create(nil);
|
|
try
|
|
Dlg.Title := 'NILM 설정 가져오기';
|
|
Dlg.Filter := 'JSON 파일 (*.json)|*.json';
|
|
if not Dlg.Execute then Exit;
|
|
FilePath := Dlg.FileName;
|
|
finally
|
|
Dlg.Free;
|
|
end;
|
|
|
|
if MessageDlg('JSON 파일을 가져오면 기존 DB 데이터와 병합(UPSERT)됩니다.' + sLineBreak +
|
|
'계속하시겠습니까?', mtConfirmation, [mbYes, mbNo], 0) <> mrYes then Exit;
|
|
|
|
if not Assigned(FNILMManager) then FNILMManager := TNILMManager.Create;
|
|
|
|
ImportList := nil;
|
|
try
|
|
ImportList := FNILMManager.ImportFromJSON(FilePath);
|
|
OkCnt := 0;
|
|
ErrCnt := 0;
|
|
for D in ImportList do
|
|
begin
|
|
try
|
|
var DTmp: TNILMDevice := D;
|
|
if FNILMManager.SaveDevice(DTmp) then Inc(OkCnt) else Inc(ErrCnt);;
|
|
except
|
|
Inc(ErrCnt);
|
|
end;
|
|
end;
|
|
Log(reDBLog, 'complete', Format('[NILM] JSON 가져오기 완료: 성공 %d, 실패 %d', [OkCnt, ErrCnt]));
|
|
NILM_LoadGrid;
|
|
MessageDlg(Format('JSON 가져오기가 완료되었습니다.' + sLineBreak + '성공: %d건, 실패: %d건', [OkCnt, ErrCnt]),
|
|
mtInformation, [mbOK], 0);
|
|
except
|
|
on E: Exception do
|
|
Log(reDBLog, 'error', '[NILM] JSON 가져오기 오류: ' + E.Message);
|
|
end;
|
|
|
|
if Assigned(ImportList) then
|
|
begin
|
|
for D in ImportList do
|
|
if Assigned(D.SensorList) then D.SensorList.Free;
|
|
ImportList.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.btnNILMInitDBClick(Sender: TObject);
|
|
begin
|
|
if not Assigned(FNILMManager) then FNILMManager := TNILMManager.Create;
|
|
try
|
|
FNILMManager.EnsureTables;
|
|
Log(reDBLog, 'complete', '[NILM] DB 테이블 생성/확인 완료 (nilm_device, nilm_sensor_config)');
|
|
MessageDlg('DB 테이블 초기화가 완료되었습니다.', mtInformation, [mbOK], 0);
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
Log(reDBLog, 'error', '[NILM] DB 테이블 초기화 오류: ' + E.Message);
|
|
MessageDlg('오류: ' + E.Message, mtError, [mbOK], 0);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.btnNILMLoadClick(Sender: TObject);
|
|
begin
|
|
chartrefresh;
|
|
end;
|
|
|
|
procedure TfMain.btnNILMRefreshClick(Sender: TObject);
|
|
begin
|
|
NILM_InitGrid;
|
|
NILM_LoadGrid;
|
|
end;
|
|
|
|
procedure TfMain.chartrefresh;
|
|
var
|
|
DevID : Integer;
|
|
MaxR : Integer;
|
|
Q : TFDQuery;
|
|
datetime_str : string;
|
|
begin
|
|
if not Assigned(FNILMManager) then FNILMManager := TNILMManager.Create;
|
|
|
|
DevID := 0;
|
|
if Assigned(cmbTrendDevice) and (cmbTrendDevice.ItemIndex > -1) then
|
|
begin
|
|
var S := cmbTrendDevice.Items[cmbTrendDevice.ItemIndex];
|
|
DevID := StrToIntDef(Copy(S, 2, Pos(']', S) - 2), 0);
|
|
end;
|
|
|
|
MaxR := StrToIntDef(edtHistMaxRows.Text, 500);
|
|
|
|
FreeAndNil(FEventLogQuery);
|
|
try
|
|
Series1.Clear;
|
|
Series2.Clear;
|
|
Series3.Clear;
|
|
Series4.Clear;
|
|
Series5.Clear;
|
|
|
|
// 히스토리 테이블 직접 조회
|
|
Q := FNILMManager.QueryNILMHistory(
|
|
DevID, dtpTrendFrom.DateTime, dtpTrendTo.DateTime + 1, -1, MaxR);
|
|
FEventLogQuery := Q; // 메모리 해제를 위해 기존 변수 재활용 또는 FHistoryQuery 할당 가능
|
|
|
|
// 차트 데이터가 있으면 역순으로 정렬되어 나오므로,
|
|
// 최신 값이 오른쪽에 그려지게 하려면 배열에 넣거나 순서를 뒤집어야 할 수도 있지만
|
|
// 기존 로직을 그대로 유지합니다.
|
|
while not Q.EOF do
|
|
begin
|
|
datetime_str := FormatDateTime('yy-mm-dd hh:nn:ss', Q.FieldByName('received_at').AsDateTime);
|
|
|
|
case rgCHSelect.ItemIndex of
|
|
0 : begin // L1
|
|
if cbpf.Checked then Series1.Add(Q.FieldByName('power_factor_a').AsFloat, datetime_str, $000080FF);
|
|
if cbvoltage.Checked then Series2.Add(Q.FieldByName('vrms_a').AsFloat, datetime_str, clRed);
|
|
if cbcurrent.Checked then Series3.Add(Q.FieldByName('irms_a').AsFloat, datetime_str, clGreen);
|
|
if cbVar.Checked then Series4.Add(Q.FieldByName('reactive_power_a').AsFloat, datetime_str, $00FF0080);
|
|
if cbVa.Checked then Series5.Add(Q.FieldByName('apparent_power_a').AsFloat, datetime_str, $00FF8000);
|
|
if cbw.Checked then Series6.Add(Q.FieldByName('active_power_a').AsFloat, datetime_str, clBlue);
|
|
end;
|
|
1 : begin // L2
|
|
if cbpf.Checked then Series1.Add(Q.FieldByName('power_factor_b').AsFloat, datetime_str, $000080FF);
|
|
if cbvoltage.Checked then Series2.Add(Q.FieldByName('vrms_b').AsFloat, datetime_str, clRed);
|
|
if cbcurrent.Checked then Series3.Add(Q.FieldByName('irms_b').AsFloat, datetime_str, clGreen);
|
|
if cbVar.Checked then Series4.Add(Q.FieldByName('reactive_power_b').AsFloat, datetime_str, $00FF0080);
|
|
if cbVa.Checked then Series5.Add(Q.FieldByName('apparent_power_b').AsFloat, datetime_str, $00FF8000);
|
|
if cbw.Checked then Series6.Add(Q.FieldByName('active_power_b').AsFloat, datetime_str, clBlue);
|
|
end;
|
|
2 : begin // L3
|
|
if cbpf.Checked then Series1.Add(Q.FieldByName('power_factor_c').AsFloat, datetime_str, $000080FF);
|
|
if cbvoltage.Checked then Series2.Add(Q.FieldByName('vrms_c').AsFloat, datetime_str, clRed);
|
|
if cbcurrent.Checked then Series3.Add(Q.FieldByName('irms_c').AsFloat, datetime_str, clGreen);
|
|
if cbVar.Checked then Series4.Add(Q.FieldByName('reactive_power_c').AsFloat, datetime_str, $00FF0080);
|
|
if cbVa.Checked then Series5.Add(Q.FieldByName('apparent_power_c').AsFloat, datetime_str, $00FF8000);
|
|
if cbw.Checked then Series6.Add(Q.FieldByName('active_power_c').AsFloat, datetime_str, clBlue);
|
|
end;
|
|
end;
|
|
Q.Next;
|
|
end;
|
|
|
|
if cbpf.Checked then Series1.Visible := True;
|
|
if cbvoltage.Checked then Series2.Visible := True;
|
|
if cbcurrent.Checked then Series3.Visible := True;
|
|
if cbVar.Checked then Series4.Visible := True;
|
|
if cbVa.Checked then Series5.Visible := True;
|
|
if cbw.Checked then Series6.Visible := True;
|
|
|
|
lblTrendCount.Caption := Format('트렌드 로그: %d 건', [Q.RecordCount]);
|
|
except
|
|
on E: Exception do
|
|
Log(reDBLog, 'error', '[트렌드 로그] 조회 오류: ' + E.Message);
|
|
end;
|
|
end;
|
|
procedure TfMain.Chart_MainClickSeries(Sender: TCustomChart;
|
|
Series: TChartSeries; ValueIndex: Integer; Button: TMouseButton;
|
|
Shift: TShiftState; X, Y: Integer);
|
|
var
|
|
ckSTR : string;
|
|
begin
|
|
ckSTR := '['+
|
|
'NILM: '+cmbTrendDevice.Text+','+
|
|
'CH: '+rgCHSelect.Items[rgCHSelect.ItemIndex]+','+
|
|
'DateTime:'+'20'+Series.XLabel[ValueIndex];
|
|
|
|
if (Assigned(Series1) and Series1.Visible and (Series1.YValues.Count > 0)) then
|
|
ckSTR := ckSTR +','+'PF:'+ Series1.YValue[ValueIndex].ToString
|
|
else
|
|
ckSTR := ckSTR +','+'PF:'+ '---';
|
|
|
|
if (Assigned(Series2) and Series2.Visible and (Series2.YValues.Count > 0)) then
|
|
ckSTR := ckSTR +','+'Volt:'+ Series2.YValue[ValueIndex].ToString
|
|
else
|
|
ckSTR := ckSTR +','+'Volt:'+ '---';
|
|
|
|
if (Assigned(Series3) and Series3.Visible and (Series3.YValues.Count > 0)) then
|
|
ckSTR := ckSTR +','+'CUR:'+ Series3.YValue[ValueIndex].ToString
|
|
else
|
|
ckSTR := ckSTR +','+'CUR:'+ '---';
|
|
|
|
if (Assigned(Series4) and Series4.Visible and (Series4.YValues.Count > 0)) then
|
|
ckSTR := ckSTR +','+'Var:'+ Series4.YValue[ValueIndex].ToString
|
|
else
|
|
ckSTR := ckSTR +','+'Var:'+ '---';
|
|
|
|
if (Assigned(Series5) and Series5.Visible and (Series5.YValues.Count > 0)) then
|
|
ckSTR := ckSTR +','+'VA:'+ Series5.YValue[ValueIndex].ToString
|
|
else
|
|
ckSTR := ckSTR +','+'VA:'+ '---';
|
|
|
|
ckSTR := ckSTR +']';
|
|
|
|
reTrendCheckLog.Lines.add( ckSTR );
|
|
end;
|
|
|
|
{
|
|
var
|
|
i : integer;
|
|
ck, ck_min, ck_max, ck_sum : Double;
|
|
ck1, ck_min1, ck_max1, ck_sum1 : Double;
|
|
ck2, ck_min2, ck_max2, ck_sum2 : Double;
|
|
ck3, ck_min3, ck_max3, ck_sum3 : Double;
|
|
ck4, ck_min4, ck_max4, ck_sum4 : Double;
|
|
|
|
begin
|
|
Series1.Clear;
|
|
Series2.Clear;
|
|
Series3.Clear;
|
|
Series4.Clear;
|
|
Series5.Clear;
|
|
|
|
ck_min :=0.0; ck_max := 0.0; ck_sum :=0;
|
|
ck_min1:=0.0; ck_max1 := 0.0; ck_sum1:=0;
|
|
ck_min2:=0.0; ck_max2 := 0.0; ck_sum2:=0;
|
|
ck_min3:=0.0; ck_max3 := 0.0; ck_sum3:=0;
|
|
ck_min4:=0.0; ck_max4 := 0.0; ck_sum4:=0;
|
|
|
|
for I := 0 to 100 do begin
|
|
|
|
if cbpf.Checked then Series1.Add(I*1, IntToStr(I), $000080FF);
|
|
if cbvoltage.Checked then Series2.Add(I*10, IntToStr(I), clRed);
|
|
if cbcurrent.Checked then Series3.Add(I*100, IntToStr(I), clGreen);
|
|
if cbVar.Checked then Series4.Add(I*1000, IntToStr(I), $00FF0080);
|
|
if cbVa.Checked then Series5.Add(I*10000, IntToStr(I), $00FF8000);
|
|
if cbw.Checked then Series6.Add(I*100000, IntToStr(I), clBlue);
|
|
|
|
end;
|
|
|
|
if cbpf.Checked then Series1.Visible := True;
|
|
if cbvoltage.Checked then Series2.Visible := True;
|
|
if cbcurrent.Checked then Series3.Visible := True;
|
|
if cbVar.Checked then Series4.Visible := True;
|
|
if cbVa.Checked then Series5.Visible := True;
|
|
if cbw.Checked then Series6.Visible := True;
|
|
|
|
for I := 1 to AdvColumnGrid1.RowCount-1 do
|
|
begin
|
|
|
|
ck := AdvColumnGrid1.Floats[2,i];
|
|
if (ck_min=0) or (ck_min > ck) then ck_min := ck;
|
|
if (ck_max=0) or (ck_max < ck) then ck_max := ck;
|
|
ck_sum := ck_sum + ck;
|
|
|
|
ck1 := AdvColumnGrid1.Floats[3,i];
|
|
if (ck_min1=0) or (ck_min1 > ck1) then ck_min1 := ck1;
|
|
if (ck_max1=0) or (ck_max1 < ck1) then ck_max1 := ck1;
|
|
ck_sum1 := ck_sum1 + ck1;
|
|
|
|
ck2 := AdvColumnGrid1.Floats[4,i];
|
|
if (ck_min2=0) or (ck_min2 > ck2) then ck_min2 := ck2;
|
|
if (ck_max2=0) or (ck_max2 < ck2) then ck_max2 := ck2;
|
|
ck_sum2 := ck_sum2 + ck2;
|
|
|
|
ck3 := AdvColumnGrid1.Floats[5,i];
|
|
if (ck_min3=0) or (ck_min3 > ck3) then ck_min3 := ck3;
|
|
if (ck_max3=0) or (ck_max3 < ck3) then ck_max3 := ck3;
|
|
ck_sum3 := ck_sum3 + ck3;
|
|
|
|
ck4 := AdvColumnGrid1.Floats[6,i];
|
|
if (ck_min4=0) or (ck_min4 > ck4) then ck_min4 := ck4;
|
|
if (ck_max4=0) or (ck_max4 < ck4) then ck_max4 := ck4;
|
|
ck_sum4 := ck_sum4 + ck4;
|
|
|
|
if CheckBox1.Checked then Series1.Add(ck*1000000, AdvColumnGrid1.Cells[0,i], $000080FF);
|
|
if CheckBox2.Checked then Series2.Add(ck1*1000, AdvColumnGrid1.Cells[0,i], clRed);
|
|
if CheckBox3.Checked then Series3.Add(ck2*100000, AdvColumnGrid1.Cells[0,i], clGreen);
|
|
if CheckBox4.Checked then Series4.Add(ck3*1000, AdvColumnGrid1.Cells[0,i], $00FF0080);
|
|
if CheckBox5.Checked then Series5.Add(ck4*1000, AdvColumnGrid1.Cells[0,i], $00FF8000);
|
|
end;
|
|
|
|
ListBox2.Clear;
|
|
ListBox2.Items.Add('최소값 : '+Format('%s / %s / %s / %s / %s', [LeftStr( FloatToStr(ck_min)+' ',30),
|
|
LeftStr( FloatToStr(ck_min1)+' ',30),
|
|
LeftStr( FloatToStr(ck_min2)+' ',30),
|
|
LeftStr( FloatToStr(ck_min3)+' ',30),
|
|
LeftStr( FloatToStr(ck_min4)+' ',30)]));
|
|
ListBox2.Items.Add('최대값 : '+Format('%s / %s / %s / %s / %s', [LeftStr( FloatToStr(ck_max)+' ',30),
|
|
LeftStr( FloatToStr(ck_max1)+' ',30),
|
|
LeftStr( FloatToStr(ck_max2)+' ',30),
|
|
LeftStr( FloatToStr(ck_max3)+' ',30),
|
|
LeftStr( FloatToStr(ck_max4)+' ',30)]));
|
|
ListBox2.Items.Add('평균값 : '+Format('%s / %s / %s / %s / %s', [LeftStr( FloatToStr(ck_sum / (AdvColumnGrid1.RowCount-1))+' ',20),
|
|
LeftStr( FloatToStr(ck_sum1 / (AdvColumnGrid1.RowCount-1))+' ',20),
|
|
LeftStr( FloatToStr(ck_sum2 / (AdvColumnGrid1.RowCount-1))+' ',20),
|
|
LeftStr( FloatToStr(ck_sum3 / (AdvColumnGrid1.RowCount-1))+' ',20),
|
|
LeftStr( FloatToStr(ck_sum4 / (AdvColumnGrid1.RowCount-1))+' ',20)]));
|
|
|
|
if CheckBox1.Checked then Series1.Visible := True;
|
|
if CheckBox2.Checked then Series2.Visible := True;
|
|
if CheckBox3.Checked then Series3.Visible := True;
|
|
if CheckBox4.Checked then Series4.Visible := True;
|
|
if CheckBox5.Checked then Series5.Visible := True;
|
|
end;
|
|
}
|
|
|
|
|
|
procedure TfMain.btnOPCConnectClick(Sender: TObject);
|
|
var
|
|
i: Integer;
|
|
StepMsg: string;
|
|
begin
|
|
|
|
if Trim(edtOPCServerName.Text) = '' then
|
|
begin
|
|
Log(reOPCLog,'error','OPC Server Name is not registered.');
|
|
Exit;
|
|
end;
|
|
|
|
if bConnect then begin
|
|
try
|
|
if not VarIsEmpty(OPCGroup) then
|
|
OPCServer.OPCGroups.RemoveAll;
|
|
if not VarIsEmpty(OPCServer) then
|
|
OPCServer.Disconnect;
|
|
except
|
|
end;
|
|
|
|
for i := 1 to ITEMMAX do
|
|
OPCItemObjects[i] := Unassigned;
|
|
|
|
OPCGroup := Unassigned;
|
|
OPCServer := Unassigned;
|
|
|
|
bConnect := False;
|
|
|
|
Log(reOPCLog,'process','OPC DisConnected.');
|
|
|
|
lblOPCConnectionStatus.Caption := 'DisConnected';
|
|
lblOPCConnectionStatus.Font.Color := CLR_RED;
|
|
lblOPCConnectionStatus.Tag := 2;
|
|
|
|
btnOPCRead.Enabled := False;
|
|
btnOPCWrite.Enabled := False;
|
|
edtOPCServerName.Enabled := True;
|
|
|
|
// btnAdvise.Enabled := False;
|
|
|
|
for i := 1 to ITEMMAX do
|
|
txtItemName[i - 1].Enabled := True;
|
|
end;
|
|
|
|
try
|
|
StepMsg := 'CreateOleObject';
|
|
// Create OPC Server using Late Binding
|
|
OPCServer := CreateOleObject('OPC.Automation.1');
|
|
|
|
StepMsg := 'OPCServer.Connect';
|
|
OPCServer.Connect(edtOPCServerName.Text, EmptyParam);
|
|
|
|
StepMsg := 'OPCGroups.Add';
|
|
OPCGroup := OPCServer.OPCGroups.Add('Group1');
|
|
|
|
StepMsg := 'Set UpdateRate';
|
|
OPCGroup.UpdateRate := StrToIntDef(edtOPCUpdateRate.Text, 1000);
|
|
|
|
StepMsg := 'Set IsActive';
|
|
OPCGroup.IsActive := True;
|
|
|
|
StepMsg := 'Set IsSubscribed';
|
|
OPCGroup.IsSubscribed := False;
|
|
|
|
StepMsg := 'AddItem (Loop)';
|
|
|
|
bConnect := True;
|
|
|
|
// 20260624 btnOPCRead.Enabled := True;
|
|
// 20260624 btnOPCWrite.Enabled := True;
|
|
edtOPCServerName.Enabled := False;
|
|
|
|
// btnAdvise.Enabled := True;
|
|
|
|
lblOPCConnectionStatus.Caption := 'Connected';
|
|
lblOPCConnectionStatus.Font.Color := CLR_GREEN;
|
|
lblOPCConnectionStatus.Tag := 1;
|
|
|
|
Log(reOPCLog,'process','OPC Connected.');
|
|
|
|
// 20260624 btnOPCReadClick(Sender);
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
Log(reOPCLog,'error','OPC Connection failed at [' + StepMsg + ']: ' + E.Message);
|
|
Exit;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.btnOPCDisConnectClick(Sender: TObject);
|
|
var
|
|
i : integer;
|
|
begin
|
|
if bConnect then begin
|
|
try
|
|
Log(reOPCLog, 'process', ' OPC Step: Unassign OPCItemObjects');
|
|
for i := 1 to ITEMMAX do
|
|
OPCItemObjects[i] := Unassigned;
|
|
|
|
Log(reOPCLog, 'process', ' OPC Step: FDynamicOPCItems.Clear');
|
|
if FDynamicOPCItems <> nil then
|
|
FDynamicOPCItems.Clear;
|
|
|
|
Log(reOPCLog, 'process', ' OPC Step: Unassign OPCGroup');
|
|
OPCGroup := Unassigned;
|
|
|
|
if not VarIsEmpty(OPCServer) then
|
|
begin
|
|
Log(reOPCLog, 'process', ' OPC Step: Skipping RemoveAll and Disconnect to prevent DLL crash');
|
|
// try OPCServer.OPCGroups.RemoveAll; except end;
|
|
// try OPCServer.Disconnect; except end;
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
Log(reOPCLog, 'error', 'Ignored COM Error during OPC disconnect: ' + E.Message);
|
|
end;
|
|
|
|
Log(reOPCLog, 'process', ' OPC Step: Unassign OPCServer');
|
|
try
|
|
OPCServer := Unassigned;
|
|
except
|
|
end;
|
|
|
|
Log(reOPCLog, 'process', ' OPC Step: Clearing collections');
|
|
FPreviousOPCValues.Clear;
|
|
|
|
bConnect := False;
|
|
|
|
Log(reOPCLog,'process','OPC DisConnected.');
|
|
|
|
Log(reOPCLog,'process',' OPC Step: Update UI (Status)');
|
|
lblOPCConnectionStatus.Caption := 'DisConnected';
|
|
lblOPCConnectionStatus.Font.Color := CLR_RED;
|
|
lblOPCConnectionStatus.Tag := 2;
|
|
|
|
Log(reOPCLog,'process',' OPC Step: Update UI (ServerName)');
|
|
edtOPCServerName.Enabled := True;
|
|
|
|
Log(reOPCLog,'process',' OPC Step: Update UI (txtItemName)');
|
|
try
|
|
for i := 1 to ITEMMAX do
|
|
begin
|
|
if Assigned(txtItemName[i - 1]) then
|
|
txtItemName[i - 1].Enabled := True;
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
Log(reOPCLog, 'error', ' Error updating txtItemName: ' + E.Message);
|
|
end;
|
|
|
|
Log(reOPCLog,'process',' OPC Step: Update UI (btnMainStart)');
|
|
try
|
|
if not btnMainStart.Enabled then
|
|
btnMainStart.Enabled := true;
|
|
except
|
|
end;
|
|
|
|
Log(reOPCLog,'process',' OPC Step: btnOPCDisConnectClick Returning');
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.btnOPCReadClick(Sender: TObject);
|
|
var
|
|
i: Integer;
|
|
ItemValue: OLEVariant;
|
|
Quality: OLEVariant;
|
|
TimeStamp: OLEVariant;
|
|
begin
|
|
if not bConnect then Exit;
|
|
|
|
try
|
|
for i := 1 to ITEMMAX do
|
|
begin
|
|
// 빈 슬롯 스킵
|
|
if VarIsEmpty(OPCItemObjects[i]) or VarIsNull(OPCItemObjects[i]) then
|
|
Continue;
|
|
|
|
OPCItemObjects[i].Read(Integer(1), ItemValue, Quality, TimeStamp);
|
|
|
|
// CanonicalDataType: 서버가 선언한 태그의 원본 타입 (VarType(ItemValue)와 다를 수 있음)
|
|
// Write 시 반드시 이 타입을 사용해야 E_INVALIDARG를 피할 수 있음
|
|
FItemVarTypes[i] := OPCItemObjects[i].CanonicalDataType;
|
|
case FItemVarTypes[i] of
|
|
varSmallInt: txtType[i-1].Text := 'Short';
|
|
varInteger: txtType[i-1].Text := 'Int';
|
|
varSingle: txtType[i-1].Text := 'Float';
|
|
varDouble: txtType[i-1].Text := 'Double';
|
|
varBoolean: txtType[i-1].Text := 'Bool';
|
|
varOleStr: txtType[i-1].Text := 'String';
|
|
varByte: txtType[i-1].Text := 'Byte';
|
|
varWord: txtType[i-1].Text := 'Word';
|
|
varLongWord: txtType[i-1].Text := 'DWord';
|
|
varInt64: txtType[i-1].Text := 'Int64';
|
|
else
|
|
txtType[i-1].Text := 'CDT' + IntToStr(FItemVarTypes[i]);
|
|
end;
|
|
|
|
txtValue[i - 1].Text := VarToStr(ItemValue);
|
|
if not VarIsEmpty(TimeStamp) and not VarIsNull(TimeStamp) then
|
|
txtTime[i - 1].Text := DateTimeToStr(VarToDateTime(TimeStamp));
|
|
if not VarIsEmpty(Quality) and not VarIsNull(Quality) then
|
|
txtQuality[i - 1].Text := VarToStr(Quality);
|
|
end;
|
|
except
|
|
on E: Exception do Log(reOPCLog,'error','OPC Read Error: ' + E.Message);
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.btnOPCWriteClick(Sender: TObject);
|
|
var
|
|
i, cnt: Integer;
|
|
ServerHandles: OLEVariant;
|
|
ItemValues: OLEVariant;
|
|
Errors: OLEVariant;
|
|
targetVT: Integer;
|
|
inputText: string;
|
|
begin
|
|
if not bConnect then Exit;
|
|
|
|
// 유효한 아이템 수 집계
|
|
cnt := 0;
|
|
for i := 1 to ITEMMAX do
|
|
if not VarIsEmpty(OPCItemObjects[i]) and not VarIsNull(OPCItemObjects[i]) then
|
|
Inc(cnt);
|
|
if cnt = 0 then Exit;
|
|
|
|
ServerHandles := VarArrayCreate([0, cnt - 1], varInteger);
|
|
ItemValues := VarArrayCreate([0, cnt - 1], varVariant);
|
|
Errors := VarArrayCreate([0, cnt - 1], varInteger);
|
|
|
|
cnt := -1;
|
|
for i := 1 to ITEMMAX do
|
|
begin
|
|
if VarIsEmpty(OPCItemObjects[i]) or VarIsNull(OPCItemObjects[i]) then
|
|
Continue;
|
|
|
|
inputText := Trim(txtValue[i - 1].Text);
|
|
if inputText = '' then
|
|
Continue;
|
|
|
|
Inc(cnt);
|
|
ServerHandles[cnt] := Integer(sH[i]);
|
|
|
|
// CanonicalDataType 기반으로 올바른 타입의 Variant 생성
|
|
// ※ VarAsType() 대신 Delphi 직접 캐스팅 사용:
|
|
// varVariant 배열 요소에 VarAsType 결과를 넣으면 VT_VARIANT|VT_I2 로 이중 래핑되어
|
|
// SyncWrite에서 DISP_E_TYPEMISMATCH가 발생할 수 있음
|
|
targetVT := FItemVarTypes[i];
|
|
if targetVT = 0 then targetVT := varSmallInt;
|
|
|
|
case targetVT of
|
|
varSmallInt,
|
|
varInteger,
|
|
varByte,
|
|
varWord:
|
|
ItemValues[cnt] := Integer(StrToIntDef(inputText, 0));
|
|
|
|
varLongWord:
|
|
ItemValues[cnt] := Integer(StrToInt64Def(inputText, 0));
|
|
|
|
varInt64:
|
|
ItemValues[cnt] := StrToInt64Def(inputText, 0);
|
|
|
|
varSingle,
|
|
varDouble:
|
|
ItemValues[cnt] := Double(StrToFloatDef(inputText, 0.0));
|
|
|
|
varBoolean:
|
|
ItemValues[cnt] := OleVariant(Boolean(
|
|
(inputText <> '0') and (inputText <> '')
|
|
));
|
|
|
|
varOleStr:
|
|
ItemValues[cnt] := OleVariant(inputText);
|
|
else
|
|
ItemValues[cnt] := OleVariant(inputText);
|
|
end;
|
|
|
|
try
|
|
OPCItemObjects[i].Write(ItemValues[cnt]);
|
|
|
|
Log(reOPCLog,'process','['+IntToStr(i)+'/'+inputText+'] Write 완료!');
|
|
except
|
|
on E: EOleException do
|
|
Log(reOPCLog,'error',
|
|
'Write Error [0x' + IntToHex(E.ErrorCode, 8) + ']: ' + E.Message +
|
|
' / ' + 'CanonicalDataType: ' + IntToStr(targetVT) +
|
|
' / ' + '입력값: ' + inputText);
|
|
|
|
on E: Exception do
|
|
Log(reOPCLog,'error',
|
|
'Write Error [' + E.ClassName + ']: ' + E.Message);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
{
|
|
procedure TfMain.btnOPCWriteClick(Sender: TObject);
|
|
var
|
|
i, cnt: Integer;
|
|
ServerHandles: OLEVariant;
|
|
ItemValues: OLEVariant;
|
|
Errors: OLEVariant;
|
|
targetVT: Integer;
|
|
inputText: string;
|
|
|
|
ItemValue: OLEVariant;
|
|
Quality: OLEVariant;
|
|
TimeStamp: OLEVariant;
|
|
|
|
begin
|
|
if not bConnect then Exit;
|
|
|
|
// 유효한 아이템 수 집계
|
|
cnt := 0;
|
|
for i := 1 to ITEMMAX do
|
|
if not VarIsEmpty(OPCItemObjects[i]) and not VarIsNull(OPCItemObjects[i]) then
|
|
Inc(cnt);
|
|
if cnt = 0 then Exit;
|
|
|
|
ServerHandles := VarArrayCreate([0, cnt - 1], varInteger);
|
|
ItemValues := VarArrayCreate([0, cnt - 1], varVariant);
|
|
Errors := VarArrayCreate([0, cnt - 1], varInteger);
|
|
|
|
cnt := -1;
|
|
for i := 1 to ITEMMAX do
|
|
begin
|
|
if VarIsEmpty(OPCItemObjects[i]) or VarIsNull(OPCItemObjects[i]) then
|
|
Continue;
|
|
|
|
inputText := Trim(txtValue[i - 1].Text);
|
|
if inputText = '' then
|
|
Continue;
|
|
|
|
Inc(cnt);
|
|
ServerHandles[cnt] := Integer(sH[i]);
|
|
|
|
// CanonicalDataType 기반으로 올바른 타입의 Variant 생성
|
|
// ※ VarAsType() 대신 Delphi 직접 캐스팅 사용:
|
|
// varVariant 배열 요소에 VarAsType 결과를 넣으면 VT_VARIANT|VT_I2 로 이중 래핑되어
|
|
// SyncWrite에서 DISP_E_TYPEMISMATCH가 발생할 수 있음
|
|
targetVT := FItemVarTypes[i];
|
|
if targetVT = 0 then targetVT := varSmallInt;
|
|
|
|
case targetVT of
|
|
varSmallInt,
|
|
varInteger,
|
|
varByte,
|
|
varWord:
|
|
ItemValues[cnt] := Integer(StrToIntDef(inputText, 0));
|
|
|
|
varLongWord:
|
|
ItemValues[cnt] := Integer(StrToInt64Def(inputText, 0));
|
|
|
|
varInt64:
|
|
ItemValues[cnt] := StrToInt64Def(inputText, 0);
|
|
|
|
varSingle,
|
|
varDouble:
|
|
ItemValues[cnt] := Double(StrToFloatDef(inputText, 0.0));
|
|
|
|
varBoolean:
|
|
ItemValues[cnt] := OleVariant(Boolean(
|
|
(inputText <> '0') and (inputText <> '')
|
|
));
|
|
|
|
varOleStr:
|
|
ItemValues[cnt] := OleVariant(inputText);
|
|
else
|
|
ItemValues[cnt] := OleVariant(inputText);
|
|
end;
|
|
|
|
try
|
|
OPCItemObjects[i].Write(ItemValues[cnt]);
|
|
|
|
Log(reOPCLog,'process','['+IntToStr(i)+'/'+inputText+'] Write 완료!');
|
|
except
|
|
on E: EOleException do
|
|
Log(reOPCLog,'error',
|
|
'Write Error [0x' + IntToHex(E.ErrorCode, 8) + ']: ' + E.Message +
|
|
' / ' + 'CanonicalDataType: ' + IntToStr(targetVT) +
|
|
' / ' + '입력값: ' + inputText);
|
|
|
|
on E: Exception do
|
|
Log(reOPCLog,'error',
|
|
'Write Error [' + E.ClassName + ']: ' + E.Message);
|
|
end;
|
|
end;
|
|
end;
|
|
}
|
|
|
|
procedure TfMain.FormCreate(Sender: TObject);
|
|
begin
|
|
pcMain.ActivePageIndex := 0;
|
|
pcSetting.ActivePageIndex := 0;
|
|
pcHistory.ActivePageIndex := 0;
|
|
|
|
// BaseList_NILMItem := TList<TNILMItem>.Create;
|
|
BaseList_NILMItem := TObjectList<TNILMItem>.Create(True);
|
|
NILMItem := TNILMItem.Create;
|
|
|
|
FPreviousOPCValues := TDictionary<string, string>.Create;
|
|
FDynamicOPCItems := TDictionary<string, OLEVariant>.Create;
|
|
FLastEnergyUpdate := TDictionary<Integer, TDateTime>.Create;
|
|
FDayEnergyValue := TDictionary<Integer, Double>.Create;
|
|
FLastDailyEnergySyncDate := Date;
|
|
FLastRecvTime := TDictionary<Integer, TDateTime>.Create;
|
|
FDeviceTimeoutHandled := TDictionary<Integer, Boolean>.Create;
|
|
|
|
if not Assigned(FNILMManager) then
|
|
FNILMManager := TNILMManager.Create;
|
|
FNILMManager.OnError := NILMErrorHandler;
|
|
|
|
dtpHistFrom.DateTime := now;
|
|
dtpHistTo.DateTime := now;
|
|
dtpEvtFrom.DateTime := now;
|
|
dtpEvtTo.DateTime := now;
|
|
dtpTrendFrom.DateTime := now;
|
|
dtpTrendTo.DateTime := now;
|
|
|
|
InitLogger('.\Logs', 'log', rtHourly);
|
|
|
|
// SetupUI;
|
|
|
|
LoadSettings;
|
|
FLastOPCRetryTime := Now;
|
|
|
|
if cbAutoStart.Checked then
|
|
begin
|
|
TThread.CreateAnonymousThread(
|
|
procedure
|
|
begin
|
|
Sleep(500); // 폼과 데이터모듈(DM)이 완전히 생성될 때까지 충분히 대기
|
|
TThread.ForceQueue(nil,
|
|
procedure
|
|
begin
|
|
if btnMainStart.Enabled then
|
|
btnMainStart.Click;
|
|
end);
|
|
end).Start;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.FormDestroy(Sender: TObject);
|
|
begin
|
|
SaveSettings;
|
|
|
|
if FMQTTClient <> nil then begin
|
|
FMQTTClient.Disconnect;
|
|
FreeAndNil(FMQTTClient);
|
|
end;
|
|
BaseList_NILMItem.Free;
|
|
NILMItem.Free;
|
|
|
|
FPreviousOPCValues.Free;
|
|
FDynamicOPCItems.Free;
|
|
FLastEnergyUpdate.Free;
|
|
FDayEnergyValue.Free;
|
|
FLastRecvTime.Free;
|
|
FDeviceTimeoutHandled.Free;
|
|
end;
|
|
|
|
// ─── MQTT ────────────────────────────────────────────────────────────────────
|
|
procedure TfMain.OnMQTTStatus(AConnected: Boolean);
|
|
begin
|
|
if AConnected then begin
|
|
lblMQTTConnectionStatus.Caption := 'Connected';
|
|
lblMQTTConnectionStatus.Font.Color := CLR_GREEN;
|
|
lblMQTTConnectionStatus.Tag := 1;
|
|
|
|
btnSubscribe.Enabled := True;
|
|
|
|
btnSubscribe.Click;
|
|
|
|
Log(reMQTTLog,'process','MQTT Connected.');
|
|
|
|
end else begin
|
|
lblMQTTConnectionStatus.Caption := 'DisConnected';
|
|
lblMQTTConnectionStatus.Font.Color := CLR_RED;
|
|
lblMQTTConnectionStatus.Tag := 2;
|
|
|
|
Log(reMQTTLog,'process','MQTT Connection Error - Connection to server was lost. Auto reconnecting in background...');
|
|
|
|
btnSubscribe.Enabled := False;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.tmrdelaystopTimer(Sender: TObject);
|
|
begin
|
|
// tmrdelaystop.Enabled := False;
|
|
// Self.Enabled := True;
|
|
end;
|
|
|
|
procedure TfMain.tmrTimeTimer(Sender: TObject);
|
|
begin
|
|
lbDateTime.Caption := FormatDateTime('yyyy-mm-dd hh:nn:ss',now);
|
|
|
|
// 1분(60초) 이상 통신 두절된 장비 감시 및 전원꺼짐(0) / COMM=255 자동 처리
|
|
CheckDeviceTimeouts;
|
|
|
|
// 10분(10.0 / 1440.0)마다 실패했던 태그들의 이력을 지워서 다시 시도하도록 함
|
|
if (Now - FLastOPCRetryTime) >= (10.0 / 1440.0) then
|
|
begin
|
|
FLastOPCRetryTime := Now;
|
|
ClearFailedOPCTags;
|
|
end;
|
|
|
|
// 자정(00시 00분) 일일 전력량 외부 DB 동기화 (하루 1회)
|
|
if (Date > FLastDailyEnergySyncDate) and (FormatDateTime('hhnn', Now) = '0000') then
|
|
begin
|
|
FLastDailyEnergySyncDate := Date;
|
|
if Assigned(FDeviceList) and Assigned(FNILMManager) then
|
|
begin
|
|
var D: TNILMDevice;
|
|
for D in FDeviceList do
|
|
begin
|
|
var EnergyWh: Double := 0;
|
|
if FDayEnergyValue.TryGetValue(D.DeviceID, EnergyWh) then
|
|
begin
|
|
// 자정에 전송하는 데이터는 "어제(Date - 1)" 기준
|
|
var devName := Trim(D.DeviceName);
|
|
if devName = '' then devName := IntToStr(D.DeviceID);
|
|
FNILMManager.SaveExternalDailyEnergy(devName, Date - 1, EnergyWh);
|
|
end;
|
|
// 다음 날을 위해 현재 장비 누적 전력량 초기화
|
|
FDayEnergyValue.AddOrSetValue(D.DeviceID, 0);
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.CheckDeviceTimeouts;
|
|
var
|
|
currTime: TDateTime;
|
|
D: TNILMDevice;
|
|
did: Integer;
|
|
lastTime: TDateTime;
|
|
bHandled: Boolean;
|
|
devName: string;
|
|
begin
|
|
currTime := Now;
|
|
|
|
// 1. 등록된 장비 목록 확인
|
|
if Assigned(FDeviceList) then
|
|
begin
|
|
for D in FDeviceList do
|
|
begin
|
|
did := D.DeviceID;
|
|
devName := D.DeviceName;
|
|
if FLastRecvTime.TryGetValue(did, lastTime) and (lastTime > 0) then
|
|
begin
|
|
// 1분(60초) 이상 통신 패킷이 들어오지 않은 경우
|
|
if (currTime - lastTime) >= (60.0 / 86400.0) then
|
|
begin
|
|
bHandled := False;
|
|
FDeviceTimeoutHandled.TryGetValue(did, bHandled);
|
|
if not bHandled then
|
|
HandleDeviceTimeout(did, devName);
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
// 2. 그리드에 올라와 있는 장비도 FLastRecvTime 기반으로 감시
|
|
for var r := 1 to grdMQTTMonitor.RowCount - 1 do
|
|
begin
|
|
did := StrToIntDef(grdMQTTMonitor.Cells[0, r], 0);
|
|
if did > 0 then
|
|
begin
|
|
if FLastRecvTime.TryGetValue(did, lastTime) and (lastTime > 0) then
|
|
begin
|
|
if (currTime - lastTime) >= (60.0 / 86400.0) then
|
|
begin
|
|
bHandled := False;
|
|
FDeviceTimeoutHandled.TryGetValue(did, bHandled);
|
|
if not bHandled then
|
|
begin
|
|
devName := IntToStr(did);
|
|
if Assigned(FDeviceList) then
|
|
for D in FDeviceList do
|
|
if D.DeviceID = did then
|
|
begin
|
|
devName := D.DeviceName;
|
|
Break;
|
|
end;
|
|
HandleDeviceTimeout(did, devName);
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.HandleDeviceTimeout(ADeviceID: Integer; const ADeviceName: string);
|
|
var
|
|
NilmName: string;
|
|
row_ck: Integer;
|
|
Q: TFDQuery;
|
|
begin
|
|
FDeviceTimeoutHandled.AddOrSetValue(ADeviceID, True);
|
|
|
|
NilmName := Trim(ADeviceName);
|
|
if NilmName = '' then NilmName := IntToStr(ADeviceID);
|
|
|
|
// 1. 화면 모니터링 그리드 (grdMQTTMonitor) 갱신
|
|
row_ck := 0;
|
|
for var r := 1 to grdMQTTMonitor.RowCount - 1 do
|
|
begin
|
|
if grdMQTTMonitor.Cells[0, r] = ADeviceID.ToString then
|
|
begin
|
|
row_ck := r;
|
|
Break;
|
|
end;
|
|
end;
|
|
|
|
if row_ck > 0 then
|
|
begin
|
|
grdMQTTMonitor.Cells[4, row_ck] := '전원꺼짐(0)';
|
|
end;
|
|
|
|
// 2. OPC 서버 쓰기 (COMM=255 통신두절, OpStatus=0 전원꺼짐)
|
|
WriteOPCValueIfChanged('SYSTEM.' + IntToStr(ADeviceID) + '/COMM', '255');
|
|
WriteOPCValueIfChanged('SYSTEM.' + IntToStr(ADeviceID) + '/OpStatus', '0');
|
|
|
|
// 3. 외부 타사 DB (inf_operation_active / inf_operation_history) 상태 '0'(전원꺼짐) 연동
|
|
if Assigned(FNILMManager) then
|
|
begin
|
|
try
|
|
FNILMManager.SaveExternalOpStatus(NilmName, 0);
|
|
except
|
|
on E: Exception do
|
|
Log(reDBLog, 'error', Format('[타임아웃] 외부 DB 상태 연동 오류 (%s): %s', [NilmName, E.Message]));
|
|
end;
|
|
end;
|
|
|
|
// 4. 내부 DB nilm_data 테이블 최신 상태 '0' 및 통신 255로 동기화
|
|
if Assigned(DM) and DM.fdConnNilm.Connected then
|
|
begin
|
|
Q := TFDQuery.Create(nil);
|
|
try
|
|
Q.Connection := DM.fdConnNilm;
|
|
Q.SQL.Text := 'UPDATE nilm_data SET op_status = ''0'', comm_status = 255 WHERE device_id = :did';
|
|
Q.ParamByName('did').AsInteger := ADeviceID;
|
|
Q.ExecSQL;
|
|
except
|
|
end;
|
|
Q.Free;
|
|
end;
|
|
|
|
Log(reMainLog, 'data', Format('[장비 %d (%s)] 1분 이상 통신 단절 감지 -> 전원꺼짐(0) / COMM=255 자동 전환', [ADeviceID, NilmName]));
|
|
end;
|
|
|
|
function TfMain.gridmonitorRefresh_main(ADeviceID, ASeq, APreSeq, ACommStatus: Integer;
|
|
const APayloadJSON, AOpStatus: string; AData: TJSONObject; ASensorList: TNILMSensorConfigList): Boolean;
|
|
|
|
function GetNum(const AKey: string): Double;
|
|
var JV: TJSONValue;
|
|
begin
|
|
Result := 0;
|
|
JV := AData.GetValue(AKey);
|
|
if Assigned(JV) then
|
|
Result := StrToFloatDef(JV.Value, 0);
|
|
end;
|
|
|
|
function GetStr(const AKey, ADefault: string): string;
|
|
var JV: TJSONValue;
|
|
begin
|
|
Result := ADefault;
|
|
JV := AData.GetValue(AKey);
|
|
if Assigned(JV) then
|
|
Result := JV.Value;
|
|
end;
|
|
|
|
function GetJsonKey(const AFieldKey, ADefault: string): string;
|
|
var SC: TNILMSensorConfig;
|
|
begin
|
|
Result := ADefault;
|
|
if Assigned(ASensorList) then
|
|
for SC in ASensorList do
|
|
if SameText(SC.FieldKey, AFieldKey) then
|
|
begin
|
|
if SC.JsonKey <> '' then
|
|
Result := SC.JsonKey;
|
|
Exit;
|
|
end;
|
|
end;
|
|
|
|
var
|
|
row_ck : Integer;
|
|
wireVal: string;
|
|
is3P3W : Boolean;
|
|
begin
|
|
Result := False;
|
|
// row 찾기
|
|
row_ck := 0;
|
|
for var r := 1 to grdMQTTMonitor.RowCount - 1 do
|
|
begin
|
|
if grdMQTTMonitor.Cells[0, r] = ADeviceID.ToString then
|
|
begin
|
|
row_ck := r;
|
|
Break;
|
|
end;
|
|
end;
|
|
|
|
// 없으면 신규 row 추가
|
|
if row_ck = 0 then
|
|
begin
|
|
if Trim(grdMQTTMonitor.Cells[ 0, 1])='' then begin
|
|
row_ck := 1;
|
|
end
|
|
else begin
|
|
row_ck := grdMQTTMonitor.RowCount;
|
|
grdMQTTMonitor.RowCount := grdMQTTMonitor.RowCount + 1;
|
|
end;
|
|
grdMQTTMonitor.Cells[ 0, row_ck] := ADeviceID.ToString;
|
|
end;
|
|
|
|
wireVal := GetStr('wire', PHASE_3P4W);
|
|
is3P3W := SameText(wireVal, PHASE_3P3W);
|
|
|
|
var bSeqChanged: Boolean := (grdMQTTMonitor.Cells[ 2, row_ck] <> ASeq.ToString) or (ASeq = 0);
|
|
Result := bSeqChanged;
|
|
|
|
grdMQTTMonitor.Cells[ 0, row_ck] := ADeviceID.ToString;
|
|
grdMQTTMonitor.Cells[ 1, row_ck] := FormatDateTime('yyyymmdd hhnnss', now);
|
|
grdMQTTMonitor.Cells[ 2, row_ck] := ASeq.ToString;
|
|
grdMQTTMonitor.Cells[ 3, row_ck] := wireVal;
|
|
|
|
var statusStr: string := '???';
|
|
if AOpStatus = '2' then statusStr := '가동(2)'
|
|
else if AOpStatus = '1' then statusStr := '미가동(1)'
|
|
else if AOpStatus = '0' then statusStr := '전원꺼짐(0)';
|
|
grdMQTTMonitor.Cells[ 4, row_ck] := statusStr;
|
|
|
|
// RSSI
|
|
grdMQTTMonitor.Cells[ 5, row_ck] := Format('%ddBm', [Trunc(GetNum('rssi'))]);
|
|
|
|
// Total W, Total PF
|
|
grdMQTTMonitor.Cells[ 6, row_ck] := FormatFloat('#,##0.0', GetNum('total_p'));
|
|
grdMQTTMonitor.Cells[ 7, row_ck] := FormatFloat('0.000', GetNum('total_pf'));
|
|
|
|
// A상 (ch[0])
|
|
grdMQTTMonitor.Cells[ 8, row_ck] := FormatFloat('0.000', GetNum('power_factor_a'));
|
|
grdMQTTMonitor.Cells[ 9, row_ck] := FormatFloat('0.0', GetNum('vrms_a'));
|
|
grdMQTTMonitor.Cells[10, row_ck] := FormatFloat('0.00', GetNum('irms_a'));
|
|
grdMQTTMonitor.Cells[11, row_ck] := FormatFloat('0.0', GetNum('active_power_a'));
|
|
grdMQTTMonitor.Cells[12, row_ck] := FormatFloat('0.0', GetNum('reactive_power_a'));
|
|
grdMQTTMonitor.Cells[13, row_ck] := FormatFloat('0.0', GetNum('apparent_power_a'));
|
|
|
|
// B상 (ch[1]) - 3P3W일 경우 미사용 표시
|
|
if is3P3W then
|
|
begin
|
|
grdMQTTMonitor.Cells[14, row_ck] := '-';
|
|
grdMQTTMonitor.Cells[15, row_ck] := '-';
|
|
grdMQTTMonitor.Cells[16, row_ck] := '-';
|
|
grdMQTTMonitor.Cells[17, row_ck] := '-';
|
|
grdMQTTMonitor.Cells[18, row_ck] := '-';
|
|
grdMQTTMonitor.Cells[19, row_ck] := '-';
|
|
end
|
|
else
|
|
begin
|
|
grdMQTTMonitor.Cells[14, row_ck] := FormatFloat('0.000', GetNum('power_factor_b'));
|
|
grdMQTTMonitor.Cells[15, row_ck] := FormatFloat('0.0', GetNum('vrms_b'));
|
|
grdMQTTMonitor.Cells[16, row_ck] := FormatFloat('0.00', GetNum('irms_b'));
|
|
grdMQTTMonitor.Cells[17, row_ck] := FormatFloat('0.0', GetNum('active_power_b'));
|
|
grdMQTTMonitor.Cells[18, row_ck] := FormatFloat('0.0', GetNum('reactive_power_b'));
|
|
grdMQTTMonitor.Cells[19, row_ck] := FormatFloat('0.0', GetNum('apparent_power_b'));
|
|
end;
|
|
|
|
// C상 (ch[2])
|
|
grdMQTTMonitor.Cells[20, row_ck] := FormatFloat('0.000', GetNum('power_factor_c'));
|
|
grdMQTTMonitor.Cells[21, row_ck] := FormatFloat('0.0', GetNum('vrms_c'));
|
|
grdMQTTMonitor.Cells[22, row_ck] := FormatFloat('0.00', GetNum('irms_c'));
|
|
grdMQTTMonitor.Cells[23, row_ck] := FormatFloat('0.0', GetNum('active_power_c'));
|
|
grdMQTTMonitor.Cells[24, row_ck] := FormatFloat('0.0', GetNum('reactive_power_c'));
|
|
grdMQTTMonitor.Cells[25, row_ck] := FormatFloat('0.0', GetNum('apparent_power_c'));
|
|
|
|
// 누적 전력량 (wh)
|
|
grdMQTTMonitor.Cells[26, row_ck] := FormatFloat('#,##0', GetNum('wh'));
|
|
|
|
// 당일 누적 전력량 갱신 (1분마다 1번씩만 DB 쿼리하여 부하 방지)
|
|
var LastUpdate: TDateTime;
|
|
if not FLastEnergyUpdate.TryGetValue(ADeviceID, LastUpdate) or (Now - LastUpdate >= 1.0 / 1440.0) then
|
|
begin
|
|
var E1, E2, E3: Double;
|
|
FNILMManager.CalculateEnergyConsumption(ADeviceID, Date, Now, E1, E2, E3);
|
|
var EnergyWh: Double := E1 + E2 + E3;
|
|
FDayEnergyValue.AddOrSetValue(ADeviceID, EnergyWh);
|
|
FLastEnergyUpdate.AddOrSetValue(ADeviceID, Now);
|
|
end;
|
|
|
|
if FDayEnergyValue.ContainsKey(ADeviceID) then
|
|
grdMQTTMonitor.Cells[27, row_ck] := FormatFloat('#,##0.0', FDayEnergyValue[ADeviceID])
|
|
else
|
|
grdMQTTMonitor.Cells[27, row_ck] := '-';
|
|
end;
|
|
{
|
|
procedure TfMain.gridmonitorRefresh_main(const S: string);
|
|
var
|
|
row_ck, i, x : Integer;
|
|
JSONObject : TJSONObject;
|
|
JSONArray : TJSONArray;
|
|
ItemObj : TJSONObject;
|
|
Item_NewCk, Item_UpdateCk : Boolean;
|
|
Item_idindex : integer;
|
|
CK_id : string;
|
|
NILMItemTemp : TNILMItem;
|
|
begin
|
|
try
|
|
JSONObject := TJSONObject.ParseJSONValue(S) as TJSONObject;
|
|
|
|
try
|
|
// fMain.grdMQTTMonitor.BeginUpdate;
|
|
if JSONObject.GetValue('slave_data') <> nil then
|
|
begin
|
|
JSONArray := JSONObject.GetValue<TJSONArray>('slave_data');
|
|
for i := 0 to JSONArray.Count - 1 do
|
|
begin
|
|
ItemObj := JSONArray.Items[i] as TJSONObject;
|
|
Item_NewCk := True; Item_UpdateCk := True;
|
|
Item_idindex := -1;
|
|
CK_id := ItemObj.GetValue<Integer>('id').ToString;
|
|
|
|
if BaseList_NILMItem.Count > 0 then begin
|
|
for x := 0 to (BaseList_NILMItem.Count-1) do begin
|
|
if Trim(BaseList_NILMItem[x].id) = trim(CK_id) then begin
|
|
Item_idindex := x;
|
|
Item_NewCk := False;
|
|
if BaseList_NILMItem[x].seq = ItemObj.GetValue<Integer>('seq').ToString then begin
|
|
// BaseList_NILMItem[x].lastupdatetime := FormatDateTime('yyyymmdd hhnnss',now);
|
|
Item_UpdateCk := False;
|
|
end
|
|
else Begin
|
|
BaseList_NILMItem[x].seq := ItemObj.GetValue<Integer>('seq').ToString;
|
|
BaseList_NILMItem[x].PreSeq := ItemObj.GetValue<Integer>('PreSeq').ToString;
|
|
BaseList_NILMItem[x].COMM := ItemObj.GetValue<Integer>('COMM').ToString;
|
|
BaseList_NILMItem[x].pfA := FormatFloat('0.0', ItemObj.GetValue<Double>('pfA'));
|
|
BaseList_NILMItem[x].voltageA := FormatFloat('0.0', ItemObj.GetValue<Double>('voltageA'));
|
|
BaseList_NILMItem[x].currentA := FormatFloat('0.0', ItemObj.GetValue<Double>('currentA'));
|
|
BaseList_NILMItem[x].Var_A := FormatFloat('0.0', ItemObj.GetValue<Double>('Var_A'));
|
|
BaseList_NILMItem[x].Va_A := FormatFloat('0.0', ItemObj.GetValue<Double>('Va_A'));
|
|
BaseList_NILMItem[x].pfB := FormatFloat('0.0', ItemObj.GetValue<Double>('pfB'));
|
|
BaseList_NILMItem[x].voltageB := FormatFloat('0.0', ItemObj.GetValue<Double>('voltageB'));
|
|
BaseList_NILMItem[x].currentB := FormatFloat('0.0', ItemObj.GetValue<Double>('currentB'));
|
|
BaseList_NILMItem[x].Var_B := FormatFloat('0.0', ItemObj.GetValue<Double>('Var_B'));
|
|
BaseList_NILMItem[x].Va_B := FormatFloat('0.0', ItemObj.GetValue<Double>('Va_B'));
|
|
BaseList_NILMItem[x].lastupdatetime := FormatDateTime('yyyymmdd hhnnss',now);
|
|
BaseList_NILMItem[x].nilmstatus := '?????';
|
|
|
|
// BaseList_NILMItem[x].mqttTopic := '';
|
|
Item_UpdateCk := True;
|
|
End;
|
|
Break;
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
if Item_NewCk then begin
|
|
NILMItemTemp := TNILMItem.Create;
|
|
|
|
NILMItemTemp.id := ItemObj.GetValue<Integer>('id').ToString;
|
|
NILMItemTemp.seq := ItemObj.GetValue<Integer>('seq').ToString;
|
|
NILMItemTemp.PreSeq := ItemObj.GetValue<Integer>('PreSeq').ToString;
|
|
NILMItemTemp.COMM := ItemObj.GetValue<Integer>('COMM').ToString;
|
|
NILMItemTemp.pfA := FormatFloat('0.0', ItemObj.GetValue<Double>('pfA'));
|
|
NILMItemTemp.voltageA := FormatFloat('0.0', ItemObj.GetValue<Double>('voltageA'));
|
|
NILMItemTemp.currentA := FormatFloat('0.0', ItemObj.GetValue<Double>('currentA'));
|
|
NILMItemTemp.Var_A := FormatFloat('0.0', ItemObj.GetValue<Double>('Var_A'));
|
|
NILMItemTemp.Va_A := FormatFloat('0.0', ItemObj.GetValue<Double>('Va_A'));
|
|
NILMItemTemp.pfB := FormatFloat('0.0', ItemObj.GetValue<Double>('pfB'));
|
|
NILMItemTemp.voltageB := FormatFloat('0.0', ItemObj.GetValue<Double>('voltageB'));
|
|
NILMItemTemp.currentB := FormatFloat('0.0', ItemObj.GetValue<Double>('currentB'));
|
|
NILMItemTemp.Var_B := FormatFloat('0.0', ItemObj.GetValue<Double>('Var_B'));
|
|
NILMItemTemp.Va_B := FormatFloat('0.0', ItemObj.GetValue<Double>('Va_B'));
|
|
NILMItemTemp.lastupdatetime := FormatDateTime('yyyymmdd hhnnss',now);
|
|
NILMItemTemp.nilmstatus := '?????';
|
|
|
|
NILMItemTemp.delaycount := '';
|
|
NILMItemTemp.statusCheck_off := '';
|
|
NILMItemTemp.statusCheck_wait := '';
|
|
NILMItemTemp.statusCheck_drive := '';
|
|
|
|
// NILMItem.mqttTopic := '';
|
|
|
|
BaseList_NILMItem.Add(NILMItemTemp);
|
|
end;
|
|
|
|
// row 찾기
|
|
row_ck := 0;
|
|
|
|
for var r := 1 to fMain.grdMQTTMonitor.RowCount - 1 do
|
|
begin
|
|
if fMain.grdMQTTMonitor.Cells[0, r] = ItemObj.GetValue<Integer>('id').ToString then
|
|
begin
|
|
row_ck := r;
|
|
Break;
|
|
end;
|
|
end;
|
|
|
|
// 없으면 신규 row 추가
|
|
if row_ck = 0 then
|
|
begin
|
|
if Trim(fMain.grdMQTTMonitor.Cells[ 0, 1])='' then begin
|
|
row_ck := 1;
|
|
end
|
|
else begin
|
|
row_ck := fMain.grdMQTTMonitor.RowCount;
|
|
fMain.grdMQTTMonitor.RowCount := fMain.grdMQTTMonitor.RowCount + 1;
|
|
end;
|
|
fMain.grdMQTTMonitor.Cells[ 0, row_ck] := ItemObj.GetValue<Integer>('id').ToString;
|
|
end;
|
|
|
|
if Item_UpdateCk then begin
|
|
fMain.grdMQTTMonitor.Cells[ 1, row_ck] := FormatDateTime('yyyymmdd hhnnss',now);
|
|
fMain.grdMQTTMonitor.Cells[ 2, row_ck] := ItemObj.GetValue<Integer>('seq').ToString;
|
|
fMain.grdMQTTMonitor.Cells[ 3, row_ck] := ItemObj.GetValue<Integer>('PreSeq').ToString;
|
|
// fMain.grdMQTTMonitor.Cells[ 4, row_ck] := ItemObj.GetValue<Integer>('COMM').ToString;
|
|
fMain.grdMQTTMonitor.Cells[ 4, row_ck] := '???';
|
|
fMain.grdMQTTMonitor.Cells[ 5, row_ck] := FormatFloat('0.0', ItemObj.GetValue<Double>('pfA'));
|
|
fMain.grdMQTTMonitor.Cells[ 6, row_ck] := FormatFloat('0.0', ItemObj.GetValue<Double>('voltageA'));
|
|
fMain.grdMQTTMonitor.Cells[ 7, row_ck] := FormatFloat('0.0', ItemObj.GetValue<Double>('currentA'));
|
|
fMain.grdMQTTMonitor.Cells[ 8, row_ck] := FormatFloat('0.0', ItemObj.GetValue<Double>('Var_A'));
|
|
fMain.grdMQTTMonitor.Cells[ 9, row_ck] := FormatFloat('0.0', ItemObj.GetValue<Double>('Va_A'));
|
|
fMain.grdMQTTMonitor.Cells[10, row_ck] := FormatFloat('0.0', ItemObj.GetValue<Double>('pfB'));
|
|
fMain.grdMQTTMonitor.Cells[11, row_ck] := FormatFloat('0.0', ItemObj.GetValue<Double>('voltageB'));
|
|
fMain.grdMQTTMonitor.Cells[12, row_ck] := FormatFloat('0.0', ItemObj.GetValue<Double>('currentB'));
|
|
fMain.grdMQTTMonitor.Cells[13, row_ck] := FormatFloat('0.0', ItemObj.GetValue<Double>('Var_B'));
|
|
fMain.grdMQTTMonitor.Cells[14, row_ck] := FormatFloat('0.0', ItemObj.GetValue<Double>('Va_B'));
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
finally
|
|
// fMain.grdMQTTMonitor.EndUpdate;
|
|
JSONObject.Free;
|
|
end;
|
|
|
|
except
|
|
on e : Exception do
|
|
begin
|
|
fMain.Log(fMain.reMQTTLog,'error',e.Message);
|
|
end;
|
|
end;
|
|
end;
|
|
}
|
|
|
|
procedure TfMain.OnMQTTMessage(const ATopic, APayload: string);
|
|
begin
|
|
TThread.ForceQueue(nil,
|
|
procedure
|
|
begin
|
|
ProcessIncomingMQTTMessage(ATopic, APayload);
|
|
end);
|
|
end;
|
|
|
|
procedure TfMain.ProcessIncomingMQTTMessage(const ATopic, APayload: string);
|
|
begin
|
|
if ATopic.EndsWith('/gateway') then
|
|
ProcessGatewayMessage(ATopic, APayload)
|
|
else if ATopic.EndsWith('/status') then
|
|
ProcessNodeStatusMessage(ATopic, APayload)
|
|
else if ATopic.EndsWith('/event') then
|
|
ProcessEventMessage(ATopic, APayload)
|
|
else
|
|
NILM_SaveMQTTData(ATopic, APayload);
|
|
end;
|
|
|
|
procedure TfMain.ProcessGatewayMessage(const ATopic, APayload: string);
|
|
var
|
|
Root: TJSONObject;
|
|
GW: TNILMGatewayPayload;
|
|
begin
|
|
GW.Clear;
|
|
Root := TJSONObject.ParseJSONValue(APayload) as TJSONObject;
|
|
if Root = nil then Exit;
|
|
try
|
|
var jvGW := Root.GetValue('gw');
|
|
if Assigned(jvGW) and (not (jvGW is TJSONNull)) then GW.gw := jvGW.Value;
|
|
var jvOnline := Root.GetValue('online');
|
|
if Assigned(jvOnline) and (not (jvOnline is TJSONNull)) then GW.online := SameText(jvOnline.Value, 'true');
|
|
var jvTot := Root.GetValue('nodes_total');
|
|
if Assigned(jvTot) and (not (jvTot is TJSONNull)) then GW.nodes_total := StrToIntDef(jvTot.Value, 0);
|
|
var jvOnl := Root.GetValue('nodes_online');
|
|
if Assigned(jvOnl) and (not (jvOnl is TJSONNull)) then GW.nodes_online := StrToIntDef(jvOnl.Value, 0);
|
|
var jvCyc := Root.GetValue('cycle_ms');
|
|
if Assigned(jvCyc) and (not (jvCyc is TJSONNull)) then GW.cycle_ms := StrToIntDef(jvCyc.Value, 0);
|
|
var jvUpt := Root.GetValue('uptime_s');
|
|
if Assigned(jvUpt) and (not (jvUpt is TJSONNull)) then GW.uptime_s := StrToInt64Def(jvUpt.Value, 0);
|
|
|
|
if Assigned(FNILMManager) then
|
|
FNILMManager.SaveGatewayStatus(GW);
|
|
|
|
Log(reMQTTLog, 'data', Format('[Gateway] %s (온라인: %s, 노드: %d/%d, 주기: %dms, 가동시간: %ds)',
|
|
[GW.gw, BoolToStr(GW.online, True), GW.nodes_online, GW.nodes_total, GW.cycle_ms, GW.uptime_s]));
|
|
finally
|
|
Root.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.ProcessNodeStatusMessage(const ATopic, APayload: string);
|
|
var
|
|
TopicParts: TArray<string>;
|
|
DeviceID: Integer;
|
|
Root: TJSONObject;
|
|
Status: TNILMNodeStatusPayload;
|
|
begin
|
|
TopicParts := ATopic.Split(['/']);
|
|
if Length(TopicParts) < 2 then Exit;
|
|
DeviceID := StrToIntDef(TopicParts[High(TopicParts) - 1], 0);
|
|
if DeviceID <= 0 then Exit;
|
|
|
|
Status.Clear;
|
|
Root := TJSONObject.ParseJSONValue(APayload) as TJSONObject;
|
|
if Root = nil then Exit;
|
|
try
|
|
var jvGW := Root.GetValue('gw');
|
|
if Assigned(jvGW) and (not (jvGW is TJSONNull)) then Status.gw := jvGW.Value;
|
|
var jvDev := Root.GetValue('dev');
|
|
if Assigned(jvDev) and (not (jvDev is TJSONNull)) then Status.dev := jvDev.Value;
|
|
var jvOnline := Root.GetValue('online');
|
|
if Assigned(jvOnline) and (not (jvOnline is TJSONNull)) then Status.online := SameText(jvOnline.Value, 'true');
|
|
var jvAge := Root.GetValue('last_seen_age_s');
|
|
if Assigned(jvAge) and (not (jvAge is TJSONNull)) then Status.last_seen_age_s := StrToIntDef(jvAge.Value, 0);
|
|
var jvLoss := Root.GetValue('loss_rate');
|
|
if Assigned(jvLoss) and (not (jvLoss is TJSONNull)) then Status.loss_rate := StrToFloatDef(jvLoss.Value, 0.0);
|
|
var jvPolls := Root.GetValue('polls');
|
|
if Assigned(jvPolls) and (not (jvPolls is TJSONNull)) then Status.polls := StrToIntDef(jvPolls.Value, 0);
|
|
var jvResps := Root.GetValue('responses');
|
|
if Assigned(jvResps) and (not (jvResps is TJSONNull)) then Status.responses := StrToIntDef(jvResps.Value, 0);
|
|
|
|
if Assigned(FNILMManager) then
|
|
FNILMManager.SaveNodeStatus(DeviceID, Status);
|
|
|
|
Log(reMQTTLog, 'data', Format('[노드상태] 장비 %d [%s]: %s (손실률: %.2f%%, 지연: %ds)',
|
|
[DeviceID, Status.dev, IfThen(Status.online, 'ONLINE', 'OFFLINE'), Status.loss_rate * 100, Status.last_seen_age_s]));
|
|
finally
|
|
Root.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.ProcessEventMessage(const ATopic, APayload: string);
|
|
var
|
|
TopicParts: TArray<string>;
|
|
DeviceID: Integer;
|
|
Root: TJSONObject;
|
|
Evt: TNILMEventPayload;
|
|
begin
|
|
TopicParts := ATopic.Split(['/']);
|
|
if Length(TopicParts) < 2 then Exit;
|
|
DeviceID := StrToIntDef(TopicParts[High(TopicParts) - 1], 0);
|
|
|
|
Evt.Clear;
|
|
Root := TJSONObject.ParseJSONValue(APayload) as TJSONObject;
|
|
if Root = nil then Exit;
|
|
try
|
|
var jvGW := Root.GetValue('gw');
|
|
if Assigned(jvGW) and (not (jvGW is TJSONNull)) then Evt.gw := jvGW.Value;
|
|
var jvDev := Root.GetValue('dev');
|
|
if Assigned(jvDev) and (not (jvDev is TJSONNull)) then Evt.dev := jvDev.Value;
|
|
var jvAge := Root.GetValue('age_ms');
|
|
if Assigned(jvAge) and (not (jvAge is TJSONNull)) then Evt.age_ms := StrToIntDef(jvAge.Value, 0);
|
|
var jvCode := Root.GetValue('code');
|
|
if Assigned(jvCode) and (not (jvCode is TJSONNull)) then Evt.code := StrToIntDef(jvCode.Value, 0);
|
|
var jvName := Root.GetValue('name');
|
|
if Assigned(jvName) and (not (jvName is TJSONNull)) then Evt.name := jvName.Value;
|
|
var jvSeq := Root.GetValue('seq');
|
|
if Assigned(jvSeq) and (not (jvSeq is TJSONNull)) then Evt.seq := StrToIntDef(jvSeq.Value, 0);
|
|
var jvVal := Root.GetValue('value');
|
|
if Assigned(jvVal) and (not (jvVal is TJSONNull)) then Evt.value := StrToFloatDef(jvVal.Value, 0.0);
|
|
|
|
if Assigned(FNILMManager) then
|
|
FNILMManager.SaveNILMEvent(DeviceID, Evt);
|
|
|
|
Log(reMainLog, 'alarm', Format('[ALARM] 장비 %d (%s) 이벤트: %s (코드:%d, 값:%.2f, 지연:%dms)',
|
|
[DeviceID, Evt.dev, Evt.name, Evt.code, Evt.value, Evt.age_ms]));
|
|
finally
|
|
Root.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.Log(Target: TRichEdit; LogType: string; Msg: string);
|
|
begin
|
|
if cbLogSave.Checked then AddLog_Thread(Msg);
|
|
|
|
if cbLogView.Checked then begin
|
|
|
|
if ((LogType = 'data') and (Target=reMainLog) and (not cbDataLogVIew.Checked)) or
|
|
((LogType = 'mqttmsg') and (Target=reMainLog) and (not cbDataLogVIew.Checked)) then exit;
|
|
|
|
if (LogType = 'data') and (Target=reMqttlog) and (not cbMqttLogVIew.Checked) then exit;
|
|
if (LogType = 'data') and (Target=reDblog) and (not cbDbLogVIew.Checked) then exit;
|
|
if (LogType = 'data') and (Target=reOPClog) and (not cbOPCLogVIew.Checked) then exit;
|
|
|
|
TThread.ForceQueue(nil, procedure
|
|
begin
|
|
if not Assigned(Target) then Exit;
|
|
// 로그 라인 제한 (100줄)
|
|
if Target.Lines.Count > StrToIntDef(etdMaxRow.Text, 100) then
|
|
Target.Clear;
|
|
|
|
if LogType = 'process' then Target.SelAttributes.Color := clLime
|
|
else if LogType = 'complete' then Target.SelAttributes.Color := $0064B1FF
|
|
else if LogType = 'error' then Target.SelAttributes.Color := clRed
|
|
else if LogType = 'data' then Target.SelAttributes.Color := clSkyBlue
|
|
else if LogType = 'mqttmsg' then Target.SelAttributes.Color := clSkyBlue
|
|
else if LogType = 'api' then Target.SelAttributes.Color := clYellow
|
|
else if LogType = 'alarm' then Target.SelAttributes.Color := clYellow
|
|
else Target.SelAttributes.Color := clWhite;
|
|
|
|
if (not cbFullLengthSet.Checked) and
|
|
(Length(Msg) > StrToIntDef(etdMaxLength.Text, 100)) then begin
|
|
Target.Lines.Add(Format('[%s] %s', [FormatDateTime('mm-dd HH:mm:ss', Now), Copy(Msg, 1, StrToIntDef(etdMaxLength.Text, 100)) + '......']));
|
|
end
|
|
else begin
|
|
Target.Lines.Add(Format('[%s] %s', [FormatDateTime('mm-dd HH:mm:ss', Now), Msg]));
|
|
end;
|
|
|
|
Target.Perform(EM_SCROLL, SB_BOTTOM, 0);
|
|
end);
|
|
end;
|
|
end;
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// NILM 설정 탭 구현
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
procedure TfMain.NILM_ClearDeviceList;
|
|
var
|
|
D: TNILMDevice;
|
|
begin
|
|
if Assigned(FDeviceList) then
|
|
begin
|
|
for D in FDeviceList do
|
|
if Assigned(D.SensorList) then
|
|
D.SensorList.Free;
|
|
FreeAndNil(FDeviceList);
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.NILM_InitGrid;
|
|
begin
|
|
if not Assigned(grdNILMDevice) then Exit;
|
|
|
|
grdNILMDevice.ColCount := 11;
|
|
grdNILMDevice.RowCount := 2;
|
|
grdNILMDevice.FixedRows := 1;
|
|
grdNILMDevice.FixedCols := 0;
|
|
grdNILMDevice.Options := grdNILMDevice.Options + [goRowSelect, goColSizing];
|
|
grdNILMDevice.DefaultRowHeight := 24;
|
|
|
|
grdNILMDevice.ColWidths[0] := 60; // 장비번호
|
|
grdNILMDevice.ColWidths[1] := 100; // 장비명
|
|
grdNILMDevice.ColWidths[2] := 120; // 위치
|
|
grdNILMDevice.ColWidths[3] := 60; // 상수
|
|
grdNILMDevice.ColWidths[4] := 60; // 대상상
|
|
grdNILMDevice.ColWidths[5] := 100; // 꺼짐기준전류
|
|
grdNILMDevice.ColWidths[6] := 100; // 가동기준전류
|
|
grdNILMDevice.ColWidths[7] := 100; // 가동기준PF
|
|
grdNILMDevice.ColWidths[8] := 50; // 활성
|
|
grdNILMDevice.ColWidths[9] := 250; // MQTT 토픽
|
|
grdNILMDevice.ColWidths[10] := 100; // 등록일
|
|
|
|
grdNILMDevice.Cells[0, 0] := '장비번호';
|
|
grdNILMDevice.Cells[1, 0] := '장비명칭';
|
|
grdNILMDevice.Cells[2, 0] := '설치위치';
|
|
grdNILMDevice.Cells[3, 0] := '상수';
|
|
grdNILMDevice.Cells[4, 0] := '대상 상';
|
|
grdNILMDevice.Cells[5, 0] := '꺼짐기준전류(A)';
|
|
grdNILMDevice.Cells[6, 0] := '가동기준전류(A)';
|
|
grdNILMDevice.Cells[7, 0] := '가동기준PF';
|
|
grdNILMDevice.Cells[8, 0] := '활성';
|
|
grdNILMDevice.Cells[9, 0] := 'MQTT 토픽';
|
|
grdNILMDevice.Cells[10, 0] := '등록일시';
|
|
end;
|
|
|
|
procedure TfMain.NILM_LoadGrid;
|
|
var
|
|
D : TNILMDevice;
|
|
Row : Integer;
|
|
begin
|
|
if not Assigned(grdNILMDevice) then Exit;
|
|
if not Assigned(FNILMManager) then FNILMManager := TNILMManager.Create;
|
|
|
|
NILM_ClearDeviceList;
|
|
try
|
|
FDeviceList := FNILMManager.LoadDevices;
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
Log(reDBLog, 'error', '[NILM] 장비 목록 로드 오류: ' + E.Message);
|
|
FDeviceList := TList<TNILMDevice>.Create;
|
|
end;
|
|
end;
|
|
|
|
grdNILMDevice.RowCount := Max(2, FDeviceList.Count + 1);
|
|
Row := 1;
|
|
for D in FDeviceList do
|
|
begin
|
|
grdNILMDevice.Cells[0, Row] := IntToStr(D.DeviceID);
|
|
grdNILMDevice.Cells[1, Row] := D.DeviceName;
|
|
grdNILMDevice.Cells[2, Row] := D.Location;
|
|
grdNILMDevice.Cells[3, Row] := D.PhaseType;
|
|
grdNILMDevice.Cells[4, Row] := D.OpTargetPhase;
|
|
grdNILMDevice.Cells[5, Row] := Format('%.2f', [D.OpThresholdOffCurrent]);
|
|
grdNILMDevice.Cells[6, Row] := Format('%.2f', [D.OpThresholdRunCurrent]);
|
|
grdNILMDevice.Cells[7, Row] := Format('%.2f', [D.OpThresholdPF]);
|
|
grdNILMDevice.Cells[8, Row] := IfThen(D.IsActive, '✔', '─');
|
|
grdNILMDevice.Cells[9, Row] := D.MqttTopic;
|
|
grdNILMDevice.Cells[10, Row] := FormatDateTime('yy-mm-dd hh:nn', D.CreatedAt);
|
|
Inc(Row);
|
|
end;
|
|
|
|
Log(reDBLog, 'process', Format('[NILM] 장비 %d 건 로드 완료', [FDeviceList.Count]));
|
|
|
|
// 콤보박스 동기화 (히스토리/이벤트 탭)
|
|
if Assigned(cmbHistDevice) then Hist_LoadDeviceCombo(cmbHistDevice);
|
|
if Assigned(cmbEvtDevice) then Hist_LoadDeviceCombo(cmbEvtDevice);
|
|
if Assigned(cmbTrendDevice) then Hist_LoadDeviceCombo(cmbTrendDevice, False);
|
|
|
|
end;
|
|
|
|
function TfMain.NILM_SelectedDeviceID: Integer;
|
|
var
|
|
Row: Integer;
|
|
begin
|
|
Result := -1;
|
|
if not Assigned(grdNILMDevice) then Exit;
|
|
Row := grdNILMDevice.Row;
|
|
if Row < 1 then Exit;
|
|
Result := StrToIntDef(grdNILMDevice.Cells[0, Row], -1);
|
|
end;
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// 히스토리 탭 구현
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
procedure TfMain.Hist_LoadDeviceCombo(ACombo: TComboBox; TotalUse:Boolean = True);
|
|
var
|
|
Q: TFDQuery;
|
|
begin
|
|
ACombo.Items.Clear;
|
|
if TotalUse then begin
|
|
ACombo.Items.Add('(전체)');
|
|
ACombo.ItemIndex := 0;
|
|
end;
|
|
|
|
if not Assigned(FNILMManager) then FNILMManager := TNILMManager.Create;
|
|
try
|
|
Q := FNILMManager.LoadDevicesSimple;
|
|
try
|
|
while not Q.EOF do
|
|
begin
|
|
ACombo.Items.Add(
|
|
Format('[%d] %s', [Q.Fields[0].AsInteger, Q.Fields[1].AsString]));
|
|
Q.Next;
|
|
end;
|
|
if not TotalUse then begin
|
|
if ACombo.Items.Count > 0 then ACombo.ItemIndex := 0;
|
|
end;
|
|
finally
|
|
Q.Free;
|
|
end;
|
|
except
|
|
// DB 미연결 시 무시
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.Hist_InitGrid;
|
|
const
|
|
COLS: array[0..31] of string = (
|
|
'수신시각','장비번호','SEQ','이전SEQ','COMM',
|
|
'결선','RSSI','Total_W','Total_PF',
|
|
'pf_a','vrms_a','irms_a','w_a','var_a','va_a',
|
|
'pf_b','vrms_b','irms_b','w_b','var_b','va_b',
|
|
'pf_c','vrms_c','irms_c','w_c','var_c','va_c',
|
|
'wh','내부온도','외부온도','진동','기울기');
|
|
var i: Integer;
|
|
begin
|
|
if not Assigned(grdHistory) then Exit;
|
|
grdHistory.ColCount := Length(COLS);
|
|
grdHistory.RowCount := 2;
|
|
grdHistory.FixedRows := 1;
|
|
grdHistory.FixedCols := 0;
|
|
grdHistory.Options := grdHistory.Options + [goRowSelect, goColSizing];
|
|
grdHistory.DefaultRowHeight := 22;
|
|
for i := 0 to High(COLS) do
|
|
begin
|
|
grdHistory.ColWidths[i] := IfThen(i = 0, 140, IfThen(i < 5, 60, 62));
|
|
grdHistory.Cells[i, 0] := COLS[i];
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.Hist_InitEventGrid;
|
|
const
|
|
COLS: array[0..4] of string = ('발생시각','유형','장비번호','메시지','상세내용');
|
|
var i: Integer;
|
|
begin
|
|
if not Assigned(grdEventLog) then Exit;
|
|
grdEventLog.ColCount := Length(COLS);
|
|
grdEventLog.RowCount := 2;
|
|
grdEventLog.FixedRows := 1;
|
|
grdEventLog.FixedCols := 0;
|
|
grdEventLog.Options := grdEventLog.Options + [goRowSelect, goColSizing];
|
|
grdEventLog.DefaultRowHeight := 22;
|
|
grdEventLog.ColWidths[0] := 140;
|
|
grdEventLog.ColWidths[1] := 90;
|
|
grdEventLog.ColWidths[2] := 70;
|
|
grdEventLog.ColWidths[3] := 300;
|
|
grdEventLog.ColWidths[4] := 300;
|
|
for i := 0 to High(COLS) do
|
|
grdEventLog.Cells[i, 0] := COLS[i];
|
|
end;
|
|
|
|
procedure TfMain.Hist_ExportGridToCSV(AGrid: TStringGrid; const ATitle: string);
|
|
var
|
|
Dlg : TSaveDialog;
|
|
F : TextFile;
|
|
Row, Col: Integer;
|
|
Line : string;
|
|
begin
|
|
Dlg := TSaveDialog.Create(nil);
|
|
try
|
|
Dlg.Title := ATitle + ' CSV 내보내기';
|
|
Dlg.Filter := 'CSV 파일 (*.csv)|*.csv';
|
|
Dlg.DefaultExt := 'csv';
|
|
Dlg.FileName := ATitle + '_' + FormatDateTime('yyyymmdd_hhnn', Now) + '.csv';
|
|
if not Dlg.Execute then Exit;
|
|
|
|
AssignFile(F, Dlg.FileName);
|
|
Rewrite(F);
|
|
try
|
|
for Row := 0 to AGrid.RowCount - 1 do
|
|
begin
|
|
Line := '';
|
|
for Col := 0 to AGrid.ColCount - 1 do
|
|
begin
|
|
if Col > 0 then Line := Line + ',';
|
|
Line := Line + '"' + StringReplace(AGrid.Cells[Col, Row], '"', '""', [rfReplaceAll]) + '"';
|
|
end;
|
|
Writeln(F, Line);
|
|
end;
|
|
finally
|
|
CloseFile(F);
|
|
end;
|
|
MessageDlg('CSV 내보내기 완료: ' + Dlg.FileName, mtInformation, [mbOK], 0);
|
|
finally
|
|
Dlg.Free;
|
|
end;
|
|
end;
|
|
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
// MQTT → DB 자동 저장 & OPC 쓰기
|
|
// ══════════════════════════════════════════════════════════════════════════
|
|
|
|
function TfMain.WriteOPCValueIfChanged(const ATagName: string; const AValue: string): Integer;
|
|
var
|
|
PrevVal: string;
|
|
Item: OLEVariant;
|
|
targetVT: Integer;
|
|
ItemValues: OLEVariant;
|
|
inputText: string;
|
|
begin
|
|
Result := 0; // Default: 변경없음
|
|
|
|
if FPreviousOPCValues.TryGetValue(ATagName, PrevVal) then
|
|
begin
|
|
if PrevVal = AValue then Exit; // Value hasn't changed
|
|
end;
|
|
|
|
// Attempt to write to OPC
|
|
if not bConnect or VarIsEmpty(OPCGroup) or VarIsNull(OPCGroup) then Exit(2);
|
|
|
|
if not FDynamicOPCItems.TryGetValue(ATagName, Item) or VarIsNull(Item) or VarIsEmpty(Item) then
|
|
begin
|
|
try
|
|
Item := OPCGroup.OPCItems.AddItem(ATagName, FDynamicOPCItems.Count + 1000);
|
|
try
|
|
Item.IsActive := True;
|
|
except
|
|
end;
|
|
FDynamicOPCItems.AddOrSetValue(ATagName, Item);
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
Log(reOPCLog, 'error', Format('[OPC 태그등록 실패] %s (값: %s) - %s', [ATagName, AValue, E.Message]));
|
|
Exit(-1); // Error
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
if VarIsNull(Item) or VarIsEmpty(Item) then
|
|
Exit(2);
|
|
|
|
inputText := Trim(AValue);
|
|
targetVT := varEmpty;
|
|
try
|
|
targetVT := Item.CanonicalDataType;
|
|
except
|
|
targetVT := varEmpty;
|
|
end;
|
|
|
|
if (targetVT = 0) or (targetVT = varEmpty) then
|
|
begin
|
|
if SameText(ExtractFileName(StringReplace(ATagName, '/', '\', [rfReplaceAll])), 'COMM') or
|
|
SameText(ExtractFileName(StringReplace(ATagName, '/', '\', [rfReplaceAll])), 'OpStatus') or
|
|
SameText(ExtractFileName(StringReplace(ATagName, '/', '\', [rfReplaceAll])), 'wire') or
|
|
SameText(ExtractFileName(StringReplace(ATagName, '/', '\', [rfReplaceAll])), 'rssi') then
|
|
targetVT := varSmallInt
|
|
else
|
|
targetVT := varSingle;
|
|
end;
|
|
|
|
ItemValues := VarArrayCreate([0, 0], varVariant);
|
|
case targetVT of
|
|
varSmallInt,
|
|
varInteger,
|
|
varByte,
|
|
varWord,
|
|
varShortInt:
|
|
ItemValues[0] := Integer(StrToIntDef(inputText, 0));
|
|
|
|
varLongWord:
|
|
ItemValues[0] := Integer(StrToInt64Def(inputText, 0));
|
|
|
|
varInt64:
|
|
ItemValues[0] := StrToInt64Def(inputText, 0);
|
|
|
|
varSingle,
|
|
varDouble:
|
|
ItemValues[0] := Double(StrToFloatDef(inputText, 0.0));
|
|
|
|
varBoolean:
|
|
ItemValues[0] := OleVariant(Boolean((inputText <> '0') and (inputText <> '')));
|
|
|
|
varOleStr:
|
|
ItemValues[0] := OleVariant(inputText);
|
|
else
|
|
ItemValues[0] := OleVariant(inputText);
|
|
end;
|
|
|
|
try
|
|
Item.Write(ItemValues[0]);
|
|
FPreviousOPCValues.AddOrSetValue(ATagName, AValue);
|
|
Result := 1; // Success
|
|
except
|
|
on E: Exception do
|
|
begin
|
|
Result := -1; // Error
|
|
Log(reOPCLog, 'error', Format('[OPC 쓰기 실패] %s = %s (타입: %d) - %s', [ATagName, AValue, targetVT, E.Message]));
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.ClearFailedOPCTags;
|
|
var
|
|
Key: string;
|
|
KeysToRemove: TList<string>;
|
|
Item: OLEVariant;
|
|
begin
|
|
KeysToRemove := TList<string>.Create;
|
|
try
|
|
for Key in FDynamicOPCItems.Keys do
|
|
begin
|
|
if FDynamicOPCItems.TryGetValue(Key, Item) then
|
|
begin
|
|
if VarIsNull(Item) or VarIsEmpty(Item) then
|
|
KeysToRemove.Add(Key);
|
|
end;
|
|
end;
|
|
|
|
for Key in KeysToRemove do
|
|
FDynamicOPCItems.Remove(Key);
|
|
finally
|
|
KeysToRemove.Free;
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.NILM_SaveMQTTData(const ATopic, APayload: string);
|
|
var
|
|
TopicParts : TArray<string>;
|
|
DeviceID : Integer;
|
|
Root, Slave, TotObj, ChObj: TJSONObject;
|
|
SlaveArr, ChArr: TJSONArray;
|
|
Seq, Comm, Rssi: Integer;
|
|
TargetDevice: TNILMDevice;
|
|
deviceFound: Boolean;
|
|
DevSensors: TNILMSensorConfigList;
|
|
i, k: Integer;
|
|
opStatus: string;
|
|
currVal, pfVal: Double;
|
|
MeasurePayload: TNILMMeasurementPayload;
|
|
successCount, failCount, skipCount, DupCount: Integer;
|
|
res: Integer;
|
|
GridDataObj: TJSONObject;
|
|
|
|
function GetValNum(const AObj: TJSONObject; const AKey: string; ADefault: Double = 0.0): Double;
|
|
var JV: TJSONValue;
|
|
begin
|
|
Result := ADefault;
|
|
if AObj = nil then Exit;
|
|
JV := AObj.GetValue(AKey);
|
|
if Assigned(JV) and (not (JV is TJSONNull)) then
|
|
Result := StrToFloatDef(JV.Value, ADefault);
|
|
end;
|
|
|
|
function GetValInt(const AObj: TJSONObject; const AKey: string; ADefault: Integer = 0): Integer;
|
|
var JV: TJSONValue;
|
|
begin
|
|
Result := ADefault;
|
|
if AObj = nil then Exit;
|
|
JV := AObj.GetValue(AKey);
|
|
if Assigned(JV) and (not (JV is TJSONNull)) then
|
|
Result := StrToIntDef(JV.Value, ADefault);
|
|
end;
|
|
|
|
function GetValInt64(const AObj: TJSONObject; const AKey: string; ADefault: Int64 = 0): Int64;
|
|
var JV: TJSONValue;
|
|
begin
|
|
Result := ADefault;
|
|
if AObj = nil then Exit;
|
|
JV := AObj.GetValue(AKey);
|
|
if Assigned(JV) and (not (JV is TJSONNull)) then
|
|
Result := StrToInt64Def(JV.Value, ADefault);
|
|
end;
|
|
|
|
function GetValStr(const AObj: TJSONObject; const AKey, ADefault: string): string;
|
|
var JV: TJSONValue;
|
|
begin
|
|
Result := ADefault;
|
|
if AObj = nil then Exit;
|
|
JV := AObj.GetValue(AKey);
|
|
if Assigned(JV) and (not (JV is TJSONNull)) then
|
|
Result := JV.Value;
|
|
end;
|
|
|
|
procedure SendOPC(const ATagSuffix, AValStr: string);
|
|
begin
|
|
var TagName := IntToStr(DeviceID) + '/' + ATagSuffix;
|
|
res := WriteOPCValueIfChanged('SYSTEM.' + TagName, AValStr);
|
|
if res = 1 then Inc(successCount)
|
|
else if res = 2 then Inc(skipCount)
|
|
else if res = -1 then Inc(failCount)
|
|
else Inc(DupCount);
|
|
end;
|
|
|
|
begin
|
|
if not Assigned(FNILMManager) then Exit;
|
|
try
|
|
Root := TJSONObject.ParseJSONValue(APayload) as TJSONObject;
|
|
if Root = nil then Exit;
|
|
try
|
|
// 1순위: JSON 페이로드의 dev 필드에서 번호 추출 (예: "NILM_032" -> 32, "NILM_002" -> 2)
|
|
DeviceID := 0;
|
|
var devStr := GetValStr(Root, 'dev', '');
|
|
if devStr <> '' then
|
|
begin
|
|
var numStr := '';
|
|
for var ch in devStr do
|
|
if ch in ['0'..'9'] then numStr := numStr + ch;
|
|
DeviceID := StrToIntDef(numStr, 0);
|
|
end;
|
|
|
|
// 2순위: 토픽의 마지막 세그먼트에서 추출
|
|
if DeviceID <= 0 then
|
|
begin
|
|
TopicParts := ATopic.Split(['/']);
|
|
if Length(TopicParts) > 0 then
|
|
DeviceID := StrToIntDef(TopicParts[High(TopicParts)], 0);
|
|
end;
|
|
|
|
if DeviceID <= 0 then Exit;
|
|
|
|
// 수신 시각 기록 및 타임아웃 처리 플래그 리셋
|
|
if Assigned(FLastRecvTime) then
|
|
FLastRecvTime.AddOrSetValue(DeviceID, Now);
|
|
if Assigned(FDeviceTimeoutHandled) then
|
|
FDeviceTimeoutHandled.AddOrSetValue(DeviceID, False);
|
|
|
|
MeasurePayload.Clear;
|
|
|
|
// 1. 신규 규격 vs 레거시 포맷 파싱
|
|
if Root.GetValue('slave_data') <> nil then
|
|
begin
|
|
// 레거시 slave_data 구조
|
|
SlaveArr := Root.GetValue<TJSONArray>('slave_data');
|
|
if (SlaveArr = nil) or (SlaveArr.Count = 0) then Exit;
|
|
Slave := SlaveArr.Items[0] as TJSONObject;
|
|
|
|
MeasurePayload.seq := GetValInt(Slave, 'seq', 0);
|
|
MeasurePayload.COMM := GetValInt(Slave, 'COMM', 0);
|
|
MeasurePayload.wire := GetValStr(Slave, 'wire', PHASE_3P4W);
|
|
MeasurePayload.rssi := GetValInt(Slave, 'rssi', 0);
|
|
MeasurePayload.freq := GetValNum(Slave, 'frequency', 60.0);
|
|
MeasurePayload.wh := GetValInt64(Slave, 'wh', 0);
|
|
|
|
// A상
|
|
MeasurePayload.ch[0].IsValid := True;
|
|
MeasurePayload.ch[0].pf := GetValNum(Slave, 'power_factor_a', GetValNum(Slave, 'pfA', 0));
|
|
MeasurePayload.ch[0].v := GetValNum(Slave, 'vrms_a', GetValNum(Slave, 'voltageA', 0));
|
|
MeasurePayload.ch[0].i := GetValNum(Slave, 'irms_a', GetValNum(Slave, 'currentA', 0));
|
|
MeasurePayload.ch[0].p := GetValNum(Slave, 'active_power_a', GetValNum(Slave, 'wA', 0));
|
|
MeasurePayload.ch[0].q := GetValNum(Slave, 'reactive_power_a', GetValNum(Slave, 'Var_A', 0));
|
|
MeasurePayload.ch[0].s := GetValNum(Slave, 'apparent_power_a', GetValNum(Slave, 'Va_A', 0));
|
|
|
|
// B상
|
|
if MeasurePayload.wire <> PHASE_3P3W then
|
|
begin
|
|
MeasurePayload.ch[1].IsValid := True;
|
|
MeasurePayload.ch[1].pf := GetValNum(Slave, 'power_factor_b', GetValNum(Slave, 'pfB', 0));
|
|
MeasurePayload.ch[1].v := GetValNum(Slave, 'vrms_b', GetValNum(Slave, 'voltageB', 0));
|
|
MeasurePayload.ch[1].i := GetValNum(Slave, 'irms_b', GetValNum(Slave, 'currentB', 0));
|
|
MeasurePayload.ch[1].p := GetValNum(Slave, 'active_power_b', GetValNum(Slave, 'wB', 0));
|
|
MeasurePayload.ch[1].q := GetValNum(Slave, 'reactive_power_b', GetValNum(Slave, 'Var_B', 0));
|
|
MeasurePayload.ch[1].s := GetValNum(Slave, 'apparent_power_b', GetValNum(Slave, 'Va_B', 0));
|
|
end;
|
|
|
|
// C상
|
|
MeasurePayload.ch[2].IsValid := True;
|
|
MeasurePayload.ch[2].pf := GetValNum(Slave, 'power_factor_c', GetValNum(Slave, 'pfC', 0));
|
|
MeasurePayload.ch[2].v := GetValNum(Slave, 'vrms_c', GetValNum(Slave, 'voltageC', 0));
|
|
MeasurePayload.ch[2].i := GetValNum(Slave, 'irms_c', GetValNum(Slave, 'currentC', 0));
|
|
MeasurePayload.ch[2].p := GetValNum(Slave, 'active_power_c', GetValNum(Slave, 'wC', 0));
|
|
MeasurePayload.ch[2].q := GetValNum(Slave, 'reactive_power_c', GetValNum(Slave, 'Var_C', 0));
|
|
MeasurePayload.ch[2].s := GetValNum(Slave, 'apparent_power_c', GetValNum(Slave, 'Va_C', 0));
|
|
end
|
|
else
|
|
begin
|
|
// 신규 표준 규격 구조
|
|
MeasurePayload.gw := GetValStr(Root, 'gw', '');
|
|
MeasurePayload.dev := GetValStr(Root, 'dev', '');
|
|
MeasurePayload.seq := GetValInt(Root, 'seq', 0);
|
|
MeasurePayload.COMM := GetValInt(Root, 'COMM', 0);
|
|
MeasurePayload.rssi := GetValInt(Root, 'rssi', 0);
|
|
MeasurePayload.wire := GetValStr(Root, 'wire', PHASE_3P4W);
|
|
MeasurePayload.freq := GetValNum(Root, 'freq', 60.0);
|
|
MeasurePayload.wh := GetValInt64(Root, 'wh', 0);
|
|
|
|
ChArr := Root.GetValue<TJSONArray>('ch');
|
|
if Assigned(ChArr) then
|
|
begin
|
|
for k := 0 to Min(2, ChArr.Count - 1) do
|
|
begin
|
|
if (ChArr.Items[k] <> nil) and (not (ChArr.Items[k] is TJSONNull)) and (ChArr.Items[k] is TJSONObject) then
|
|
begin
|
|
ChObj := ChArr.Items[k] as TJSONObject;
|
|
MeasurePayload.ch[k].IsValid := True;
|
|
MeasurePayload.ch[k].v := GetValNum(ChObj, 'v', 0.0);
|
|
MeasurePayload.ch[k].i := GetValNum(ChObj, 'i', 0.0);
|
|
MeasurePayload.ch[k].p := GetValNum(ChObj, 'p', 0.0);
|
|
MeasurePayload.ch[k].q := GetValNum(ChObj, 'q', 0.0);
|
|
MeasurePayload.ch[k].s := GetValNum(ChObj, 's', 0.0);
|
|
MeasurePayload.ch[k].pf := GetValNum(ChObj, 'pf', 0.0);
|
|
end;
|
|
end;
|
|
end;
|
|
|
|
var jvTotal := Root.GetValue('total');
|
|
if Assigned(jvTotal) and (not (jvTotal is TJSONNull)) and (jvTotal is TJSONObject) then
|
|
begin
|
|
TotObj := jvTotal as TJSONObject;
|
|
MeasurePayload.total.p := GetValNum(TotObj, 'p', 0.0);
|
|
MeasurePayload.total.q := GetValNum(TotObj, 'q', 0.0);
|
|
MeasurePayload.total.s := GetValNum(TotObj, 's', 0.0);
|
|
MeasurePayload.total.pf := GetValNum(TotObj, 'pf', 0.0);
|
|
end;
|
|
end;
|
|
|
|
deviceFound := False;
|
|
if Assigned(FDeviceList) then
|
|
begin
|
|
for i := 0 to FDeviceList.Count - 1 do
|
|
if FDeviceList[i].DeviceID = DeviceID then
|
|
begin
|
|
TargetDevice := FDeviceList[i];
|
|
deviceFound := True;
|
|
Break;
|
|
end;
|
|
end;
|
|
|
|
// wire가 null이거나 비어있을 경우 장비 설정에서 Fallback
|
|
if (MeasurePayload.wire = '') or SameText(MeasurePayload.wire, 'null') then
|
|
begin
|
|
if deviceFound and (TargetDevice.PhaseType <> '') then
|
|
MeasurePayload.wire := TargetDevice.PhaseType
|
|
else
|
|
MeasurePayload.wire := PHASE_3P4W;
|
|
end;
|
|
|
|
// ─── [가상 전력 계산] 전압 미연결 / 전류 전용(1-CT) 패킷 처리 ───
|
|
var hasRealVoltage: Boolean := False;
|
|
var hasRealPower: Boolean := False;
|
|
var hasCurrent: Boolean := False;
|
|
for k := 0 to 2 do
|
|
begin
|
|
if MeasurePayload.ch[k].v > 0 then hasRealVoltage := True;
|
|
if MeasurePayload.ch[k].p > 0 then hasRealPower := True;
|
|
if MeasurePayload.ch[k].i > 0 then hasCurrent := True;
|
|
end;
|
|
if (MeasurePayload.total.p > 0) then hasRealPower := True;
|
|
|
|
var bDoEstimate: Boolean := False;
|
|
if deviceFound then
|
|
bDoEstimate := TargetDevice.IsEstimatePower
|
|
else
|
|
bDoEstimate := True;
|
|
|
|
if bDoEstimate and hasCurrent and (not hasRealPower) and (not hasRealVoltage) then
|
|
begin
|
|
var NomV: Double := 220.0;
|
|
var AssPF: Double := 0.90;
|
|
var EstWire: string := 'SINGLE';
|
|
|
|
if deviceFound then
|
|
begin
|
|
if TargetDevice.NominalVoltage > 0 then NomV := TargetDevice.NominalVoltage;
|
|
if TargetDevice.AssumedPF > 0 then AssPF := TargetDevice.AssumedPF;
|
|
if TargetDevice.EstimateWiring <> '' then EstWire := TargetDevice.EstimateWiring;
|
|
end;
|
|
|
|
var sumP: Double := 0.0;
|
|
var sumQ: Double := 0.0;
|
|
var sumS: Double := 0.0;
|
|
var maxI: Double := 0.0;
|
|
|
|
for k := 0 to 2 do
|
|
begin
|
|
if MeasurePayload.ch[k].i > 0 then
|
|
begin
|
|
MeasurePayload.ch[k].v := NomV;
|
|
MeasurePayload.ch[k].pf := AssPF;
|
|
MeasurePayload.ch[k].s := NomV * MeasurePayload.ch[k].i;
|
|
MeasurePayload.ch[k].p := MeasurePayload.ch[k].s * AssPF;
|
|
MeasurePayload.ch[k].q := Sqrt(Max(Sqr(MeasurePayload.ch[k].s) - Sqr(MeasurePayload.ch[k].p), 0.0));
|
|
sumP := sumP + MeasurePayload.ch[k].p;
|
|
sumQ := sumQ + MeasurePayload.ch[k].q;
|
|
sumS := sumS + MeasurePayload.ch[k].s;
|
|
if MeasurePayload.ch[k].i > maxI then maxI := MeasurePayload.ch[k].i;
|
|
end;
|
|
end;
|
|
|
|
if SameText(EstWire, '3PHASE_BALANCED') and (maxI > 0) then
|
|
begin
|
|
// 3상 평형 부하 추정: P = √3 * V_line * I * PF
|
|
MeasurePayload.total.s := Sqrt(3.0) * NomV * maxI;
|
|
MeasurePayload.total.p := MeasurePayload.total.s * AssPF;
|
|
MeasurePayload.total.q := Sqrt(Max(Sqr(MeasurePayload.total.s) - Sqr(MeasurePayload.total.p), 0.0));
|
|
MeasurePayload.total.pf := AssPF;
|
|
end
|
|
else
|
|
begin
|
|
// 단상 또는 다채널 합산
|
|
MeasurePayload.total.p := sumP;
|
|
MeasurePayload.total.q := sumQ;
|
|
MeasurePayload.total.s := sumS;
|
|
MeasurePayload.total.pf := AssPF;
|
|
end;
|
|
end;
|
|
|
|
// 3P3W / 3P4W 전체 전력 계산 보정 (기존 정상 다채널 계측용)
|
|
if (MeasurePayload.total.p = 0) and (MeasurePayload.ch[0].p > 0) then
|
|
begin
|
|
if SameText(MeasurePayload.wire, PHASE_3P3W) then
|
|
begin
|
|
MeasurePayload.total.p := MeasurePayload.ch[0].p + MeasurePayload.ch[2].p;
|
|
MeasurePayload.total.q := MeasurePayload.ch[0].q + MeasurePayload.ch[2].q;
|
|
end
|
|
else
|
|
begin
|
|
MeasurePayload.total.p := MeasurePayload.ch[0].p + MeasurePayload.ch[1].p + MeasurePayload.ch[2].p;
|
|
MeasurePayload.total.q := MeasurePayload.ch[0].q + MeasurePayload.ch[1].q + MeasurePayload.ch[2].q;
|
|
end;
|
|
MeasurePayload.total.s := Sqrt(Sqr(MeasurePayload.total.p) + Sqr(MeasurePayload.total.q));
|
|
if MeasurePayload.total.s > 0 then
|
|
MeasurePayload.total.pf := MeasurePayload.total.p / MeasurePayload.total.s;
|
|
end;
|
|
|
|
DevSensors := nil;
|
|
if deviceFound then
|
|
DevSensors := TargetDevice.SensorList;
|
|
|
|
// 가동 상태(opStatus) 판정
|
|
opStatus := '0'; // 전원꺼짐 (0)
|
|
if deviceFound then
|
|
begin
|
|
currVal := 0.0;
|
|
pfVal := 0.0;
|
|
|
|
if SameText(TargetDevice.OpTargetPhase, 'L1') then
|
|
begin
|
|
currVal := MeasurePayload.ch[0].i;
|
|
pfVal := MeasurePayload.ch[0].pf;
|
|
end
|
|
else if SameText(TargetDevice.OpTargetPhase, 'L2') and (not SameText(MeasurePayload.wire, PHASE_3P3W)) then
|
|
begin
|
|
currVal := MeasurePayload.ch[1].i;
|
|
pfVal := MeasurePayload.ch[1].pf;
|
|
end
|
|
else if SameText(TargetDevice.OpTargetPhase, 'L3') then
|
|
begin
|
|
currVal := MeasurePayload.ch[2].i;
|
|
pfVal := MeasurePayload.ch[2].pf;
|
|
end
|
|
else // 'TOTAL' or 'AVG'
|
|
begin
|
|
if SameText(MeasurePayload.wire, PHASE_3P3W) then
|
|
begin
|
|
currVal := (MeasurePayload.ch[0].i + MeasurePayload.ch[2].i) / 2.0;
|
|
pfVal := MeasurePayload.total.pf;
|
|
if pfVal = 0 then pfVal := (MeasurePayload.ch[0].pf + MeasurePayload.ch[2].pf) / 2.0;
|
|
end
|
|
else
|
|
begin
|
|
currVal := (MeasurePayload.ch[0].i + MeasurePayload.ch[1].i + MeasurePayload.ch[2].i) / 3.0;
|
|
pfVal := MeasurePayload.total.pf;
|
|
if pfVal = 0 then pfVal := (MeasurePayload.ch[0].pf + MeasurePayload.ch[1].pf + MeasurePayload.ch[2].pf) / 3.0;
|
|
end;
|
|
end;
|
|
|
|
if (currVal >= TargetDevice.OpThresholdRunCurrent) and (pfVal >= TargetDevice.OpThresholdPF) then
|
|
opStatus := '2' // 가동
|
|
else if (currVal > TargetDevice.OpThresholdOffCurrent) then
|
|
opStatus := '1' // 미가동
|
|
else
|
|
opStatus := '0'; // 전원꺼짐
|
|
end;
|
|
|
|
// 그리드 표시용 JSON 객체 구성
|
|
GridDataObj := TJSONObject.Create;
|
|
try
|
|
GridDataObj.AddPair('wire', MeasurePayload.wire);
|
|
GridDataObj.AddPair('rssi', TJSONNumber.Create(MeasurePayload.rssi));
|
|
GridDataObj.AddPair('frequency', TJSONNumber.Create(MeasurePayload.freq));
|
|
GridDataObj.AddPair('wh', TJSONNumber.Create(MeasurePayload.wh));
|
|
GridDataObj.AddPair('total_p', TJSONNumber.Create(MeasurePayload.total.p));
|
|
GridDataObj.AddPair('total_q', TJSONNumber.Create(MeasurePayload.total.q));
|
|
GridDataObj.AddPair('total_s', TJSONNumber.Create(MeasurePayload.total.s));
|
|
GridDataObj.AddPair('total_pf', TJSONNumber.Create(MeasurePayload.total.pf));
|
|
|
|
GridDataObj.AddPair('power_factor_a', TJSONNumber.Create(MeasurePayload.ch[0].pf));
|
|
GridDataObj.AddPair('vrms_a', TJSONNumber.Create(MeasurePayload.ch[0].v));
|
|
GridDataObj.AddPair('irms_a', TJSONNumber.Create(MeasurePayload.ch[0].i));
|
|
GridDataObj.AddPair('active_power_a', TJSONNumber.Create(MeasurePayload.ch[0].p));
|
|
GridDataObj.AddPair('reactive_power_a', TJSONNumber.Create(MeasurePayload.ch[0].q));
|
|
GridDataObj.AddPair('apparent_power_a', TJSONNumber.Create(MeasurePayload.ch[0].s));
|
|
|
|
GridDataObj.AddPair('power_factor_b', TJSONNumber.Create(MeasurePayload.ch[1].pf));
|
|
GridDataObj.AddPair('vrms_b', TJSONNumber.Create(MeasurePayload.ch[1].v));
|
|
GridDataObj.AddPair('irms_b', TJSONNumber.Create(MeasurePayload.ch[1].i));
|
|
GridDataObj.AddPair('active_power_b', TJSONNumber.Create(MeasurePayload.ch[1].p));
|
|
GridDataObj.AddPair('reactive_power_b', TJSONNumber.Create(MeasurePayload.ch[1].q));
|
|
GridDataObj.AddPair('apparent_power_b', TJSONNumber.Create(MeasurePayload.ch[1].s));
|
|
|
|
GridDataObj.AddPair('power_factor_c', TJSONNumber.Create(MeasurePayload.ch[2].pf));
|
|
GridDataObj.AddPair('vrms_c', TJSONNumber.Create(MeasurePayload.ch[2].v));
|
|
GridDataObj.AddPair('irms_c', TJSONNumber.Create(MeasurePayload.ch[2].i));
|
|
GridDataObj.AddPair('active_power_c', TJSONNumber.Create(MeasurePayload.ch[2].p));
|
|
GridDataObj.AddPair('reactive_power_c', TJSONNumber.Create(MeasurePayload.ch[2].q));
|
|
GridDataObj.AddPair('apparent_power_c', TJSONNumber.Create(MeasurePayload.ch[2].s));
|
|
|
|
// 1. 그리드를 갱신하면서 seq 변경 여부 확인
|
|
var bSeqChanged: Boolean := gridmonitorRefresh_main(
|
|
DeviceID, MeasurePayload.seq, 0, MeasurePayload.COMM, APayload, opStatus, GridDataObj, DevSensors);
|
|
|
|
var devName: string := '';
|
|
if deviceFound then
|
|
devName := TargetDevice.DeviceName;
|
|
|
|
// 2. DB에 저장 (chkAutoSaveDB 체크 시에만 실행)
|
|
if Assigned(chkAutoSaveDB) and chkAutoSaveDB.Checked then
|
|
begin
|
|
try
|
|
FNILMManager.SaveMeasurementData(
|
|
DeviceID, devName, MeasurePayload, APayload, opStatus, DevSensors, bSeqChanged);
|
|
except
|
|
on E: Exception do
|
|
Log(reDBLog, 'error', '[DB저장 오류] ' + E.Message);
|
|
end;
|
|
end;
|
|
|
|
// 당일 누적 전력량(Wh) 실시간 적분 계산 (자정 외부 DB 전송용)
|
|
var nowTime := Now;
|
|
var lastTime: TDateTime := 0;
|
|
if FLastEnergyUpdate.TryGetValue(DeviceID, lastTime) and (lastTime > 0) then
|
|
begin
|
|
var deltaSec: Double := (nowTime - lastTime) * 86400.0;
|
|
if (deltaSec > 0) and (deltaSec < 60.0) then
|
|
begin
|
|
var currentDayWh: Double := 0;
|
|
FDayEnergyValue.TryGetValue(DeviceID, currentDayWh);
|
|
currentDayWh := currentDayWh + (MeasurePayload.total.p * deltaSec / 3600.0);
|
|
FDayEnergyValue.AddOrSetValue(DeviceID, currentDayWh);
|
|
end;
|
|
end;
|
|
FLastEnergyUpdate.AddOrSetValue(DeviceID, nowTime);
|
|
|
|
// 3. seq가 변경된 경우에만 OPC 서버에 쓰기 수행
|
|
if bSeqChanged then
|
|
begin
|
|
Log(reMainLog, 'mqttmsg', ATopic + ' ' + APayload);
|
|
|
|
successCount := 0;
|
|
failCount := 0;
|
|
skipCount := 0;
|
|
DupCount := 0;
|
|
|
|
// 공통/메타
|
|
SendOPC('COMM', IntToStr(MeasurePayload.COMM));
|
|
SendOPC('OpStatus', opStatus);
|
|
SendOPC('wire', IfThen(SameText(MeasurePayload.wire, PHASE_3P3W), '0', '1'));
|
|
SendOPC('rssi', IntToStr(MeasurePayload.rssi));
|
|
SendOPC('freq', FloatToStr(MeasurePayload.freq));
|
|
SendOPC('wh', IntToStr(MeasurePayload.wh));
|
|
|
|
// Total
|
|
SendOPC('total_p', FloatToStr(MeasurePayload.total.p));
|
|
SendOPC('total_q', FloatToStr(MeasurePayload.total.q));
|
|
SendOPC('total_s', FloatToStr(MeasurePayload.total.s));
|
|
SendOPC('total_pf', FloatToStr(MeasurePayload.total.pf));
|
|
|
|
// A상
|
|
SendOPC('power_factor_a', FloatToStr(MeasurePayload.ch[0].pf));
|
|
SendOPC('vrms_a', FloatToStr(MeasurePayload.ch[0].v));
|
|
SendOPC('irms_a', FloatToStr(MeasurePayload.ch[0].i));
|
|
SendOPC('active_power_a', FloatToStr(MeasurePayload.ch[0].p));
|
|
SendOPC('reactive_power_a', FloatToStr(MeasurePayload.ch[0].q));
|
|
SendOPC('apparent_power_a', FloatToStr(MeasurePayload.ch[0].s));
|
|
|
|
// B상
|
|
if SameText(MeasurePayload.wire, PHASE_3P3W) then
|
|
begin
|
|
SendOPC('power_factor_b', '0');
|
|
SendOPC('vrms_b', '0');
|
|
SendOPC('irms_b', '0');
|
|
SendOPC('active_power_b', '0');
|
|
SendOPC('reactive_power_b', '0');
|
|
SendOPC('apparent_power_b', '0');
|
|
end
|
|
else
|
|
begin
|
|
SendOPC('power_factor_b', FloatToStr(MeasurePayload.ch[1].pf));
|
|
SendOPC('vrms_b', FloatToStr(MeasurePayload.ch[1].v));
|
|
SendOPC('irms_b', FloatToStr(MeasurePayload.ch[1].i));
|
|
SendOPC('active_power_b', FloatToStr(MeasurePayload.ch[1].p));
|
|
SendOPC('reactive_power_b', FloatToStr(MeasurePayload.ch[1].q));
|
|
SendOPC('apparent_power_b', FloatToStr(MeasurePayload.ch[1].s));
|
|
end;
|
|
|
|
// C상
|
|
SendOPC('power_factor_c', FloatToStr(MeasurePayload.ch[2].pf));
|
|
SendOPC('vrms_c', FloatToStr(MeasurePayload.ch[2].v));
|
|
SendOPC('irms_c', FloatToStr(MeasurePayload.ch[2].i));
|
|
SendOPC('active_power_c', FloatToStr(MeasurePayload.ch[2].p));
|
|
SendOPC('reactive_power_c', FloatToStr(MeasurePayload.ch[2].q));
|
|
SendOPC('apparent_power_c', FloatToStr(MeasurePayload.ch[2].s));
|
|
|
|
if failCount > 0 then
|
|
Log(reOPCLog, 'error', Format('[OPC 쓰기] %s - 성공: %d, 에러: %d, 무시: %d, 변경없음: %d', [ATopic, successCount, failCount, skipCount, DupCount]))
|
|
else if (successCount > 0) or (failCount = 0) then
|
|
Log(reOPCLog, 'process', Format('[OPC 쓰기] %s - 성공: %d, 에러: 0, 무시: %d, 변경없음: %d', [ATopic, successCount, skipCount, DupCount]));
|
|
end;
|
|
finally
|
|
GridDataObj.Free;
|
|
end;
|
|
finally
|
|
Root.Free;
|
|
end;
|
|
except
|
|
on E: Exception do
|
|
Log(reMainLog, 'error', '[NILM-SAVE] ' + E.Message);
|
|
end;
|
|
end;
|
|
|
|
procedure TfMain.NILMErrorHandler(const AMsg: string);
|
|
begin
|
|
Log(reDBLog, 'error', AMsg);
|
|
end;
|
|
|
|
end.
|