diff --git a/src/main/java/org/gridsuite/study/server/controller/logicalcontrols/LogicalControlsController.java b/src/main/java/org/gridsuite/study/server/controller/logicalcontrols/LogicalControlsController.java new file mode 100644 index 0000000000..9bedd28d6d --- /dev/null +++ b/src/main/java/org/gridsuite/study/server/controller/logicalcontrols/LogicalControlsController.java @@ -0,0 +1,96 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.study.server.controller.logicalcontrols; + +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.responses.ApiResponses; +import io.swagger.v3.oas.annotations.tags.Tag; +import org.gridsuite.study.server.StudyApi; +import org.gridsuite.study.server.nodeactivity.NodeActivityRunnerService; +import org.gridsuite.study.server.service.RootNetworkNodeInfoService; +import org.gridsuite.study.server.service.StudyService; +import org.gridsuite.study.server.service.logicalcontrols.LogicalControlsService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; +import java.util.UUID; + +import static org.gridsuite.study.server.StudyConstants.HEADER_USER_ID; +import static org.gridsuite.study.server.nodeactivity.NodeActivityType.COMPUTE; + +/** + * @author David Braquart + */ +@RestController +@RequestMapping(value = "/" + StudyApi.API_VERSION + "/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/logical-controls") +@Tag(name = "Study server - Logical controls") +public class LogicalControlsController { + private final RootNetworkNodeInfoService rootNetworkNodeInfoService; + private final StudyService studyService; + private final LogicalControlsService logicalControlsService; + private final NodeActivityRunnerService nodeActivityRunnerService; + + public LogicalControlsController(RootNetworkNodeInfoService rootNetworkNodeInfoService, StudyService studyService, LogicalControlsService logicalControlsService, + NodeActivityRunnerService nodeActivityRunnerService) { + this.rootNetworkNodeInfoService = rootNetworkNodeInfoService; + this.studyService = studyService; + this.logicalControlsService = logicalControlsService; + this.nodeActivityRunnerService = nodeActivityRunnerService; + } + + @PostMapping(value = "/run") + @Operation(summary = "run logical controls on study") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The logical controls has started")}) + public ResponseEntity runLogicalControls(@Parameter(description = "studyUuid") @PathVariable("studyUuid") UUID studyUuid, + @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid, + @RequestHeader(HEADER_USER_ID) String userId) { + studyService.assertIsNodeNotReadOnly(nodeUuid); + // no quota on this computation + nodeActivityRunnerService.runWith(COMPUTE, studyUuid, rootNetworkUuid, List.of(nodeUuid), + () -> logicalControlsService.runLogicalControls(studyUuid, nodeUuid, rootNetworkUuid, userId)); + return ResponseEntity.ok().build(); + } + + @PutMapping(value = "/stop") + @Operation(summary = "stop logical controls on study") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The logical controls has been stopped")}) + public ResponseEntity stopLogicalControls(@Parameter(description = "Study uuid") @PathVariable("studyUuid") UUID studyUuid, + @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { + rootNetworkNodeInfoService.stopLogicalControls(studyUuid, nodeUuid, rootNetworkUuid); + return ResponseEntity.ok().build(); + } + + @GetMapping(value = "/result") + @Operation(summary = "Get a logical controls result on study") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The logical controls result"), + @ApiResponse(responseCode = "204", description = "No logical controls has been done yet"), + @ApiResponse(responseCode = "404", description = "The logical controls has not been found")}) + public ResponseEntity getLogicalControlsResult(@Parameter(description = "study UUID") @PathVariable("studyUuid") UUID studyUuid, + @Parameter(description = "rootNetwork Uuid") @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "node Uuid") @PathVariable("nodeUuid") UUID nodeUuid) { + String result = rootNetworkNodeInfoService.getLogicalControlsResult(nodeUuid, rootNetworkUuid); + return result != null ? ResponseEntity.ok().body(result) : + ResponseEntity.noContent().build(); + } + + @GetMapping(value = "/status") + @Operation(summary = "Get the logical controls status on study") + @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The logical controls status"), + @ApiResponse(responseCode = "204", description = "No logical controls has been done yet"), + @ApiResponse(responseCode = "404", description = "The logical controls status has not been found")}) + public ResponseEntity getLogicalControlsStatus(@Parameter(description = "Study UUID") @PathVariable("studyUuid") UUID studyUuid, + @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, + @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { + String status = rootNetworkNodeInfoService.getLogicalControlsStatus(nodeUuid, rootNetworkUuid); + return status != null ? ResponseEntity.ok().body(status) : ResponseEntity.noContent().build(); + } +} diff --git a/src/main/java/org/gridsuite/study/server/controller/stateestimation/StateEstimationController.java b/src/main/java/org/gridsuite/study/server/controller/stateestimation/StateEstimationController.java index b2001af251..9b53197bc7 100644 --- a/src/main/java/org/gridsuite/study/server/controller/stateestimation/StateEstimationController.java +++ b/src/main/java/org/gridsuite/study/server/controller/stateestimation/StateEstimationController.java @@ -26,7 +26,6 @@ import static org.gridsuite.study.server.StudyConstants.HEADER_USER_ID; import static org.gridsuite.study.server.dto.ComputationType.STATE_ESTIMATION; import static org.gridsuite.study.server.nodeactivity.NodeActivityType.COMPUTE; -import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; /** * @author Bassel El Cheikh @@ -99,14 +98,4 @@ public ResponseEntity stopStateEstimation(@Parameter(description = "Study rootNetworkNodeInfoService.stopStateEstimation(studyUuid, nodeUuid, rootNetworkUuid, userId); return ResponseEntity.ok().build(); } - - @PostMapping(value = "/logical-controls", produces = APPLICATION_JSON_VALUE) - @Operation(summary = "Run logical controls on a study node") - @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "The computation has been done and the results are returned")}) - public ResponseEntity computeLogicalControls(@Parameter(description = "studyUuid") @PathVariable("studyUuid") UUID studyUuid, - @PathVariable("rootNetworkUuid") UUID rootNetworkUuid, - @Parameter(description = "nodeUuid") @PathVariable("nodeUuid") UUID nodeUuid) { - // no quota on this small synchronous computation - return ResponseEntity.ok().body(stateEstimationService.computeLogicalControls(studyUuid, nodeUuid, rootNetworkUuid)); - } } diff --git a/src/main/java/org/gridsuite/study/server/dto/ComputationType.java b/src/main/java/org/gridsuite/study/server/dto/ComputationType.java index 4db1c04848..c68564fe7e 100644 --- a/src/main/java/org/gridsuite/study/server/dto/ComputationType.java +++ b/src/main/java/org/gridsuite/study/server/dto/ComputationType.java @@ -41,7 +41,11 @@ public enum ComputationType { ASYMMETRICAL_LOAD("Asymmetrical load", "asymmetricalLoadResultUuid", NotificationService.UPDATE_TYPE_ASYMMETRICAL_LOAD_STATUS, NotificationService.UPDATE_TYPE_ASYMMETRICAL_LOAD_RESULT, - NotificationService.UPDATE_TYPE_ASYMMETRICAL_LOAD_FAILED); + NotificationService.UPDATE_TYPE_ASYMMETRICAL_LOAD_FAILED), + LOGICAL_CONTROLS("Logical controls", "logicalControlsResultUuid", + NotificationService.UPDATE_TYPE_LOGICAL_CONTROLS_STATUS, + NotificationService.UPDATE_TYPE_LOGICAL_CONTROLS_RESULT, + NotificationService.UPDATE_TYPE_LOGICAL_CONTROLS_FAILED); private final String label; // used for logs private final String resultUuidLabel; diff --git a/src/main/java/org/gridsuite/study/server/dto/InvalidateNodeInfos.java b/src/main/java/org/gridsuite/study/server/dto/InvalidateNodeInfos.java index 62f597deec..302e70169a 100644 --- a/src/main/java/org/gridsuite/study/server/dto/InvalidateNodeInfos.java +++ b/src/main/java/org/gridsuite/study/server/dto/InvalidateNodeInfos.java @@ -68,6 +68,7 @@ public void add(InvalidateNodeInfos invalidateNodeInfos) { stateEstimationResultUuids.addAll(invalidateNodeInfos.getStateEstimationResultUuids()); pccMinResultUuids.addAll(invalidateNodeInfos.getPccMinResultUuids()); asymmetricalLoadResultUuids.addAll(invalidateNodeInfos.getAsymmetricalLoadResultUuids()); + logicalControlsResultUuids.addAll(invalidateNodeInfos.getLogicalControlsResultUuids()); dynamicSimulationResultUuids.addAll(invalidateNodeInfos.getDynamicSimulationResultUuids()); dynamicSecurityAnalysisResultUuids.addAll(invalidateNodeInfos.getDynamicSecurityAnalysisResultUuids()); dynamicMarginCalculationResultUuids.addAll(invalidateNodeInfos.getDynamicMarginCalculationResultUuids()); @@ -88,6 +89,7 @@ public RemoteDeletionInfos toRemoteDeletionInfos(InvalidateNodeInfos invalidateN infos.setStateEstimationResultUuids(new HashSet<>(invalidateNodeInfos.getStateEstimationResultUuids())); infos.setPccMinResultUuids(new HashSet<>(invalidateNodeInfos.getPccMinResultUuids())); infos.setAsymmetricalLoadResultUuids(new HashSet<>(invalidateNodeInfos.getAsymmetricalLoadResultUuids())); + infos.setLogicalControlsResultUuids(new HashSet<>(invalidateNodeInfos.getLogicalControlsResultUuids())); return infos; } } diff --git a/src/main/java/org/gridsuite/study/server/dto/QuotaType.java b/src/main/java/org/gridsuite/study/server/dto/QuotaType.java index cc8dc7fed0..ab669acfc0 100644 --- a/src/main/java/org/gridsuite/study/server/dto/QuotaType.java +++ b/src/main/java/org/gridsuite/study/server/dto/QuotaType.java @@ -38,6 +38,7 @@ public static QuotaType mapFromComputationType(ComputationType computationType) case PCC_MIN -> QuotaType.PCC_MIN; case SHORT_CIRCUIT, SHORT_CIRCUIT_ONE_BUS -> QuotaType.SHORT_CIRCUIT; case ASYMMETRICAL_LOAD -> QuotaType.ASYMMETRICAL_LOAD; + case LOGICAL_CONTROLS -> null; // no quota for logical controls }; } } diff --git a/src/main/java/org/gridsuite/study/server/dto/RemoteDeletionInfos.java b/src/main/java/org/gridsuite/study/server/dto/RemoteDeletionInfos.java index addb50c0c2..87ec1c83a9 100644 --- a/src/main/java/org/gridsuite/study/server/dto/RemoteDeletionInfos.java +++ b/src/main/java/org/gridsuite/study/server/dto/RemoteDeletionInfos.java @@ -33,6 +33,7 @@ public class RemoteDeletionInfos { protected Set stateEstimationResultUuids = new HashSet<>(); protected Set pccMinResultUuids = new HashSet<>(); protected Set asymmetricalLoadResultUuids = new HashSet<>(); + protected Set logicalControlsResultUuids = new HashSet<>(); public List getReportUuids() { return reportUuids.stream().toList(); @@ -74,6 +75,10 @@ public List getAsymmetricalLoadResultUuids() { return asymmetricalLoadResultUuids.stream().toList(); } + public List getLogicalControlsResultUuids() { + return logicalControlsResultUuids.stream().toList(); + } + public List getDynamicSimulationResultUuids() { return dynamicSimulationResultUuids.stream().toList(); } @@ -138,4 +143,8 @@ public void addAsymmetricalLoadResultUuid(UUID asymmetricalLoadResultUuid) { asymmetricalLoadResultUuids.add(asymmetricalLoadResultUuid); } + public void addLogicalControlsResultUuid(UUID logicalControlsResultUuid) { + logicalControlsResultUuids.add(logicalControlsResultUuid); + } + } diff --git a/src/main/java/org/gridsuite/study/server/dto/RootNetworkNodeInfo.java b/src/main/java/org/gridsuite/study/server/dto/RootNetworkNodeInfo.java index 8ac9c59dce..d073b97234 100644 --- a/src/main/java/org/gridsuite/study/server/dto/RootNetworkNodeInfo.java +++ b/src/main/java/org/gridsuite/study/server/dto/RootNetworkNodeInfo.java @@ -56,6 +56,8 @@ public class RootNetworkNodeInfo { private UUID asymmetricalLoadResultUuid; + private UUID logicalControlsResultUuid; + private NodeBuildStatus nodeBuildStatus; private List nodeExportNetwork; @@ -77,6 +79,7 @@ public RootNetworkNodeInfoEntity toEntity() { .dynamicSecurityAnalysisResultUuid(dynamicSecurityAnalysisResultUuid) .dynamicMarginCalculationResultUuid(dynamicMarginCalculationResultUuid) .stateEstimationResultUuid(stateEstimationResultUuid) + .logicalControlsResultUuid(logicalControlsResultUuid) .pccMinResultUuid(pccMinResultUuid) .nodeBuildStatus(nodeBuildStatus.toEntity()) .build(); diff --git a/src/main/java/org/gridsuite/study/server/networkmodificationtree/entities/RootNetworkNodeInfoEntity.java b/src/main/java/org/gridsuite/study/server/networkmodificationtree/entities/RootNetworkNodeInfoEntity.java index 350ced0926..c3f7929083 100644 --- a/src/main/java/org/gridsuite/study/server/networkmodificationtree/entities/RootNetworkNodeInfoEntity.java +++ b/src/main/java/org/gridsuite/study/server/networkmodificationtree/entities/RootNetworkNodeInfoEntity.java @@ -106,6 +106,9 @@ public class RootNetworkNodeInfoEntity { @Column(name = "asymmetricalLoadResultUuid") private UUID asymmetricalLoadResultUuid; + @Column(name = "logicalControlsResultUuid") + private UUID logicalControlsResultUuid; + @Embedded @AttributeOverrides(value = { @AttributeOverride(name = "localBuildStatus", column = @Column(name = "localBuildStatus", nullable = false)), @@ -129,6 +132,7 @@ public RootNetworkNodeInfo toDto() { .stateEstimationResultUuid(stateEstimationResultUuid) .pccMinResultUuid(pccMinResultUuid) .asymmetricalLoadResultUuid(asymmetricalLoadResultUuid) + .logicalControlsResultUuid(logicalControlsResultUuid) .sensitivityAnalysisResultUuid(sensitivityAnalysisResultUuid) .voltageInitResultUuid(voltageInitResultUuid) .shortCircuitAnalysisResultUuid(shortCircuitAnalysisResultUuid) diff --git a/src/main/java/org/gridsuite/study/server/notification/NotificationService.java b/src/main/java/org/gridsuite/study/server/notification/NotificationService.java index cca368759b..01815f0195 100644 --- a/src/main/java/org/gridsuite/study/server/notification/NotificationService.java +++ b/src/main/java/org/gridsuite/study/server/notification/NotificationService.java @@ -110,6 +110,9 @@ public class NotificationService { public static final String UPDATE_TYPE_ASYMMETRICAL_LOAD_FAILED = "asymmetricalLoad_failed"; public static final String UPDATE_TYPE_ASYMMETRICAL_LOAD_RESULT = "asymmetricalLoadResult"; public static final String UPDATE_TYPE_ASYMMETRICAL_LOAD_STATUS = "asymmetricalLoad_status"; + public static final String UPDATE_TYPE_LOGICAL_CONTROLS_FAILED = "logicalControls_failed"; + public static final String UPDATE_TYPE_LOGICAL_CONTROLS_RESULT = "logicalControlsResult"; + public static final String UPDATE_TYPE_LOGICAL_CONTROLS_STATUS = "logicalControls_status"; public static final String UPDATE_TYPE_COMPUTATION_PARAMETERS = "computationParametersUpdated"; public static final String UPDATE_COMPUTATION_RESULT_COLUMN_FILTER = "computationResultColumnFilterUpdated"; public static final String UPDATE_COMPUTATION_RESULT_GLOBAL_FILTER = "computationResultGlobalFilterUpdated"; diff --git a/src/main/java/org/gridsuite/study/server/repository/rootnetwork/RootNetworkNodeInfoRepository.java b/src/main/java/org/gridsuite/study/server/repository/rootnetwork/RootNetworkNodeInfoRepository.java index be89e18b75..6e03ef9ad6 100644 --- a/src/main/java/org/gridsuite/study/server/repository/rootnetwork/RootNetworkNodeInfoRepository.java +++ b/src/main/java/org/gridsuite/study/server/repository/rootnetwork/RootNetworkNodeInfoRepository.java @@ -46,6 +46,8 @@ public interface RootNetworkNodeInfoRepository extends JpaRepository findAllByAsymmetricalLoadResultUuidNotNull(); + List findAllByLogicalControlsResultUuidNotNull(); + List findAllByNodeInfoId(UUID nodeInfoId); List findAllByRootNetworkId(UUID rootNetworkUuid); diff --git a/src/main/java/org/gridsuite/study/server/service/ConsumerService.java b/src/main/java/org/gridsuite/study/server/service/ConsumerService.java index 4fe5a8ffe8..4e37fcef9c 100644 --- a/src/main/java/org/gridsuite/study/server/service/ConsumerService.java +++ b/src/main/java/org/gridsuite/study/server/service/ConsumerService.java @@ -407,10 +407,10 @@ public void consumeCalculationFailed(Message msg, ComputationType comput } private void handleQuotaEnd(ComputationType computationType, String userId, UUID resultUuid) { - if (!shouldCheckOperationQuotas) { + QuotaType quotaType = QuotaType.mapFromComputationType(computationType); + if (!shouldCheckOperationQuotas || quotaType == null) { return; } - QuotaType quotaType = QuotaType.mapFromComputationType(computationType); userAdminService.releaseQuota(userId, resultUuid); notificationService.emitQuotaChange(userId, quotaType); } @@ -839,6 +839,26 @@ public Consumer> consumeAsymmetricalLoadFailed() { return message -> consumeCalculationFailed(message, ASYMMETRICAL_LOAD); } + @Bean + public Consumer> consumeLogicalControlsResult() { + return message -> consumeCalculationResult(message, LOGICAL_CONTROLS); + } + + @Bean + public Consumer> consumeLogicalControlsRunning() { + return message -> consumeCalculationRunning(message, LOGICAL_CONTROLS); + } + + @Bean + public Consumer> consumeLogicalControlsStopped() { + return message -> consumeCalculationStopped(message, LOGICAL_CONTROLS); + } + + @Bean + public Consumer> consumeLogicalControlsFailed() { + return message -> consumeCalculationFailed(message, LOGICAL_CONTROLS); + } + public void consumeNetworkExportFinished(Message msg) { String receiverString = msg.getHeaders().get(HEADER_RECEIVER, String.class); String s3Key = msg.getHeaders().get(HEADER_S3_KEY, String.class); diff --git a/src/main/java/org/gridsuite/study/server/service/RootNetworkNodeInfoService.java b/src/main/java/org/gridsuite/study/server/service/RootNetworkNodeInfoService.java index 1a569a9cba..4d2ff0a57b 100644 --- a/src/main/java/org/gridsuite/study/server/service/RootNetworkNodeInfoService.java +++ b/src/main/java/org/gridsuite/study/server/service/RootNetworkNodeInfoService.java @@ -33,6 +33,7 @@ import org.gridsuite.study.server.service.dynamicsecurityanalysis.DynamicSecurityAnalysisRestService; import org.gridsuite.study.server.service.dynamicsimulation.DynamicSimulationRestService; import org.gridsuite.study.server.service.loadflow.LoadFlowRestService; +import org.gridsuite.study.server.service.logicalcontrols.LogicalControlsRestService; import org.gridsuite.study.server.service.pccmin.PccMinRestService; import org.gridsuite.study.server.service.securityanalysis.SecurityAnalysisRestService; import org.gridsuite.study.server.service.securityanalysis.SecurityAnalysisResultType; @@ -82,6 +83,7 @@ public class RootNetworkNodeInfoService { private final PccMinRestService pccMinRestService; private final ReportService reportService; private final AsymmetricalLoadRestService asymmetricalLoadRestService; + private final LogicalControlsRestService logicalControlsRestService; private final UserAdminService userAdminService; public RootNetworkNodeInfoService(RootNetworkNodeInfoRepository rootNetworkNodeInfoRepository, @@ -98,6 +100,7 @@ public RootNetworkNodeInfoService(RootNetworkNodeInfoRepository rootNetworkNodeI StateEstimationRestService stateEstimationService, PccMinRestService pccMinService, AsymmetricalLoadRestService asymmetricalLoadRestService, + LogicalControlsRestService logicalControlsRestService, ReportService reportService, UserAdminService userAdminService) { this.rootNetworkNodeInfoRepository = rootNetworkNodeInfoRepository; @@ -115,6 +118,7 @@ public RootNetworkNodeInfoService(RootNetworkNodeInfoRepository rootNetworkNodeI this.pccMinRestService = pccMinService; this.reportService = reportService; this.asymmetricalLoadRestService = asymmetricalLoadRestService; + this.logicalControlsRestService = logicalControlsRestService; this.userAdminService = userAdminService; } @@ -169,6 +173,7 @@ public void updateComputationResultUuid(UUID nodeUuid, UUID rootNetworkUuid, UUI case STATE_ESTIMATION -> rootNetworkNodeInfoEntity.setStateEstimationResultUuid(computationResultUuid); case PCC_MIN -> rootNetworkNodeInfoEntity.setPccMinResultUuid(computationResultUuid); case ASYMMETRICAL_LOAD -> rootNetworkNodeInfoEntity.setAsymmetricalLoadResultUuid(computationResultUuid); + case LOGICAL_CONTROLS -> rootNetworkNodeInfoEntity.setLogicalControlsResultUuid(computationResultUuid); } } @@ -236,6 +241,11 @@ public void fillDeleteNodeInfo(UUID nodeUuid, DeleteNodeInfos deleteNodeInfos) { deleteNodeInfos.addStateEstimationResultUuid(stateEstimationResultUuid); } + UUID logicalControlsResultUuid = getComputationResultUuid(rootNetworkNodeInfoEntity, LOGICAL_CONTROLS); + if (logicalControlsResultUuid != null) { + deleteNodeInfos.addLogicalControlsResultUuid(logicalControlsResultUuid); + } + UUID pccMinResultUuid = getComputationResultUuid(rootNetworkNodeInfoEntity, PCC_MIN); if (pccMinResultUuid != null) { deleteNodeInfos.addPccMinResultUuid(pccMinResultUuid); @@ -379,6 +389,7 @@ private void invalidateComputationResults(RootNetworkNodeInfoEntity rootNetworkN rootNetworkNodeInfoEntity.setStateEstimationResultUuid(null); rootNetworkNodeInfoEntity.setPccMinResultUuid(null); rootNetworkNodeInfoEntity.setAsymmetricalLoadResultUuid(null); + rootNetworkNodeInfoEntity.setLogicalControlsResultUuid(null); Map computationReports = rootNetworkNodeInfoEntity.getComputationReports() .entrySet() @@ -438,6 +449,8 @@ private void fillComputationResultUuids(RootNetworkNodeInfoEntity rootNetworkNod .ifPresent(invalidateNodeInfos::addPccMinResultUuid); Optional.ofNullable(getComputationResultUuid(rootNetworkNodeInfoEntity, ASYMMETRICAL_LOAD)) .ifPresent(invalidateNodeInfos::addAsymmetricalLoadResultUuid); + Optional.ofNullable(getComputationResultUuid(rootNetworkNodeInfoEntity, LOGICAL_CONTROLS)) + .ifPresent(invalidateNodeInfos::addLogicalControlsResultUuid); } // TODO : Remove optionnal and throws ROOT_NETWORK_NOT_FOUND exception @@ -459,6 +472,7 @@ private static UUID getComputationResultUuid(RootNetworkNodeInfoEntity rootNetwo case STATE_ESTIMATION -> rootNetworkNodeInfoEntity.getStateEstimationResultUuid(); case PCC_MIN -> rootNetworkNodeInfoEntity.getPccMinResultUuid(); case ASYMMETRICAL_LOAD -> rootNetworkNodeInfoEntity.getAsymmetricalLoadResultUuid(); + case LOGICAL_CONTROLS -> rootNetworkNodeInfoEntity.getLogicalControlsResultUuid(); }; } @@ -522,6 +536,9 @@ public void updateRootNetworkNode(UUID nodeUuid, UUID rootNetworkUuid, RootNetwo if (rootNetworkNodeInfo.getAsymmetricalLoadResultUuid() != null) { rootNetworkNodeInfoEntity.setAsymmetricalLoadResultUuid(rootNetworkNodeInfo.getAsymmetricalLoadResultUuid()); } + if (rootNetworkNodeInfo.getLogicalControlsResultUuid() != null) { + rootNetworkNodeInfoEntity.setLogicalControlsResultUuid(rootNetworkNodeInfo.getLogicalControlsResultUuid()); + } if (rootNetworkNodeInfo.getDynamicSimulationResultUuid() != null) { rootNetworkNodeInfoEntity.setDynamicSimulationResultUuid(rootNetworkNodeInfo.getDynamicSimulationResultUuid()); } @@ -553,7 +570,8 @@ public List> getRemoteDeletions(RemoteDeletionInfos infos) studyServerExecutionService.runAsync(() -> dynamicMarginCalculationRestService.deleteResults(infos.getDynamicMarginCalculationResultUuids())), studyServerExecutionService.runAsync(() -> stateEstimationRestService.deleteStateEstimationResults(infos.getStateEstimationResultUuids())), studyServerExecutionService.runAsync(() -> pccMinRestService.deletePccMinResults(infos.getPccMinResultUuids())), - studyServerExecutionService.runAsync(() -> asymmetricalLoadRestService.deleteAsymmetricalLoadResults(infos.getAsymmetricalLoadResultUuids())) + studyServerExecutionService.runAsync(() -> asymmetricalLoadRestService.deleteAsymmetricalLoadResults(infos.getAsymmetricalLoadResultUuids())), + studyServerExecutionService.runAsync(() -> logicalControlsRestService.deleteLogicalControlsResults(infos.getLogicalControlsResultUuids())) ); } @@ -574,6 +592,7 @@ public RemoteDeletionInfos getRemoteDeletionInfos(List root infos.setStateEstimationResultUuids(rootNetworkNodeInfos.stream().map(RootNetworkNodeInfo::getStateEstimationResultUuid).filter(Objects::nonNull).collect(Collectors.toSet())); infos.setPccMinResultUuids(rootNetworkNodeInfos.stream().map(RootNetworkNodeInfo::getPccMinResultUuid).filter(Objects::nonNull).collect(Collectors.toSet())); infos.setAsymmetricalLoadResultUuids(rootNetworkNodeInfos.stream().map(RootNetworkNodeInfo::getAsymmetricalLoadResultUuid).filter(Objects::nonNull).collect(Collectors.toSet())); + infos.setLogicalControlsResultUuids(rootNetworkNodeInfos.stream().map(RootNetworkNodeInfo::getLogicalControlsResultUuid).filter(Objects::nonNull).collect(Collectors.toSet())); return infos; } @@ -752,6 +771,12 @@ public String getAsymmetricalLoadResult(UUID nodeUuid, UUID rootNetworkUuid, Str return asymmetricalLoadRestService.getAsymmetricalLoadResultsPage(asymmetricalLoadParameters, filters, globalFilters, pageable); } + @Transactional(readOnly = true) + public String getLogicalControlsResult(UUID nodeUuid, UUID rootNetworkUuid) { + UUID resultUuid = getComputationResultUuid(nodeUuid, rootNetworkUuid, LOGICAL_CONTROLS); + return logicalControlsRestService.getLogicalControlsResult(resultUuid); + } + /************************** * GET COMPUTATION STATUS * **************************/ @@ -862,6 +887,12 @@ public String getAsymmetricalLoadStatus(UUID nodeUuid, UUID rootNetworkUuid) { return asymmetricalLoadRestService.getAsymmetricalLoadStatus(resultUuid); } + @Transactional(readOnly = true) + public String getLogicalControlsStatus(UUID nodeUuid, UUID rootNetworkUuid) { + UUID resultUuid = getComputationResultUuid(nodeUuid, rootNetworkUuid, LOGICAL_CONTROLS); + return logicalControlsRestService.getLogicalControlsStatus(resultUuid); + } + /******************************* * STOP COMPUTATION EXECUTIONS * *******************************/ @@ -944,4 +975,10 @@ public void stopAsymmetricalLoad(UUID studyUuid, UUID nodeUuid, UUID rootNetwork UUID resultUuid = getComputationResultUuid(nodeUuid, rootNetworkUuid, ASYMMETRICAL_LOAD); asymmetricalLoadRestService.stopAsymmetricalLoad(studyUuid, nodeUuid, rootNetworkUuid, resultUuid); } + + @Transactional + public void stopLogicalControls(UUID studyUuid, UUID nodeUuid, UUID rootNetworkUuid) { + UUID resultUuid = getComputationResultUuid(nodeUuid, rootNetworkUuid, LOGICAL_CONTROLS); + logicalControlsRestService.stopLogicalControls(studyUuid, nodeUuid, rootNetworkUuid, resultUuid); + } } diff --git a/src/main/java/org/gridsuite/study/server/service/StudyService.java b/src/main/java/org/gridsuite/study/server/service/StudyService.java index 0c97752297..35aa441721 100644 --- a/src/main/java/org/gridsuite/study/server/service/StudyService.java +++ b/src/main/java/org/gridsuite/study/server/service/StudyService.java @@ -148,7 +148,8 @@ public enum ReportType { VOLTAGE_INITIALIZATION("VoltageInit"), STATE_ESTIMATION("StateEstimation"), PCC_MIN("PccMin"), - ASYMMETRICAL_LOAD("AsymmetricalLoad"); + ASYMMETRICAL_LOAD("AsymmetricalLoad"), + LOGICAL_CONTROLS("LogicalControls"); public final String reportKey; @@ -2474,6 +2475,7 @@ public Map getAllComputationsStatus(@NonNull UUID study allComputationStatus.put(SECURITY_ANALYSIS, rootNetworkNodeInfoService.getSecurityAnalysisStatus(nodeUuid, rootNetworkUuid)); allComputationStatus.put(PCC_MIN, rootNetworkNodeInfoService.getPccMinStatus(nodeUuid, rootNetworkUuid)); allComputationStatus.put(ASYMMETRICAL_LOAD, rootNetworkNodeInfoService.getAsymmetricalLoadStatus(nodeUuid, rootNetworkUuid)); + allComputationStatus.put(LOGICAL_CONTROLS, rootNetworkNodeInfoService.getLogicalControlsStatus(nodeUuid, rootNetworkUuid)); allComputationStatus.put(DYNAMIC_MARGIN_CALCULATION, rootNetworkNodeInfoService.getDynamicMarginCalculationStatus(nodeUuid, rootNetworkUuid)); allComputationStatus.put(DYNAMIC_SECURITY_ANALYSIS, rootNetworkNodeInfoService.getDynamicSecurityAnalysisStatus(nodeUuid, rootNetworkUuid)); allComputationStatus.put(DYNAMIC_SIMULATION, rootNetworkNodeInfoService.getDynamicSimulationStatus(nodeUuid, rootNetworkUuid)); diff --git a/src/main/java/org/gridsuite/study/server/service/SupervisionService.java b/src/main/java/org/gridsuite/study/server/service/SupervisionService.java index b0434d0f37..e4020ad151 100644 --- a/src/main/java/org/gridsuite/study/server/service/SupervisionService.java +++ b/src/main/java/org/gridsuite/study/server/service/SupervisionService.java @@ -27,6 +27,7 @@ import org.gridsuite.study.server.service.dynamicsimulation.DynamicSimulationRestService; import org.gridsuite.study.server.service.loadflow.LoadFlowRestService; import org.gridsuite.study.server.service.loadflow.LoadFlowService; +import org.gridsuite.study.server.service.logicalcontrols.LogicalControlsRestService; import org.gridsuite.study.server.service.networkmodification.NetworkModificationRestService; import org.gridsuite.study.server.service.pccmin.PccMinRestService; import org.gridsuite.study.server.service.securityanalysis.SecurityAnalysisRestService; @@ -95,6 +96,8 @@ public class SupervisionService { private final AsymmetricalLoadRestService asymmetricalLoadRestService; + private final LogicalControlsRestService logicalControlsRestService; + private final ElasticsearchOperations elasticsearchOperations; private final StudyInfosService studyInfosService; @@ -123,6 +126,7 @@ public SupervisionService(StudyService studyService, VoltageInitRestService voltageInitService, EquipmentInfosService equipmentInfosService, AsymmetricalLoadRestService asymmetricalLoadRestService, + LogicalControlsRestService logicalControlsRestService, StateEstimationRestService stateEstimationService, PccMinRestService pccMinService, ElasticsearchOperations elasticsearchOperations, @@ -150,6 +154,7 @@ public SupervisionService(StudyService studyService, this.stateEstimationService = stateEstimationService; this.pccMinService = pccMinService; this.asymmetricalLoadRestService = asymmetricalLoadRestService; + this.logicalControlsRestService = logicalControlsRestService; this.elasticsearchOperations = elasticsearchOperations; this.studyInfosService = studyInfosService; this.rootNetworkService = rootNetworkService; @@ -183,6 +188,8 @@ public Integer deleteComputationResults(ComputationType computationType, boolean dryRun ? pccMinService.getPccMinResultsCount() : deletePccMinResults(); case ASYMMETRICAL_LOAD -> dryRun ? asymmetricalLoadRestService.getAsymmetricalLoadResultsCount() : deleteAsymmetricalLoadResults(); + case LOGICAL_CONTROLS -> + dryRun ? logicalControlsRestService.getLogicalControlsResultsCount() : deleteLogicalControlsResults(); }; } @@ -407,6 +414,22 @@ private Integer deleteAsymmetricalLoadResults() { return rootNetworkNodeInfoEntities.size(); } + private Integer deleteLogicalControlsResults() { + AtomicReference startTime = new AtomicReference<>(); + startTime.set(System.nanoTime()); + List rootNetworkNodeInfoEntities = rootNetworkNodeInfoRepository.findAllByLogicalControlsResultUuidNotNull(); + List reportsToDelete = new ArrayList<>(); + rootNetworkNodeInfoEntities.forEach(rootNetworkNodeInfo -> { + rootNetworkNodeInfo.setLogicalControlsResultUuid(null); + reportsToDelete.add(rootNetworkNodeInfo.getComputationReports().get(ComputationType.LOGICAL_CONTROLS.name())); + rootNetworkNodeInfo.getComputationReports().remove(ComputationType.LOGICAL_CONTROLS.name()); + }); + reportService.deleteReports(reportsToDelete); + logicalControlsRestService.deleteAllLogicalControlsResults(); + LOGGER.trace(DELETION_LOG_MESSAGE, ComputationType.LOGICAL_CONTROLS, TimeUnit.NANOSECONDS.toSeconds(System.nanoTime() - startTime.get())); + return rootNetworkNodeInfoEntities.size(); + } + @Transactional public void unbuildAllNodes(UUID studyUuid) { AtomicReference startTime = new AtomicReference<>(); diff --git a/src/main/java/org/gridsuite/study/server/service/logicalcontrols/LogicalControlsRestService.java b/src/main/java/org/gridsuite/study/server/service/logicalcontrols/LogicalControlsRestService.java new file mode 100644 index 0000000000..e20d3cf7d3 --- /dev/null +++ b/src/main/java/org/gridsuite/study/server/service/logicalcontrols/LogicalControlsRestService.java @@ -0,0 +1,145 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.study.server.service.logicalcontrols; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.lang3.StringUtils; +import org.gridsuite.study.server.RemoteServicesProperties; +import org.gridsuite.study.server.dto.NodeReceiver; +import org.gridsuite.study.server.dto.ReportInfos; +import org.gridsuite.study.server.service.StudyService; +import org.gridsuite.study.server.service.common.AbstractComputationRestService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.UriComponentsBuilder; + +import java.io.UncheckedIOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Objects; +import java.util.UUID; + +import static org.gridsuite.study.server.StudyConstants.*; + +/** + * @author David Braquart + */ +@Service +public class LogicalControlsRestService extends AbstractComputationRestService { + static final String RESULTS = "results"; + private static final String LOGICAL_CONTROLS = "logical-controls"; + private static final String LOGICAL_CONTROLS_URI = DELIMITER + STATE_ESTIMATION_API_VERSION + DELIMITER + LOGICAL_CONTROLS; + + private final ObjectMapper objectMapper; + + @Autowired + public LogicalControlsRestService(RemoteServicesProperties remoteServicesProperties, ObjectMapper objectMapper, RestTemplate restTemplate) { + super(remoteServicesProperties.getServiceUri("state-estimation-server"), restTemplate); + this.objectMapper = objectMapper; + } + + public UUID runLogicalControls(UUID networkUuid, String variantId, UUID parametersUuid, ReportInfos reportInfos, String receiver, String userId) { + var uriComponentsBuilder = UriComponentsBuilder + .fromPath(LOGICAL_CONTROLS_URI + DELIMITER + "networks/{networkUuid}/run-and-save") + .queryParam(QUERY_PARAM_REPORT_UUID, reportInfos.reportUuid().toString()) + .queryParam(QUERY_PARAM_REPORTER_ID, reportInfos.nodeUuid()) + .queryParam(QUERY_PARAM_REPORT_TYPE, StudyService.ReportType.LOGICAL_CONTROLS.reportKey); + if (parametersUuid != null) { + uriComponentsBuilder.queryParam(QUERY_PARAM_PARAMETERS_UUID, parametersUuid.toString()); + } + + if (!StringUtils.isBlank(variantId)) { + uriComponentsBuilder.queryParam(QUERY_PARAM_VARIANT_ID, variantId); + } + var path = uriComponentsBuilder.queryParam(QUERY_PARAM_RECEIVER, receiver).buildAndExpand(networkUuid).toUriString(); + + HttpHeaders headers = new HttpHeaders(); + headers.set(HEADER_USER_ID, userId); + headers.setContentType(MediaType.APPLICATION_JSON); + + HttpEntity httpEntity = new HttpEntity<>(null, headers); + + return restTemplate.exchange(baseUri + path, HttpMethod.POST, httpEntity, UUID.class).getBody(); + } + + public void stopLogicalControls(UUID studyUuid, UUID nodeUuid, UUID rootNetworkUuid, UUID resultUuid) { + Objects.requireNonNull(studyUuid); + Objects.requireNonNull(nodeUuid); + + if (resultUuid == null) { + return; + } + + String receiver; + try { + receiver = URLEncoder.encode(objectMapper.writeValueAsString(new NodeReceiver(nodeUuid, rootNetworkUuid)), StandardCharsets.UTF_8); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } + + String path = UriComponentsBuilder + .fromPath(LOGICAL_CONTROLS_URI + DELIMITER + "results/{resultUuid}/stop") + .queryParam(QUERY_PARAM_RECEIVER, receiver).buildAndExpand(resultUuid).toUriString(); + + restTemplate.put(baseUri + path, Void.class); + } + + public String getLogicalControlsStatus(UUID resultUuid) { + if (resultUuid == null) { + return null; + } + String path = UriComponentsBuilder + .fromPath(LOGICAL_CONTROLS_URI + DELIMITER + "results/{resultUuid}/status") + .buildAndExpand(resultUuid).toUriString(); + return restTemplate.getForObject(baseUri + path, String.class); + } + + public void invalidateLogicalControlsStatus(List resultUuids) { + if (!resultUuids.isEmpty()) { + String path = UriComponentsBuilder + .fromPath(LOGICAL_CONTROLS_URI + DELIMITER + "results/invalidate-status") + .queryParam("resultUuid", resultUuids).build().toUriString(); + restTemplate.put(baseUri + path, Void.class); + } + } + + public void deleteLogicalControlsResults(List resultsUuids) { + deleteCalculationResults(resultsUuids, LOGICAL_CONTROLS_URI + "/results", restTemplate, baseUri); + } + + public void deleteAllLogicalControlsResults() { + deleteLogicalControlsResults(null); + } + + public Integer getLogicalControlsResultsCount() { + String path = UriComponentsBuilder + .fromPath(DELIMITER + STATE_ESTIMATION_API_VERSION + DELIMITER + "supervision/logical-controls/results-count").toUriString(); + return restTemplate.getForObject(baseUri + path, Integer.class); + } + + @Override + public List getEnumValues(String enumName, UUID resultUuidOpt) { + return List.of(); + } + + public String getLogicalControlsResult(UUID resultUuid) { + if (resultUuid == null) { + return null; + } + String path = UriComponentsBuilder + .fromPath(LOGICAL_CONTROLS_URI + DELIMITER + RESULTS + "/{resultUuid}") + .buildAndExpand(resultUuid).toUriString(); + return restTemplate.getForObject(baseUri + path, String.class); + } +} diff --git a/src/main/java/org/gridsuite/study/server/service/logicalcontrols/LogicalControlsService.java b/src/main/java/org/gridsuite/study/server/service/logicalcontrols/LogicalControlsService.java new file mode 100644 index 0000000000..67ef092ff5 --- /dev/null +++ b/src/main/java/org/gridsuite/study/server/service/logicalcontrols/LogicalControlsService.java @@ -0,0 +1,83 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.study.server.service.logicalcontrols; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.NonNull; +import org.gridsuite.study.server.dto.NodeReceiver; +import org.gridsuite.study.server.dto.ReportInfos; +import org.gridsuite.study.server.notification.NotificationService; +import org.gridsuite.study.server.repository.StudyEntity; +import org.gridsuite.study.server.repository.StudyRepository; +import org.gridsuite.study.server.service.NetworkModificationTreeService; +import org.gridsuite.study.server.service.RootNetworkNodeInfoService; +import org.gridsuite.study.server.service.RootNetworkService; +import org.gridsuite.study.server.service.UserAdminService; +import org.gridsuite.study.server.service.common.AbstractComputationService; +import org.gridsuite.study.server.service.common.ComputationParametersService; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.io.UncheckedIOException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.UUID; + +import static org.gridsuite.study.server.dto.ComputationType.LOGICAL_CONTROLS; + +/** + * @author David Braquart + */ +@Service +public class LogicalControlsService extends AbstractComputationService { + + private final LogicalControlsRestService logicalControlsRestService; + private final ObjectMapper objectMapper; + + protected LogicalControlsService(StudyRepository studyRepository, + ComputationParametersService computationParametersService, + NotificationService notificationService, + RootNetworkNodeInfoService rootNetworkNodeInfoService, + LogicalControlsRestService logicalControlsRestService, + NetworkModificationTreeService networkModificationTreeService, + UserAdminService userAdminService, + ObjectMapper objectMapper, + RootNetworkService rootNetworkService) { + super(studyRepository, notificationService, networkModificationTreeService, rootNetworkNodeInfoService, rootNetworkService, + computationParametersService, userAdminService); + this.logicalControlsRestService = logicalControlsRestService; + this.objectMapper = objectMapper; + } + + @Transactional + public UUID runLogicalControls(@NonNull UUID studyUuid, @NonNull UUID nodeUuid, @NonNull UUID rootNetworkUuid, String userId) { + StudyEntity studyEntity = getStudy(studyUuid); + UUID networkUuid = rootNetworkService.getNetworkUuid(rootNetworkUuid); + String variantId = networkModificationTreeService.getVariantId(nodeUuid, rootNetworkUuid); + UUID reportUuid = networkModificationTreeService.getComputationReports(nodeUuid, rootNetworkUuid).getOrDefault(LOGICAL_CONTROLS.name(), UUID.randomUUID()); + networkModificationTreeService.updateComputationReportUuid(nodeUuid, rootNetworkUuid, LOGICAL_CONTROLS, reportUuid); + String receiver; + try { + receiver = URLEncoder.encode(objectMapper.writeValueAsString(new NodeReceiver(nodeUuid, rootNetworkUuid)), StandardCharsets.UTF_8); + } catch (JsonProcessingException e) { + throw new UncheckedIOException(e); + } + + UUID prevResultUuid = rootNetworkNodeInfoService.getComputationResultUuid(nodeUuid, rootNetworkUuid, LOGICAL_CONTROLS); + if (prevResultUuid != null) { + logicalControlsRestService.deleteLogicalControlsResults(List.of(prevResultUuid)); + } + + UUID result = logicalControlsRestService.runLogicalControls(networkUuid, variantId, studyEntity.getStateEstimationParametersUuid(), new ReportInfos(reportUuid, nodeUuid), receiver, userId); + updateComputationResultUuid(nodeUuid, rootNetworkUuid, result, LOGICAL_CONTROLS); + notificationService.emitStudyChanged(studyEntity.getId(), nodeUuid, rootNetworkUuid, NotificationService.UPDATE_TYPE_LOGICAL_CONTROLS_STATUS); + notificationService.emitElementUpdated(studyEntity.getId(), userId); + return result; + } +} diff --git a/src/main/java/org/gridsuite/study/server/service/stateestimation/StateEstimationRestService.java b/src/main/java/org/gridsuite/study/server/service/stateestimation/StateEstimationRestService.java index 4afc7eddb8..56db23f356 100644 --- a/src/main/java/org/gridsuite/study/server/service/stateestimation/StateEstimationRestService.java +++ b/src/main/java/org/gridsuite/study/server/service/stateestimation/StateEstimationRestService.java @@ -232,21 +232,4 @@ public ResponseEntity downloadDebugFile(UUID resultUuid) { .buildAndExpand(resultUuid).toUriString(); return getRestTemplate().exchange(getBaseUri() + path, HttpMethod.GET, null, Resource.class); } - - public String runLogicalComputation(UUID networkUuid, String variantId, UUID parametersUuid) { - var uriComponentsBuilder = UriComponentsBuilder.fromPath(DELIMITER + STATE_ESTIMATION_API_VERSION + "/networks/{networkUuid}/logical-controls"); - if (parametersUuid != null) { - uriComponentsBuilder.queryParam("parametersUuid", parametersUuid.toString()); - } - if (!StringUtils.isBlank(variantId)) { - uriComponentsBuilder.queryParam(QUERY_PARAM_VARIANT_ID, variantId); - } - var path = uriComponentsBuilder.buildAndExpand(networkUuid).toUriString(); - - HttpHeaders headers = new HttpHeaders(); - headers.setContentType(MediaType.APPLICATION_JSON); - HttpEntity httpEntity = new HttpEntity<>(null, headers); - - return restTemplate.exchange(baseUri + path, HttpMethod.POST, httpEntity, String.class).getBody(); - } } diff --git a/src/main/java/org/gridsuite/study/server/service/stateestimation/StateEstimationService.java b/src/main/java/org/gridsuite/study/server/service/stateestimation/StateEstimationService.java index f28fe03c9c..a3d0938834 100644 --- a/src/main/java/org/gridsuite/study/server/service/stateestimation/StateEstimationService.java +++ b/src/main/java/org/gridsuite/study/server/service/stateestimation/StateEstimationService.java @@ -16,6 +16,7 @@ import org.gridsuite.study.server.service.*; import org.gridsuite.study.server.service.common.AbstractComputationService; import org.gridsuite.study.server.service.common.ComputationParametersService; +import org.gridsuite.study.server.service.logicalcontrols.LogicalControlsRestService; import org.springframework.core.io.Resource; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -27,6 +28,7 @@ import java.util.List; import java.util.UUID; +import static org.gridsuite.study.server.dto.ComputationType.LOGICAL_CONTROLS; import static org.gridsuite.study.server.dto.ComputationType.STATE_ESTIMATION; /** @@ -36,6 +38,7 @@ @Service public class StateEstimationService extends AbstractComputationService { private final StateEstimationRestService stateEstimationRestService; + private final LogicalControlsRestService logicalControlsRestService; private final ObjectMapper objectMapper; protected StateEstimationService(StudyRepository studyRepository, @@ -44,10 +47,12 @@ protected StateEstimationService(StudyRepository studyRepository, RootNetworkNodeInfoService rootNetworkNodeInfoService, NetworkModificationTreeService networkModificationTreeService, RootNetworkService rootNetworkService, StateEstimationRestService stateEstimationRestService, + LogicalControlsRestService logicalControlsRestService, UserAdminService userAdminService, ObjectMapper objectMapper) { super(studyRepository, notificationService, networkModificationTreeService, rootNetworkNodeInfoService, rootNetworkService, computationParametersService, userAdminService); this.stateEstimationRestService = stateEstimationRestService; + this.logicalControlsRestService = logicalControlsRestService; this.objectMapper = objectMapper; } @@ -95,6 +100,8 @@ public String getStateEstimationParameters(UUID studyUuid) { @Transactional public void setStateEstimationParametersValues(UUID studyUuid, String parameters, String userId) { + // note: changing state-estimation parameters invalidates both state estimation and logical controls results + // (Logical controls use quality thresholds from state-estimation parameters) setComputationParameters( studyUuid, parameters, @@ -104,8 +111,9 @@ public void setStateEstimationParametersValues(UUID studyUuid, String parameters stateEstimationRestService::createStateEstimationParameters, stateEstimationRestService::updateStateEstimationParameters, STATE_ESTIMATION, - List.of(this::invalidateStateEstimationStatusOnAllNodes), - NotificationService.UPDATE_TYPE_STATE_ESTIMATION_STATUS + List.of(this::invalidateStateEstimationStatusOnAllNodes, this::invalidateLogicalControlsStatusOnAllNodes), + NotificationService.UPDATE_TYPE_STATE_ESTIMATION_STATUS, + NotificationService.UPDATE_TYPE_LOGICAL_CONTROLS_STATUS ); } @@ -113,15 +121,11 @@ private void invalidateStateEstimationStatusOnAllNodes(UUID studyUuid) { stateEstimationRestService.invalidateStateEstimationStatus(rootNetworkNodeInfoService.getComputationResultUuids(studyUuid, STATE_ESTIMATION)); } - public ResponseEntity downloadDebugFile(UUID resultUuid) { - return stateEstimationRestService.downloadDebugFile(resultUuid); + private void invalidateLogicalControlsStatusOnAllNodes(UUID studyUuid) { + logicalControlsRestService.invalidateLogicalControlsStatus(rootNetworkNodeInfoService.getComputationResultUuids(studyUuid, LOGICAL_CONTROLS)); } - public String computeLogicalControls(@NonNull UUID studyUuid, @NonNull UUID nodeUuid, @NonNull UUID rootNetworkUuid) { - StudyEntity studyEntity = getStudy(studyUuid); - UUID networkUuid = rootNetworkService.getNetworkUuid(rootNetworkUuid); - String variantId = networkModificationTreeService.getVariantId(nodeUuid, rootNetworkUuid); - UUID paramsUuid = studyEntity.getStateEstimationParametersUuid(); - return stateEstimationRestService.runLogicalComputation(networkUuid, variantId, paramsUuid); + public ResponseEntity downloadDebugFile(UUID resultUuid) { + return stateEstimationRestService.downloadDebugFile(resultUuid); } } diff --git a/src/main/resources/config/application.yaml b/src/main/resources/config/application.yaml index 86ec8b3ff0..cb7717e7a4 100644 --- a/src/main/resources/config/application.yaml +++ b/src/main/resources/config/application.yaml @@ -16,6 +16,7 @@ spring: consumeStateEstimationResult;consumeStateEstimationRunning;consumeStateEstimationDebug;consumeStateEstimationStopped;consumeStateEstimationFailed;\ consumePccMinResult;consumePccMinRunning;consumePccMinStopped;consumePccMinFailed;\ consumeAsymmetricalLoadResult;consumeAsymmetricalLoadRunning;consumeAsymmetricalLoadStopped;consumeAsymmetricalLoadFailed;\ + consumeLogicalControlsResult;consumeLogicalControlsRunning;consumeLogicalControlsStopped;consumeLogicalControlsFailed;\ consumeNetworkExportFinished;\ consumeSharedElementUpdate" stream: @@ -212,6 +213,18 @@ spring: consumeAsymmetricalLoadFailed-in-0: destination: ${powsybl-ws.rabbitmq.destination.prefix:}asymmetricalload.run.dlx group: dlq + consumeLogicalControlsResult-in-0: + destination: ${powsybl-ws.rabbitmq.destination.prefix:}logicalcontrols.result + group: studyLogicalControlsResultGroup + consumeLogicalControlsRunning-in-0: + destination: ${powsybl-ws.rabbitmq.destination.prefix:}logicalcontrols.running + group: studyLogicalControlsResultGroup + consumeLogicalControlsStopped-in-0: + destination: ${powsybl-ws.rabbitmq.destination.prefix:}logicalcontrols.stopped + group: studyLogicalControlsStoppedGroup + consumeLogicalControlsFailed-in-0: + destination: ${powsybl-ws.rabbitmq.destination.prefix:}logicalcontrols.run.dlx + group: dlq consumeCaseImportSucceeded-in-0: destination: ${powsybl-ws.rabbitmq.destination.prefix:}case.import.succeeded group: studyCaseImportSucceededGroup diff --git a/src/main/resources/db/changelog/changesets/changelog_20261013T120000Z.xml b/src/main/resources/db/changelog/changesets/changelog_20261013T120000Z.xml new file mode 100644 index 0000000000..f786ffdd33 --- /dev/null +++ b/src/main/resources/db/changelog/changesets/changelog_20261013T120000Z.xml @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/src/main/resources/db/changelog/db.changelog-master.yaml b/src/main/resources/db/changelog/db.changelog-master.yaml index 1250d1b11a..e0ca1b1119 100644 --- a/src/main/resources/db/changelog/db.changelog-master.yaml +++ b/src/main/resources/db/changelog/db.changelog-master.yaml @@ -404,3 +404,6 @@ databaseChangeLog: - include: file: changesets/changelog_20260915T091000Z.xml relativeToChangelogFile: true + - include: + file: changesets/changelog_20261013T120000Z.xml + relativeToChangelogFile: true diff --git a/src/test/java/org/gridsuite/study/server/AsymmetricalLoadTest.java b/src/test/java/org/gridsuite/study/server/AsymmetricalLoadTest.java index cafdfa3eaa..705f190585 100644 --- a/src/test/java/org/gridsuite/study/server/AsymmetricalLoadTest.java +++ b/src/test/java/org/gridsuite/study/server/AsymmetricalLoadTest.java @@ -95,7 +95,7 @@ class AsymmetricalLoadTest { "\"SENSITIVITY_ANALYSIS\":null,\"SHORT_CIRCUIT\":null,\"SHORT_CIRCUIT_ONE_BUS\":null," + "\"VOLTAGE_INITIALIZATION\":null,\"DYNAMIC_SIMULATION\":null,\"DYNAMIC_SECURITY_ANALYSIS\":null," + "\"DYNAMIC_MARGIN_CALCULATION\":null,\"STATE_ESTIMATION\":null,\"PCC_MIN\":null," + - "\"ASYMMETRICAL_LOAD\":\"{\\\"status\\\":\\\"COMPLETED\\\"}\"}"; + "\"ASYMMETRICAL_LOAD\":\"{\\\"status\\\":\\\"COMPLETED\\\"}\",\"LOGICAL_CONTROLS\":null}"; private static final String ELEMENT_UPDATE_DESTINATION = "element.update"; private static final String CASE_UUID_STRING = "00000000-8cf0-11bd-b23e-10b96e4ef00d"; diff --git a/src/test/java/org/gridsuite/study/server/LogicalControlsTest.java b/src/test/java/org/gridsuite/study/server/LogicalControlsTest.java new file mode 100644 index 0000000000..a1ebe67fdb --- /dev/null +++ b/src/test/java/org/gridsuite/study/server/LogicalControlsTest.java @@ -0,0 +1,445 @@ +/** + * Copyright (c) 2026, RTE (http://www.rte-france.com) + * This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. + */ +package org.gridsuite.study.server; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.github.tomakehurst.wiremock.WireMockServer; +import com.github.tomakehurst.wiremock.client.WireMock; +import com.github.tomakehurst.wiremock.core.WireMockConfiguration; +import lombok.AllArgsConstructor; +import org.gridsuite.study.server.dto.ComputationType; +import org.gridsuite.study.server.dto.NodeReceiver; +import org.gridsuite.study.server.dto.QuotaType; +import org.gridsuite.study.server.dto.RootNetworkNodeInfo; +import org.gridsuite.study.server.networkmodificationtree.dto.InsertMode; +import org.gridsuite.study.server.networkmodificationtree.dto.NetworkModificationNode; +import org.gridsuite.study.server.networkmodificationtree.dto.RootNode; +import org.gridsuite.study.server.notification.NotificationService; +import org.gridsuite.study.server.repository.StudyEntity; +import org.gridsuite.study.server.repository.StudyRepository; +import org.gridsuite.study.server.repository.rootnetwork.RootNetworkNodeInfoRepository; +import org.gridsuite.study.server.service.*; +import org.gridsuite.study.server.service.logicalcontrols.LogicalControlsRestService; +import org.gridsuite.study.server.service.stateestimation.StateEstimationRestService; +import org.gridsuite.study.server.utils.TestUtils; +import org.gridsuite.study.server.utils.elasticsearch.DisableElasticsearch; +import org.gridsuite.study.server.utils.wiremock.ComputationServerStubs; +import org.gridsuite.study.server.utils.wiremock.WireMockUtilsCriteria; +import org.json.JSONObject; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.cloud.stream.binder.test.OutputDestination; +import org.springframework.http.MediaType; +import org.springframework.messaging.Message; +import org.springframework.messaging.MessageHeaders; +import org.springframework.messaging.support.MessageBuilder; +import org.springframework.test.context.bean.override.mockito.MockitoSpyBean; +import org.springframework.test.web.servlet.MockMvc; +import org.springframework.web.client.HttpClientErrorException; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static com.github.tomakehurst.wiremock.client.WireMock.delete; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.post; +import static org.gridsuite.study.server.StudyConstants.HEADER_RECEIVER; +import static org.gridsuite.study.server.notification.NotificationService.*; +import static org.junit.jupiter.api.Assertions.*; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * @author David Braquart + */ +@SpringBootTest +@AutoConfigureMockMvc +@DisableElasticsearch +@ContextConfigurationWithTestChannel +class LogicalControlsTest { + + private static final String LOGICAL_CONTROLS_URL_BASE = "/v1/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/logical-controls/"; + private static final String COMPUTATION_URL_BASE = "/v1/studies/{studyUuid}/root-networks/{rootNetworkUuid}/nodes/{nodeUuid}/computations/"; + private static final String NETWORK_UUID_STRING = "38400000-8cf0-11bd-b23e-10b96e4ef00d"; + private static final String LOGICAL_CONTROLS_RESULT_UUID = "b3f1f5a2-0d6c-4c1e-9b0a-2f7a3e1d4c55"; + private static final String LOGICAL_CONTROLS_ERROR_RESULT_UUID = "7d4c1e2b-5a3f-4b8e-9c0d-1e2f3a4b5c6d"; + private static final String LOGICAL_CONTROLS_STATUS_JSON = "{\"status\":\"COMPLETED\"}"; + private static final String ALL_COMPUTATION_STATUS_JSON = "{\"LOAD_FLOW\":null,\"SECURITY_ANALYSIS\":null," + + "\"SENSITIVITY_ANALYSIS\":null,\"SHORT_CIRCUIT\":null,\"SHORT_CIRCUIT_ONE_BUS\":null," + + "\"VOLTAGE_INITIALIZATION\":null,\"DYNAMIC_SIMULATION\":null,\"DYNAMIC_SECURITY_ANALYSIS\":null," + + "\"DYNAMIC_MARGIN_CALCULATION\":null,\"STATE_ESTIMATION\":null,\"PCC_MIN\":null," + + "\"ASYMMETRICAL_LOAD\":null,\"LOGICAL_CONTROLS\":\"{\\\"status\\\":\\\"COMPLETED\\\"}\"}"; + private static final String LOGICAL_CONTROLS_RESULT_JSON = "[{\"id\":\"control1\"}]"; + + private static final UUID CASE_UUID = UUID.fromString("00000000-8cf0-11bd-b23e-10b96e4ef00d"); + private static final String VARIANT_ID = "variant_1"; + private static final UUID STATE_ESTIMATION_PARAMETERS_UUID = UUID.fromString("0c0f1efd-bd22-4a75-83d3-9e530245c7f5"); + private static final String VARIANT_ID_2 = "variant_2"; + + private static final String STUDY_UPDATE_DESTINATION = "study.update"; + private static final String LOGICAL_CONTROLS_RESULT_DESTINATION = "logicalcontrols.result"; + private static final String LOGICAL_CONTROLS_FAILED_DESTINATION = "logicalcontrols.run.dlx"; + + private static final String LOGICAL_CONTROLS_PREFIX = "logical-controls/"; + + @Autowired + private MockMvc mockMvc; + @Autowired + private OutputDestination output; + @Autowired + private ObjectMapper objectMapper; + @Autowired + private NetworkModificationTreeService networkModificationTreeService; + @MockitoSpyBean + private LogicalControlsRestService logicalControlsRestService; + @Autowired + private StateEstimationRestService stateEstimationRestService; + @Autowired + private StudyRepository studyRepository; + @Autowired + private ReportService reportService; + @Autowired + private SupervisionService supervisionService; + @Autowired + private RootNetworkNodeInfoRepository rootNetworkNodeInfoRepository; + @Autowired + private RootNetworkNodeInfoService rootNetworkNodeInfoService; + @Autowired + private TestUtils studyTestUtils; + @Autowired + private ConsumerService consumerService; + + private WireMockServer wireMockServer; + private ComputationServerStubs computationServerStubs; + + @BeforeEach + void setup() { + wireMockServer = new WireMockServer(WireMockConfiguration.wireMockConfig().dynamicPort()); + wireMockServer.start(); + computationServerStubs = new ComputationServerStubs(wireMockServer); + configureFor("localhost", wireMockServer.port()); + String baseUrl = wireMockServer.baseUrl(); + + logicalControlsRestService.setBaseUri(baseUrl); + reportService.setReportServerBaseUri(baseUrl); + } + + @AfterEach + void tearDown() { + studyRepository.findAll().forEach(s -> networkModificationTreeService.doDeleteTree(s.getId())); + studyRepository.deleteAll(); + wireMockServer.stop(); + TestUtils.assertQueuesEmptyThenClear( + List.of(STUDY_UPDATE_DESTINATION, LOGICAL_CONTROLS_RESULT_DESTINATION, LOGICAL_CONTROLS_FAILED_DESTINATION), + output + ); + } + + @AllArgsConstructor + private static final class StudyNodeIds { + UUID studyId; + UUID rootNetworkUuid; + UUID nodeId; + } + + private StudyNodeIds createStudyAndNode(String variantId, String nodeName) throws Exception { + StudyEntity studyEntity = TestUtils.CreateDummyStudyBuilder.builder() + .setNetworkUuid(UUID.fromString(NETWORK_UUID_STRING)).setNetworkId("netId") + .setCaseUuid(CASE_UUID).setCaseFormat("").setCaseName("") + .setLoadFlowParametersUuid(UUID.randomUUID()) + .setStateEstimationParametersUuid(STATE_ESTIMATION_PARAMETERS_UUID) + .build(); + studyRepository.save(studyEntity); + networkModificationTreeService.createRoot(studyEntity); + + UUID studyUuid = studyEntity.getId(); + UUID rootNetworkUuid = studyTestUtils.getOneRootNetworkUuid(studyUuid); + UUID rootNodeUuid = getRootNode(studyUuid).getId(); + + NetworkModificationNode node = createNetworkModificationNode(studyUuid, rootNodeUuid, UUID.randomUUID(), variantId, nodeName); + return new StudyNodeIds(studyUuid, rootNetworkUuid, node.getId()); + } + + private RootNode getRootNode(UUID study) throws Exception { + return objectMapper.readValue( + mockMvc.perform(get("/v1/studies/{uuid}/tree", study)) + .andExpect(status().isOk()) + .andReturn().getResponse().getContentAsString(), + new TypeReference<>() { + }); + } + + private NetworkModificationNode createNetworkModificationNode(UUID studyUuid, UUID parentNodeUuid, + UUID modificationGroupUuid, String variantId, String nodeName) throws Exception { + NetworkModificationNode modificationNode = NetworkModificationNode.builder() + .name(nodeName) + .description("description") + .modificationGroupUuid(modificationGroupUuid) + .variantId(variantId) + .children(Collections.emptyList()) + .build(); + + String mnBodyJson = objectMapper.writeValueAsString(modificationNode); + JSONObject jsonObject = new JSONObject(mnBodyJson); + jsonObject.put("variantId", variantId); + jsonObject.put("modificationGroupUuid", modificationGroupUuid); + mnBodyJson = jsonObject.toString(); + + mockMvc.perform(post("/v1/studies/{studyUuid}/tree/nodes/{id}", studyUuid, parentNodeUuid) + .content(mnBodyJson) + .contentType(MediaType.APPLICATION_JSON) + .header("userId", "userId")) + .andExpect(status().isOk()); + + Message msg = TestUtils.receiveStudyUpdate(output, STUDY_UPDATE_DESTINATION); + assertNotNull(msg); + modificationNode.setId(UUID.fromString(String.valueOf(msg.getHeaders().get(NotificationService.HEADER_NEW_NODE)))); + assertEquals(InsertMode.CHILD.name(), msg.getHeaders().get(NotificationService.HEADER_INSERT_MODE)); + + rootNetworkNodeInfoService.updateRootNetworkNode(modificationNode.getId(), + studyTestUtils.getOneRootNetworkUuid(studyUuid), + RootNetworkNodeInfo.builder().variantId(variantId).build()); + + return modificationNode; + } + + private void checkLogicalControlsMessagesReceived(UUID studyUuid, String updateTypeToCheck) { + Message message = TestUtils.receiveStudyUpdate(output, STUDY_UPDATE_DESTINATION); + assertEquals(studyUuid, message.getHeaders().get(NotificationService.HEADER_STUDY_UUID)); + assertEquals(updateTypeToCheck, message.getHeaders().get(HEADER_UPDATE_TYPE)); + } + + private void consumeLogicalControlsResult(StudyNodeIds ids, String resultUuid) throws JsonProcessingException { + String receiverJson = objectMapper.writeValueAsString(new NodeReceiver(ids.nodeId, ids.rootNetworkUuid)); + MessageHeaders headers = new MessageHeaders(Map.of("resultUuid", resultUuid, HEADER_RECEIVER, receiverJson)); + consumerService.consumeLogicalControlsResult().accept(MessageBuilder.createMessage("", headers)); + + checkLogicalControlsMessagesReceived(ids.studyId, UPDATE_TYPE_LOGICAL_CONTROLS_STATUS); + checkLogicalControlsMessagesReceived(ids.studyId, UPDATE_TYPE_LOGICAL_CONTROLS_STATUS); + checkLogicalControlsMessagesReceived(ids.studyId, UPDATE_TYPE_LOGICAL_CONTROLS_RESULT); + } + + private void runLogicalControls(StudyNodeIds ids) throws Exception { + computationServerStubs.stubComputationRun(NETWORK_UUID_STRING, null, LOGICAL_CONTROLS_RESULT_UUID, LOGICAL_CONTROLS_PREFIX); + + mockMvc.perform(post(LOGICAL_CONTROLS_URL_BASE + "run", ids.studyId, ids.rootNetworkUuid, ids.nodeId) + .header("userId", "userId")) + .andExpect(status().isOk()); + + consumeLogicalControlsResult(ids, LOGICAL_CONTROLS_RESULT_UUID); + WireMockUtilsCriteria.verifyPostRequest(wireMockServer, "/v1/logical-controls/networks/" + NETWORK_UUID_STRING + "/run-and-save", + true, Map.of("variantId", WireMock.equalTo(VARIANT_ID), "parametersUuid", WireMock.equalTo(STATE_ESTIMATION_PARAMETERS_UUID.toString())), null, 1); + } + + @Test + void testNoQuota() { + assertNull(QuotaType.mapFromComputationType(ComputationType.LOGICAL_CONTROLS)); + } + + @Test + void consumeLogicalControlsRunning() throws Exception { + StudyNodeIds ids = createStudyAndNode(VARIANT_ID, "node1"); + Message runningMessage = MessageBuilder.withPayload("") + .setHeader(HEADER_RECEIVER, objectMapper.writeValueAsString(new NodeReceiver(ids.nodeId, ids.rootNetworkUuid))) + .build(); + consumerService.consumeLogicalControlsRunning().accept(runningMessage); + checkLogicalControlsMessagesReceived(ids.studyId, UPDATE_TYPE_LOGICAL_CONTROLS_STATUS); + } + + @Test + void testRunAndCheckStatus() throws Exception { + StudyNodeIds ids = createStudyAndNode(VARIANT_ID, "node1"); + runLogicalControls(ids); + + assertEquals(UUID.fromString(LOGICAL_CONTROLS_RESULT_UUID), + rootNetworkNodeInfoService.getComputationResultUuid(ids.nodeId, ids.rootNetworkUuid, ComputationType.LOGICAL_CONTROLS)); + + computationServerStubs.stubGetResultStatus(LOGICAL_CONTROLS_RESULT_UUID, LOGICAL_CONTROLS_STATUS_JSON, LOGICAL_CONTROLS_PREFIX); + mockMvc.perform(get(LOGICAL_CONTROLS_URL_BASE + "status", ids.studyId, ids.rootNetworkUuid, ids.nodeId)) + .andExpectAll(status().isOk(), content().string(LOGICAL_CONTROLS_STATUS_JSON)); + computationServerStubs.verifyGetResultStatus(LOGICAL_CONTROLS_RESULT_UUID, 1, LOGICAL_CONTROLS_PREFIX); + + mockMvc.perform(get(COMPUTATION_URL_BASE + "status", ids.studyId, ids.rootNetworkUuid, ids.nodeId)) + .andExpectAll(status().isOk(), content().string(ALL_COMPUTATION_STATUS_JSON)); + } + + @Test + void testStatusWithoutRun() throws Exception { + StudyNodeIds ids = createStudyAndNode(VARIANT_ID, "node1"); + mockMvc.perform(get(LOGICAL_CONTROLS_URL_BASE + "status", ids.studyId, ids.rootNetworkUuid, ids.nodeId)) + .andExpect(status().isNoContent()); + mockMvc.perform(get(LOGICAL_CONTROLS_URL_BASE + "result", ids.studyId, ids.rootNetworkUuid, ids.nodeId)) + .andExpect(status().isNoContent()); + } + + @Test + void testParameterUpdateInvalidatesOnlyStudyResults() throws Exception { + StudyNodeIds ids = createStudyAndNode(VARIANT_ID, "node 1"); + runLogicalControls(ids); + StudyNodeIds otherStudy = createStudyAndNode(VARIANT_ID, "other study node"); + UUID otherResultUuid = UUID.randomUUID(); + rootNetworkNodeInfoService.updateComputationResultUuid(otherStudy.nodeId, otherStudy.rootNetworkUuid, otherResultUuid, ComputationType.LOGICAL_CONTROLS); + UUID stateEstimationResultUuid = UUID.randomUUID(); + rootNetworkNodeInfoService.updateComputationResultUuid(ids.nodeId, ids.rootNetworkUuid, stateEstimationResultUuid, ComputationType.STATE_ESTIMATION); + wireMockServer.stubFor(WireMock.put(urlPathEqualTo("/v1/results/invalidate-status")) + .withQueryParam("resultUuid", equalTo(stateEstimationResultUuid.toString())).willReturn(ok())); + wireMockServer.stubFor(WireMock.put(urlPathEqualTo("/v1/logical-controls/results/invalidate-status")) + .withQueryParam("resultUuid", equalTo(LOGICAL_CONTROLS_RESULT_UUID)).willReturn(ok())); + + updateStateEstimationParameters(ids); + + wireMockServer.verify(1, putRequestedFor(urlPathEqualTo("/v1/results/invalidate-status")) + .withQueryParam("resultUuid", equalTo(stateEstimationResultUuid.toString()))); + wireMockServer.verify(1, putRequestedFor(urlPathEqualTo("/v1/logical-controls/results/invalidate-status")) + .withQueryParam("resultUuid", equalTo(LOGICAL_CONTROLS_RESULT_UUID))); + wireMockServer.verify(0, putRequestedFor(urlPathEqualTo("/v1/logical-controls/results/invalidate-status")) + .withQueryParam("resultUuid", containing(otherResultUuid.toString()))); + } + + @Test + void testParameterUpdateWithoutLogicalControlsResults() throws Exception { + StudyNodeIds ids = createStudyAndNode(VARIANT_ID, "node 1"); + + updateStateEstimationParameters(ids); + + wireMockServer.verify(0, putRequestedFor(urlPathEqualTo("/v1/logical-controls/results/invalidate-status"))); + } + + private void updateStateEstimationParameters(StudyNodeIds ids) throws Exception { + stateEstimationRestService.setBaseUri(wireMockServer.baseUrl()); + wireMockServer.stubFor(WireMock.put(urlPathEqualTo("/v1/parameters/" + STATE_ESTIMATION_PARAMETERS_UUID)) + .willReturn(ok())); + + mockMvc.perform(post("/v1/studies/{studyUuid}/state-estimation/parameters", ids.studyId) + .header("userId", "userId") + .contentType(MediaType.APPLICATION_JSON) + .content(TestUtils.resourceToString("/estim-parameters.json"))) + .andExpect(status().isOk()); + + checkLogicalControlsMessagesReceived(ids.studyId, UPDATE_TYPE_STATE_ESTIMATION_STATUS); + checkLogicalControlsMessagesReceived(ids.studyId, UPDATE_TYPE_LOGICAL_CONTROLS_STATUS); + checkLogicalControlsMessagesReceived(ids.studyId, UPDATE_TYPE_COMPUTATION_PARAMETERS); + } + + @Test + void testStop() throws Exception { + StudyNodeIds ids = createStudyAndNode(VARIANT_ID, "node1"); + runLogicalControls(ids); + + wireMockServer.stubFor(WireMock.put(urlPathEqualTo("/v1/logical-controls/results/" + LOGICAL_CONTROLS_RESULT_UUID + "/stop")) + .willReturn(ok())); + + mockMvc.perform(put(LOGICAL_CONTROLS_URL_BASE + "stop", ids.studyId, ids.rootNetworkUuid, ids.nodeId)) + .andExpect(status().isOk()); + + String receiverJson = objectMapper.writeValueAsString(new NodeReceiver(ids.nodeId, ids.rootNetworkUuid)); + computationServerStubs.verifyComputationStop(LOGICAL_CONTROLS_RESULT_UUID, + Map.of("receiver", WireMock.equalTo(URLEncoder.encode(receiverJson, StandardCharsets.UTF_8))), LOGICAL_CONTROLS_PREFIX); + + consumerService.consumeLogicalControlsStopped().accept(MessageBuilder.withPayload("") + .setHeader(HEADER_RECEIVER, receiverJson) + .setHeader("resultUuid", LOGICAL_CONTROLS_RESULT_UUID) + .build()); + + checkLogicalControlsMessagesReceived(ids.studyId, UPDATE_TYPE_LOGICAL_CONTROLS_STATUS); + assertNull(rootNetworkNodeInfoService.getComputationResultUuid(ids.nodeId, ids.rootNetworkUuid, ComputationType.LOGICAL_CONTROLS)); + mockMvc.perform(get(LOGICAL_CONTROLS_URL_BASE + "status", ids.studyId, ids.rootNetworkUuid, ids.nodeId)) + .andExpect(status().isNoContent()); + } + + @Test + void testStopWithoutRun() throws Exception { + StudyNodeIds ids = createStudyAndNode(VARIANT_ID, "node1"); + mockMvc.perform(put(LOGICAL_CONTROLS_URL_BASE + "stop", ids.studyId, ids.rootNetworkUuid, ids.nodeId)) + .andExpect(status().isOk()); + wireMockServer.verify(0, putRequestedFor(urlPathMatching("/v1/logical-controls/results/.*/stop"))); + } + + @Test + void testFailure() throws Exception { + StudyNodeIds ids = createStudyAndNode(VARIANT_ID_2, "node 2"); + computationServerStubs.stubComputationRun(NETWORK_UUID_STRING, VARIANT_ID_2, LOGICAL_CONTROLS_ERROR_RESULT_UUID, LOGICAL_CONTROLS_PREFIX); + + mockMvc.perform(post(LOGICAL_CONTROLS_URL_BASE + "run", ids.studyId, ids.rootNetworkUuid, ids.nodeId) + .header("userId", "userId")) + .andExpect(status().isOk()); + checkLogicalControlsMessagesReceived(ids.studyId, UPDATE_TYPE_LOGICAL_CONTROLS_STATUS); + + String receiverJson = objectMapper.writeValueAsString(new NodeReceiver(ids.nodeId, ids.rootNetworkUuid)); + Message failedMessage = MessageBuilder.withPayload("") + .setHeader(HEADER_RECEIVER, receiverJson) + .setHeader("resultUuid", LOGICAL_CONTROLS_ERROR_RESULT_UUID) + .setHeader("userId", "userId") + .build(); + consumerService.consumeLogicalControlsFailed().accept(failedMessage); + + checkLogicalControlsMessagesReceived(ids.studyId, UPDATE_TYPE_LOGICAL_CONTROLS_FAILED); + WireMockUtilsCriteria.verifyPostRequest(wireMockServer, "/v1/logical-controls/networks/" + NETWORK_UUID_STRING + "/run-and-save", + true, Map.of("variantId", WireMock.equalTo(VARIANT_ID_2), "parametersUuid", WireMock.equalTo(STATE_ESTIMATION_PARAMETERS_UUID.toString())), null, 1); + } + + @Test + void testResultsDeletion() throws Exception { + StudyNodeIds ids = createStudyAndNode(VARIANT_ID, "node 1"); + runLogicalControls(ids); + + assertEquals(1, rootNetworkNodeInfoRepository.findAllByLogicalControlsResultUuidNotNull().size()); + + wireMockServer.stubFor(get(urlPathEqualTo("/v1/supervision/logical-controls/results-count")) + .willReturn(okJson("1"))); + wireMockServer.stubFor(delete(urlPathEqualTo("/v1/logical-controls/results")) + .willReturn(ok())); + wireMockServer.stubFor(delete(urlPathMatching("/v1/reports.*")) + .willReturn(ok())); + + Integer dryRunCount = supervisionService.deleteComputationResults(ComputationType.LOGICAL_CONTROLS, true); + assertEquals(1, dryRunCount); + wireMockServer.verify(1, getRequestedFor(urlPathEqualTo("/v1/supervision/logical-controls/results-count"))); + + Integer deletedCount = supervisionService.deleteComputationResults(ComputationType.LOGICAL_CONTROLS, false); + assertEquals(1, deletedCount); + wireMockServer.verify(1, deleteRequestedFor(urlPathEqualTo("/v1/logical-controls/results"))); + + assertEquals(0, rootNetworkNodeInfoRepository.findAllByLogicalControlsResultUuidNotNull().size()); + } + + @Test + void testGetLogicalControlsResults() throws Exception { + StudyNodeIds ids = createStudyAndNode(VARIANT_ID, "node 1"); + runLogicalControls(ids); + + UUID stubId = wireMockServer.stubFor(WireMock.get(urlPathEqualTo("/v1/logical-controls/results/" + LOGICAL_CONTROLS_RESULT_UUID)) + .willReturn(okJson(LOGICAL_CONTROLS_RESULT_JSON))).getId(); + + mockMvc.perform(get(LOGICAL_CONTROLS_URL_BASE + "result", ids.studyId, ids.rootNetworkUuid, ids.nodeId)) + .andExpect(status().isOk()) + .andExpect(content().string(LOGICAL_CONTROLS_RESULT_JSON)); + assertEquals(1, wireMockServer.getAllServeEvents().stream().filter(e -> e.getStubMapping().getId().equals(stubId)).count()); + + // results NOT FOUND + UUID resultUuid = UUID.randomUUID(); + wireMockServer.stubFor(WireMock.get(urlPathEqualTo("/v1/logical-controls/results/" + resultUuid)) + .willReturn(WireMock.notFound())); + assertThrows(HttpClientErrorException.NotFound.class, () -> + logicalControlsRestService.getLogicalControlsResult(resultUuid) + ); + } +} diff --git a/src/test/java/org/gridsuite/study/server/PccMinTest.java b/src/test/java/org/gridsuite/study/server/PccMinTest.java index 74b84c99b2..254d5cbf01 100644 --- a/src/test/java/org/gridsuite/study/server/PccMinTest.java +++ b/src/test/java/org/gridsuite/study/server/PccMinTest.java @@ -78,7 +78,7 @@ class PccMinTest { "\"SENSITIVITY_ANALYSIS\":null,\"SHORT_CIRCUIT\":null,\"SHORT_CIRCUIT_ONE_BUS\":null," + "\"VOLTAGE_INITIALIZATION\":null,\"DYNAMIC_SIMULATION\":null,\"DYNAMIC_SECURITY_ANALYSIS\":null," + "\"DYNAMIC_MARGIN_CALCULATION\":null,\"STATE_ESTIMATION\":null," + - "\"PCC_MIN\":\"{\\\"status\\\":\\\"COMPLETED\\\"}\",\"ASYMMETRICAL_LOAD\":null}"; + "\"PCC_MIN\":\"{\\\"status\\\":\\\"COMPLETED\\\"}\",\"ASYMMETRICAL_LOAD\":null,\"LOGICAL_CONTROLS\":null}"; private static final String ELEMENT_UPDATE_DESTINATION = "element.update"; private static final String CASE_UUID_STRING = "00000000-8cf0-11bd-b23e-10b96e4ef00d"; diff --git a/src/test/java/org/gridsuite/study/server/ShortCircuitTest.java b/src/test/java/org/gridsuite/study/server/ShortCircuitTest.java index 08d456939a..a0899950a7 100644 --- a/src/test/java/org/gridsuite/study/server/ShortCircuitTest.java +++ b/src/test/java/org/gridsuite/study/server/ShortCircuitTest.java @@ -109,7 +109,7 @@ class ShortCircuitTest implements WithAssertions { "\"SENSITIVITY_ANALYSIS\":null,\"SHORT_CIRCUIT\":\"{\\\"status\\\":\\\"COMPLETED\\\"}\"," + "\"SHORT_CIRCUIT_ONE_BUS\":null,\"VOLTAGE_INITIALIZATION\":null,\"DYNAMIC_SIMULATION\":null," + "\"DYNAMIC_SECURITY_ANALYSIS\":null,\"DYNAMIC_MARGIN_CALCULATION\":null,\"STATE_ESTIMATION\":null," + - "\"PCC_MIN\":null,\"ASYMMETRICAL_LOAD\":null}"; + "\"PCC_MIN\":null,\"ASYMMETRICAL_LOAD\":null,\"LOGICAL_CONTROLS\":null}"; private static final String SHORT_CIRCUIT_ANALYSIS_PARAMETERS_UUID_STRING = "0c0f1efd-bd22-4a75-83d3-9e530245c7f4"; private static final UUID SHORT_CIRCUIT_ANALYSIS_PARAMETERS_UUID = UUID.fromString(SHORT_CIRCUIT_ANALYSIS_PARAMETERS_UUID_STRING); diff --git a/src/test/java/org/gridsuite/study/server/StateEstimationTest.java b/src/test/java/org/gridsuite/study/server/StateEstimationTest.java index d0826761b3..c33fbf141b 100644 --- a/src/test/java/org/gridsuite/study/server/StateEstimationTest.java +++ b/src/test/java/org/gridsuite/study/server/StateEstimationTest.java @@ -103,8 +103,6 @@ class StateEstimationTest { private static final String ESTIM_STATUS_JSON = "{\"status\":\"COMPLETED\"}"; - private static final String LOGICAL_CONTROLS_RESULT_JSON = "{\"balances\":{}}"; - private static final String VARIANT_ID = "variant_1"; private static final String VARIANT_ID_2 = "variant_2"; @@ -227,8 +225,6 @@ public MockResponse dispatch(RecordedRequest request) { return new MockResponse(200, Headers.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE), objectMapper.writeValueAsString(STATE_ESTIMATION_PARAMETERS_UUID)); } else if (path.matches("/v1/users/.*/quota/.*") && "POST".equals(method)) { return new MockResponse(200); - } else if (path.matches("/v1/networks/" + NETWORK_UUID_STRING + "/logical-controls\\?variantId=.*") && "POST".equals(method)) { - return new MockResponse(200, Headers.of(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE), LOGICAL_CONTROLS_RESULT_JSON); } else { return new MockResponse.Builder().code(418).body("Unhandled method+path: " + request.getMethod() + " " + request.getPath()).build(); } @@ -464,6 +460,8 @@ private void createOrUpdateParametersAndDoChecks(UUID studyNameUserIdUuid, Strin assertEquals(studyNameUserIdUuid, stateEstimationStatusMessage.getHeaders().get(NotificationService.HEADER_STUDY_UUID)); assertEquals(NotificationService.UPDATE_TYPE_STATE_ESTIMATION_STATUS, stateEstimationStatusMessage.getHeaders().get(NotificationService.HEADER_UPDATE_TYPE)); + checkUpdateModelStatusMessagesReceived(studyNameUserIdUuid, NotificationService.UPDATE_TYPE_LOGICAL_CONTROLS_STATUS); + Message message = TestUtils.receiveStudyUpdate(output, STUDY_UPDATE_DESTINATION); assertEquals(UPDATE_TYPE_COMPUTATION_PARAMETERS, message.getHeaders().get(NotificationService.HEADER_UPDATE_TYPE)); } @@ -493,18 +491,6 @@ void testDebugComputation(final MockWebServer server) throws Exception { assertTrue(TestUtils.getRequestsDone(1, server).stream().anyMatch(r -> r.matches("/v1/results/" + STATE_ESTIMATION_RESULT_UUID + "/status"))); } - @Test - void testComputeLogicalControls(final MockWebServer server) throws Exception { - StudyNodeIds ids = createStudyAndNode(VARIANT_ID, "node 1", null); - - MvcResult mvcResult = mockMvc.perform(post(STATE_ESTIMATION_URL_BASE + "logical-controls", ids.studyId, ids.rootNetworkUuid, ids.nodeId)) - .andExpect(status().isOk()) - .andReturn(); - assertEquals(LOGICAL_CONTROLS_RESULT_JSON, mvcResult.getResponse().getContentAsString()); - assertTrue(TestUtils.getRequestsDone(1, server).stream().anyMatch(r -> r.matches( - "/v1/networks/" + NETWORK_UUID_STRING + "/logical-controls\\?variantId=" + VARIANT_ID))); - } - @Test void consumeStateEstimationRunning() throws Exception { StudyNodeIds ids = createStudyAndNode(VARIANT_ID, "node1", null); diff --git a/src/test/java/org/gridsuite/study/server/service/stateestimation/StateEstimationServiceTest.java b/src/test/java/org/gridsuite/study/server/service/stateestimation/StateEstimationServiceTest.java index 8f0fff01c2..55ec9f5e8a 100644 --- a/src/test/java/org/gridsuite/study/server/service/stateestimation/StateEstimationServiceTest.java +++ b/src/test/java/org/gridsuite/study/server/service/stateestimation/StateEstimationServiceTest.java @@ -11,6 +11,7 @@ import org.gridsuite.study.server.repository.StudyRepository; import org.gridsuite.study.server.service.*; import org.gridsuite.study.server.service.common.ComputationParametersService; +import org.gridsuite.study.server.service.logicalcontrols.LogicalControlsRestService; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -45,6 +46,8 @@ class StateEstimationServiceTest { @Mock private StateEstimationRestService stateEstimationRestService; @Mock + private LogicalControlsRestService logicalControlsRestService; + @Mock private UserAdminService userAdminService; private StateEstimationService stateEstimationService; @@ -52,7 +55,7 @@ class StateEstimationServiceTest { @BeforeEach void setUp() { stateEstimationService = new StateEstimationService(studyRepository, computationParametersService, notificationService, rootNetworkNodeInfoService, - networkModificationTreeService, rootNetworkService, stateEstimationRestService, userAdminService, new ObjectMapper()); + networkModificationTreeService, rootNetworkService, stateEstimationRestService, logicalControlsRestService, userAdminService, new ObjectMapper()); } @Test