unit uNILMDeviceForm; { NILM 장비 추가 / 수정 다이얼로그 폼 - 기본 정보 입력 (device_id, 명칭, 위치, 상수타입, 활성여부, 메모) - 센서 그룹별 CheckBox 설정 (탭 페이지) - 상수 타입 변경 시 L2/L3 탭 표시/숨김 처리 } interface uses Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, System.Generics.Collections, System.UITypes, System.Math, Vcl.Graphics, Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls, Vcl.ExtCtrls, Vcl.ComCtrls, Vcl.Grids, uNILMTypes; type TfNILMDevice = class(TForm) pnlTop : TPanel; lblTitle : TLabel; pnlBottom : TPanel; btnSave : TButton; btnCancel : TButton; pnlBasic : TPanel; lblBasicInfo : TLabel; lblDeviceID : TLabel; edtDeviceID : TEdit; lblDeviceName: TLabel; edtDeviceName: TEdit; lblLocation : TLabel; edtLocation : TEdit; lblPhaseType : TLabel; rgPhaseType : TRadioGroup; chkIsActive : TCheckBox; lblMqttTopic : TLabel; edtMqttTopic : TEdit; lblMemo : TLabel; memMemo : TMemo; lblOpTargetPhase: TLabel; cmbOpTargetPhase: TComboBox; lblOpThresholdOffCurrent: TLabel; edtOpThresholdOffCurrent: TEdit; lblOpThresholdRunCurrent: TLabel; edtOpThresholdRunCurrent: TEdit; lblOpThresholdPF: TLabel; edtOpThresholdPF: TEdit; grpEstimate : TGroupBox; chkIsEstimatePower: TCheckBox; lblNominalVoltage : TLabel; edtNominalVoltage : TEdit; lblAssumedPF : TLabel; edtAssumedPF : TEdit; lblEstimateWiring : TLabel; cmbEstimateWiring : TComboBox; pnlSensor : TPanel; lblSensorCfg : TLabel; grdSensor : TStringGrid; procedure btnSaveClick(Sender: TObject); procedure btnCancelClick(Sender: TObject); procedure rgPhaseTypeClick(Sender: TObject); procedure FormCreate(Sender: TObject); private FIsNew : Boolean; FDevice : TNILMDevice; procedure BuildSensorUI; function ValidateInput: Boolean; procedure PopulateFromDevice; procedure ApplyToDevice; public constructor Create(AOwner: TComponent); override; destructor Destroy; override; procedure InitForAdd; procedure InitForEdit(const ADevice: TNILMDevice); /// 다이얼로그 실행 후 결과 장비 정보 반환, OK 여부 반환 function Execute(out AResult: TNILMDevice): Boolean; end; TfNILMDeviceForm = TfNILMDevice; implementation uses uNILMManager; {$R *.dfm} { TfNILMDevice } constructor TfNILMDevice.Create(AOwner: TComponent); begin inherited Create(AOwner); FIsNew := True; FDevice.SensorList := TNILMSensorConfigList.Create; end; destructor TfNILMDevice.Destroy; begin if Assigned(FDevice.SensorList) then FreeAndNil(FDevice.SensorList); inherited Destroy; end; procedure TfNILMDevice.FormCreate(Sender: TObject); begin if FIsNew then Caption := 'NILM 장비 추가' else Caption := 'NILM 장비 수정'; lblTitle.Caption := Caption; end; procedure TfNILMDevice.InitForAdd; begin FIsNew := True; Caption := 'NILM 장비 추가'; lblTitle.Caption := Caption; edtDeviceID.ReadOnly := False; edtDeviceID.Text := ''; edtDeviceName.Text := ''; edtLocation.Text := ''; chkIsActive.Checked := True; edtMqttTopic.Text := 'QST/SUNGJOO/NEWPRIME/NE002/'; memMemo.Text := ''; cmbOpTargetPhase.ItemIndex := cmbOpTargetPhase.Items.IndexOf('AVG'); edtOpThresholdOffCurrent.Text := '0.5'; edtOpThresholdRunCurrent.Text := '1.0'; edtOpThresholdPF.Text := '0.7'; rgPhaseType.ItemIndex := 0; // 3P3W default chkIsEstimatePower.Checked := True; edtNominalVoltage.Text := '380.0'; edtAssumedPF.Text := '0.90'; cmbEstimateWiring.ItemIndex := 1; // 3상 평형 (P = √3 * V * I * PF) if not Assigned(FDevice.SensorList) then FDevice.SensorList := TNILMSensorConfigList.Create; FDevice.SensorList.Clear; BuildSensorUI; end; procedure TfNILMDevice.InitForEdit(const ADevice: TNILMDevice); var SC: TNILMSensorConfig; begin FIsNew := False; FDevice.ID := ADevice.ID; FDevice.DeviceID := ADevice.DeviceID; FDevice.DeviceName := ADevice.DeviceName; FDevice.Location := ADevice.Location; FDevice.PhaseType := ADevice.PhaseType; FDevice.IsActive := ADevice.IsActive; FDevice.Memo := ADevice.Memo; FDevice.MqttTopic := ADevice.MqttTopic; FDevice.CreatedAt := ADevice.CreatedAt; FDevice.UpdatedAt := ADevice.UpdatedAt; FDevice.OpThresholdOffCurrent := ADevice.OpThresholdOffCurrent; FDevice.OpThresholdRunCurrent := ADevice.OpThresholdRunCurrent; FDevice.OpThresholdPF := ADevice.OpThresholdPF; FDevice.OpTargetPhase := ADevice.OpTargetPhase; FDevice.IsEstimatePower := ADevice.IsEstimatePower; FDevice.NominalVoltage := ADevice.NominalVoltage; FDevice.AssumedPF := ADevice.AssumedPF; FDevice.EstimateWiring := ADevice.EstimateWiring; if not Assigned(FDevice.SensorList) then FDevice.SensorList := TNILMSensorConfigList.Create; FDevice.SensorList.Clear; if Assigned(ADevice.SensorList) then begin for SC in ADevice.SensorList do FDevice.SensorList.Add(SC); end; Caption := 'NILM 장비 수정'; lblTitle.Caption := Caption; edtDeviceID.ReadOnly := True; PopulateFromDevice; end; procedure TfNILMDevice.PopulateFromDevice; begin edtDeviceID.Text := IntToStr(FDevice.DeviceID); edtDeviceName.Text := FDevice.DeviceName; edtLocation.Text := FDevice.Location; chkIsActive.Checked:= FDevice.IsActive; edtMqttTopic.Text := FDevice.MqttTopic; memMemo.Text := FDevice.Memo; cmbOpTargetPhase.ItemIndex := cmbOpTargetPhase.Items.IndexOf(FDevice.OpTargetPhase); if cmbOpTargetPhase.ItemIndex < 0 then cmbOpTargetPhase.ItemIndex := cmbOpTargetPhase.Items.IndexOf('AVG'); edtOpThresholdOffCurrent.Text := FloatToStr(FDevice.OpThresholdOffCurrent); edtOpThresholdRunCurrent.Text := FloatToStr(FDevice.OpThresholdRunCurrent); edtOpThresholdPF.Text := FloatToStr(FDevice.OpThresholdPF); chkIsEstimatePower.Checked := FDevice.IsEstimatePower; edtNominalVoltage.Text := FloatToStr(IfThen(FDevice.NominalVoltage > 0, FDevice.NominalVoltage, 220.0)); edtAssumedPF.Text := FloatToStr(IfThen(FDevice.AssumedPF > 0, FDevice.AssumedPF, 0.90)); if SameText(FDevice.EstimateWiring, '3PHASE_BALANCED') then cmbEstimateWiring.ItemIndex := 1 else cmbEstimateWiring.ItemIndex := 0; if FDevice.PhaseType = PHASE_3P3W then rgPhaseType.ItemIndex := 0 else if (FDevice.PhaseType = PHASE_3P4W) or (FDevice.PhaseType = PHASE_3PHASE) then rgPhaseType.ItemIndex := 1 else if FDevice.PhaseType = PHASE_SINGLE then rgPhaseType.ItemIndex := 2 else rgPhaseType.ItemIndex := 1; BuildSensorUI; end; procedure TfNILMDevice.BuildSensorUI; var Is3Phase: Boolean; SC : TNILMSensorConfig; i : Integer; begin Is3Phase := (rgPhaseType.ItemIndex = 0); // 센서 목록이 없으면 기본 템플릿 생성 if FDevice.SensorList.Count = 0 then begin FDevice.SensorList.Clear; var TempList: TNILMSensorConfigList; if rgPhaseType.ItemIndex = 0 then TempList := TNILMSensorTemplate.BuildDefaultSensors(FDevice.DeviceID, PHASE_3P3W) else if rgPhaseType.ItemIndex = 1 then TempList := TNILMSensorTemplate.BuildDefaultSensors(FDevice.DeviceID, PHASE_3P4W) else TempList := TNILMSensorTemplate.BuildDefaultSensors(FDevice.DeviceID, PHASE_SINGLE); FDevice.SensorList.AddRange(TempList); TempList.Free; end; grdSensor.RowCount := Max(2, FDevice.SensorList.Count + 1); grdSensor.Cells[0, 0] := '그룹'; grdSensor.Cells[1, 0] := '센서 항목'; grdSensor.Cells[2, 0] := '사용(O/X)'; grdSensor.Cells[3, 0] := 'JSON 키'; grdSensor.Cells[4, 0] := 'FieldKey'; grdSensor.ColWidths[0] := 130; grdSensor.ColWidths[1] := 230; grdSensor.ColWidths[2] := 80; grdSensor.ColWidths[3] := 180; grdSensor.ColWidths[4] := 0; for i := 0 to FDevice.SensorList.Count - 1 do begin SC := FDevice.SensorList[i]; grdSensor.Cells[0, i+1] := SC.SensorGroup; grdSensor.Cells[1, i+1] := SC.FieldLabel; if SC.IsEnabled then grdSensor.Cells[2, i+1] := 'O' else grdSensor.Cells[2, i+1] := 'X'; grdSensor.Cells[3, i+1] := SC.JsonKey; grdSensor.Cells[4, i+1] := SC.FieldKey; end; end; procedure TfNILMDevice.rgPhaseTypeClick(Sender: TObject); begin if rgPhaseType.ItemIndex in [0, 1] then begin // 3상 3선식 또는 3상 4선식 선택 시: 3상 평형 추정 및 380V 기본 cmbEstimateWiring.ItemIndex := 1; // 3상 평형 (P = √3 * V * I * PF) if (edtNominalVoltage.Text = '') or (edtNominalVoltage.Text = '220') or (edtNominalVoltage.Text = '220.0') then edtNominalVoltage.Text := '380.0'; end else begin // 단상 선택 시: 단상 추정 및 220V 기본 cmbEstimateWiring.ItemIndex := 0; // 단상 (P = V * I * PF) if (edtNominalVoltage.Text = '') or (edtNominalVoltage.Text = '380') or (edtNominalVoltage.Text = '380.0') then edtNominalVoltage.Text := '220.0'; end; FDevice.SensorList.Clear; BuildSensorUI; end; function TfNILMDevice.ValidateInput: Boolean; var ID : Integer; D : Double; Mgr: TNILMManager; begin Result := False; if not TryStrToInt(edtDeviceID.Text, ID) or (ID <= 0) then begin MessageDlg('장비 번호는 1 이상의 숫자여야 합니다.', mtError, [mbOK], 0); edtDeviceID.SetFocus; Exit; end; if Trim(edtDeviceName.Text) = '' then begin MessageDlg('장비 명칭을 입력해주세요.', mtError, [mbOK], 0); edtDeviceName.SetFocus; Exit; end; if not TryStrToFloat(edtOpThresholdOffCurrent.Text, D) then begin MessageDlg('전원꺼짐 기준 전류는 숫자여야 합니다.', mtError, [mbOK], 0); edtOpThresholdOffCurrent.SetFocus; Exit; end; if not TryStrToFloat(edtOpThresholdRunCurrent.Text, D) then begin MessageDlg('가동 기준 전류는 숫자여야 합니다.', mtError, [mbOK], 0); edtOpThresholdRunCurrent.SetFocus; Exit; end; if not TryStrToFloat(edtOpThresholdPF.Text, D) then begin MessageDlg('가동 기준 파워팩터는 숫자여야 합니다.', mtError, [mbOK], 0); edtOpThresholdPF.SetFocus; Exit; end; Mgr := TNILMManager.Create; try try if Mgr.DeviceIDExists(ID, FDevice.ID) then begin MessageDlg(Format('장비 번호 %d 는 이미 등록되어 있습니다.', [ID]), mtError, [mbOK], 0); edtDeviceID.SetFocus; Exit; end; except on E: Exception do begin MessageDlg('DB 연결이 없어 중복 확인을 건너뜁니다.' + sLineBreak + E.Message, mtWarning, [mbOK], 0); end; end; finally Mgr.Free; end; Result := True; end; procedure TfNILMDevice.ApplyToDevice; var SC : TNILMSensorConfig; i : Integer; begin FDevice.DeviceID := StrToIntDef(edtDeviceID.Text, 0); FDevice.DeviceName := Trim(edtDeviceName.Text); FDevice.Location := Trim(edtLocation.Text); FDevice.IsActive := chkIsActive.Checked; FDevice.Memo := memMemo.Text; FDevice.MqttTopic := Trim(edtMqttTopic.Text); FDevice.OpTargetPhase := cmbOpTargetPhase.Text; FDevice.OpThresholdOffCurrent := StrToFloatDef(edtOpThresholdOffCurrent.Text, 0.0); FDevice.OpThresholdRunCurrent := StrToFloatDef(edtOpThresholdRunCurrent.Text, 0.0); FDevice.OpThresholdPF := StrToFloatDef(edtOpThresholdPF.Text, 0.0); FDevice.IsEstimatePower := chkIsEstimatePower.Checked; FDevice.NominalVoltage := StrToFloatDef(edtNominalVoltage.Text, 220.0); FDevice.AssumedPF := StrToFloatDef(edtAssumedPF.Text, 0.90); if cmbEstimateWiring.ItemIndex = 1 then FDevice.EstimateWiring := '3PHASE_BALANCED' else FDevice.EstimateWiring := 'SINGLE'; if rgPhaseType.ItemIndex = 0 then FDevice.PhaseType := PHASE_3P3W else if rgPhaseType.ItemIndex = 1 then FDevice.PhaseType := PHASE_3P4W else FDevice.PhaseType := PHASE_SINGLE; FDevice.SensorList.Clear; for i := 1 to grdSensor.RowCount - 1 do begin if Trim(grdSensor.Cells[1, i]) = '' then Continue; SC.ID := 0; SC.DeviceID := FDevice.DeviceID; SC.SensorGroup := grdSensor.Cells[0, i]; SC.FieldLabel := grdSensor.Cells[1, i]; SC.IsEnabled := (Trim(grdSensor.Cells[2, i]) = 'O'); SC.JsonKey := Trim(grdSensor.Cells[3, i]); SC.FieldKey := Trim(grdSensor.Cells[4, i]); SC.DisplayOrder := i; FDevice.SensorList.Add(SC); end; end; procedure TfNILMDevice.btnSaveClick(Sender: TObject); var Mgr: TNILMManager; begin if ValidateInput then begin ApplyToDevice; Mgr := TNILMManager.Create; try Mgr.SaveDevice(FDevice); finally Mgr.Free; end; ModalResult := mrOk; end; end; procedure TfNILMDevice.btnCancelClick(Sender: TObject); begin ModalResult := mrCancel; end; function TfNILMDevice.Execute(out AResult: TNILMDevice): Boolean; var SC: TNILMSensorConfig; begin PopulateFromDevice; Result := (ShowModal = mrOk); if Result then begin AResult := FDevice; AResult.SensorList := TNILMSensorConfigList.Create; if Assigned(FDevice.SensorList) then begin for SC in FDevice.SensorList do AResult.SensorList.Add(SC); end; end; end; end.