diff --git a/src/EnergyPlus/OutAirNodeManager.cc b/src/EnergyPlus/OutAirNodeManager.cc index 5840a91b879..c39f1cab51b 100644 --- a/src/EnergyPlus/OutAirNodeManager.cc +++ b/src/EnergyPlus/OutAirNodeManager.cc @@ -592,7 +592,8 @@ namespace OutAirNodeManager { } state.dataLoopNodes->Node(NodeNum).Temp = state.dataLoopNodes->Node(NodeNum).OutAirDryBulb; - if (state.dataLoopNodes->Node(NodeNum).IsLocalNode) { + if (state.dataLoopNodes->Node(NodeNum).IsLocalNode || state.dataLoopNodes->Node(NodeNum).EMSOverrideOutAirDryBulb || + state.dataLoopNodes->Node(NodeNum).EMSOverrideOutAirWetBulb) { if (InitCall) { if (state.dataLoopNodes->Node(NodeNum).OutAirWetBulb > state.dataLoopNodes->Node(NodeNum).OutAirDryBulb) { state.dataLoopNodes->Node(NodeNum).OutAirWetBulb = state.dataLoopNodes->Node(NodeNum).OutAirDryBulb; diff --git a/src/EnergyPlus/api/datatransfer.cc b/src/EnergyPlus/api/datatransfer.cc index 5c663e414c9..999dc1032a4 100644 --- a/src/EnergyPlus/api/datatransfer.cc +++ b/src/EnergyPlus/api/datatransfer.cc @@ -55,6 +55,7 @@ #include #include #include +#include #include #include #include @@ -456,6 +457,16 @@ int getActuatorHandle(EnergyPlusState state, const char *componentType, const ch } ++availActuator.handleCount; + // Mirror EMSManager::SetupNodeSetPointsAsActuators for Python plugin path (issue #8904) + if (typeUC == "OUTDOOR AIR SYSTEM NODE") { + for (int NodeNum = 1; NodeNum <= thisState->dataLoopNodes->NumOfNodes; ++NodeNum) { + if (EnergyPlus::Util::makeUPPER(thisState->dataLoopNodes->NodeID(NodeNum)) == keyUC) { + thisState->dataLoopNodes->Node(NodeNum).IsLocalNode = true; + break; + } + } + } + return handle; } } diff --git a/tst/EnergyPlus/unit/OutAirNodeManager.unit.cc b/tst/EnergyPlus/unit/OutAirNodeManager.unit.cc index bd41e4bcc0b..ab7036f82fa 100644 --- a/tst/EnergyPlus/unit/OutAirNodeManager.unit.cc +++ b/tst/EnergyPlus/unit/OutAirNodeManager.unit.cc @@ -104,3 +104,53 @@ TEST_F(EnergyPlusFixture, OutAirNodeManager_OATdbTwbOverrideTest) EXPECT_NEAR(0.007253013, state->dataLoopNodes->Node(2).HumRat, 0.000001); EXPECT_NEAR(0.006543816, state->dataLoopNodes->Node(3).HumRat, 0.000001); } + +// Reproduces GitHub issue #8904: EMS wetbulb override on OA node without +// IsLocalNode=true does not recalculate HumRat. +TEST_F(EnergyPlusFixture, OutAirNodeManager_EMSWetbulbOverride_NoIsLocalNode) +{ + state->dataOutAirNodeMgr->NumOutsideAirNodes = 2; + state->dataOutAirNodeMgr->OutsideAirNodeList.allocate(2); + state->dataLoopNodes->Node.allocate(2); + + state->dataEnvrn->OutDryBulbTemp = 25.0; + state->dataEnvrn->OutWetBulbTemp = 15.0; + state->dataEnvrn->WindSpeed = 2.0; + state->dataEnvrn->WindDir = 0.0; + state->dataEnvrn->OutBaroPress = 101325; + state->dataEnvrn->OutHumRat = + Psychrometrics::PsyWFnTdbTwbPb(*state, state->dataEnvrn->OutDryBulbTemp, state->dataEnvrn->OutWetBulbTemp, state->dataEnvrn->OutBaroPress); + + state->dataOutAirNodeMgr->OutsideAirNodeList(1) = 1; + state->dataOutAirNodeMgr->OutsideAirNodeList(2) = 2; + + // Node 1: EMS overrides drybulb + wetbulb, but IsLocalNode is FALSE + // (simulates Python plugin API path — getActuatorHandle never sets IsLocalNode) + state->dataLoopNodes->Node(1).IsLocalNode = false; + state->dataLoopNodes->Node(1).EMSOverrideOutAirDryBulb = true; + state->dataLoopNodes->Node(1).EMSOverrideOutAirWetBulb = true; + state->dataLoopNodes->Node(1).EMSValueForOutAirDryBulb = 26.7; + state->dataLoopNodes->Node(1).EMSValueForOutAirWetBulb = 19.4; + state->dataLoopNodes->Node(1).OutAirDryBulb = state->dataEnvrn->OutDryBulbTemp; + state->dataLoopNodes->Node(1).OutAirWetBulb = state->dataEnvrn->OutWetBulbTemp; + + // Node 2: EMS overrides only wetbulb, IsLocalNode is FALSE + state->dataLoopNodes->Node(2).IsLocalNode = false; + state->dataLoopNodes->Node(2).EMSOverrideOutAirWetBulb = true; + state->dataLoopNodes->Node(2).EMSValueForOutAirWetBulb = 19.4; + state->dataLoopNodes->Node(2).OutAirDryBulb = state->dataEnvrn->OutDryBulbTemp; + state->dataLoopNodes->Node(2).OutAirWetBulb = state->dataEnvrn->OutWetBulbTemp; + + InitOutAirNodes(*state); + + // Expected: HumRat recalculated from overridden Tdb=26.7 + Twb=19.4 + Real64 expectedHumRat1 = Psychrometrics::PsyWFnTdbTwbPb(*state, 26.7, 19.4, state->dataEnvrn->OutBaroPress); + EXPECT_NEAR(expectedHumRat1, state->dataLoopNodes->Node(1).HumRat, 0.000001); + EXPECT_NEAR(26.7, state->dataLoopNodes->Node(1).Temp, 0.001); + EXPECT_NEAR(19.4, state->dataLoopNodes->Node(1).OutAirWetBulb, 0.001); + + // Expected: HumRat recalculated from environment Tdb=25.0 + overridden Twb=19.4 + Real64 expectedHumRat2 = Psychrometrics::PsyWFnTdbTwbPb(*state, 25.0, 19.4, state->dataEnvrn->OutBaroPress); + EXPECT_NEAR(expectedHumRat2, state->dataLoopNodes->Node(2).HumRat, 0.000001); + EXPECT_NEAR(19.4, state->dataLoopNodes->Node(2).OutAirWetBulb, 0.001); +}