LCOV - code coverage report
Current view: top level - EnergyPlus - PluginManager.cc (source / functions) Coverage Total Hit
Test: lcov.output.filtered Lines: 41.6 % 760 316
Test Date: 2025-05-22 16:09:37 Functions: 75.0 % 40 30

            Line data    Source code
       1              : // EnergyPlus, Copyright (c) 1996-2025, The Board of Trustees of the University of Illinois,
       2              : // The Regents of the University of California, through Lawrence Berkeley National Laboratory
       3              : // (subject to receipt of any required approvals from the U.S. Dept. of Energy), Oak Ridge
       4              : // National Laboratory, managed by UT-Battelle, Alliance for Sustainable Energy, LLC, and other
       5              : // contributors. All rights reserved.
       6              : //
       7              : // NOTICE: This Software was developed under funding from the U.S. Department of Energy and the
       8              : // U.S. Government consequently retains certain rights. As such, the U.S. Government has been
       9              : // granted for itself and others acting on its behalf a paid-up, nonexclusive, irrevocable,
      10              : // worldwide license in the Software to reproduce, distribute copies to the public, prepare
      11              : // derivative works, and perform publicly and display publicly, and to permit others to do so.
      12              : //
      13              : // Redistribution and use in source and binary forms, with or without modification, are permitted
      14              : // provided that the following conditions are met:
      15              : //
      16              : // (1) Redistributions of source code must retain the above copyright notice, this list of
      17              : //     conditions and the following disclaimer.
      18              : //
      19              : // (2) Redistributions in binary form must reproduce the above copyright notice, this list of
      20              : //     conditions and the following disclaimer in the documentation and/or other materials
      21              : //     provided with the distribution.
      22              : //
      23              : // (3) Neither the name of the University of California, Lawrence Berkeley National Laboratory,
      24              : //     the University of Illinois, U.S. Dept. of Energy nor the names of its contributors may be
      25              : //     used to endorse or promote products derived from this software without specific prior
      26              : //     written permission.
      27              : //
      28              : // (4) Use of EnergyPlus(TM) Name. If Licensee (i) distributes the software in stand-alone form
      29              : //     without changes from the version obtained under this License, or (ii) Licensee makes a
      30              : //     reference solely to the software portion of its product, Licensee must refer to the
      31              : //     software as "EnergyPlus version X" software, where "X" is the version number Licensee
      32              : //     obtained under this License and may not use a different name for the software. Except as
      33              : //     specifically required in this Section (4), Licensee shall not use in a company name, a
      34              : //     product name, in advertising, publicity, or other promotional activities any name, trade
      35              : //     name, trademark, logo, or other designation of "EnergyPlus", "E+", "e+" or confusingly
      36              : //     similar designation, without the U.S. Department of Energy's prior written consent.
      37              : //
      38              : // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR
      39              : // IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
      40              : // AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
      41              : // CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
      42              : // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
      43              : // SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
      44              : // THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
      45              : // OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
      46              : // POSSIBILITY OF SUCH DAMAGE.
      47              : 
      48              : #include <EnergyPlus/Data/EnergyPlusData.hh>
      49              : #include <EnergyPlus/DataGlobalConstants.hh>
      50              : #include <EnergyPlus/DataStringGlobals.hh>
      51              : #include <EnergyPlus/FileSystem.hh>
      52              : #include <EnergyPlus/InputProcessing/InputProcessor.hh>
      53              : #include <EnergyPlus/OutputProcessor.hh>
      54              : #include <EnergyPlus/PluginManager.hh>
      55              : #include <EnergyPlus/UtilityRoutines.hh>
      56              : 
      57              : #include <algorithm>
      58              : #include <nlohmann/json.hpp>
      59              : 
      60              : #if LINK_WITH_PYTHON
      61              : 
      62              : #    ifdef _DEBUG
      63              : // We don't want to try to import a debug build of Python here
      64              : // so if we are building a Debug build of the C++ code, we need
      65              : // to undefine _DEBUG during the #include command for Python.h.
      66              : // Otherwise it will fail
      67              : #        undef _DEBUG
      68              : #        include <Python.h>
      69              : #        define _DEBUG
      70              : #    else
      71              : #        include <Python.h>
      72              : #    endif
      73              : 
      74              : #    include <fmt/format.h>
      75              : template <> struct fmt::formatter<PyStatus>
      76              : {
      77              :     // parse is inherited from formatter<string_view>.
      78            0 :     static constexpr auto parse(const format_parse_context &ctx) -> format_parse_context::iterator
      79              :     {
      80            0 :         return ctx.begin();
      81              :     }
      82              : 
      83            0 :     static auto format(const PyStatus &status, format_context &ctx) -> format_context::iterator
      84              :     {
      85            0 :         if (!PyStatus_Exception(status)) {
      86            0 :             return ctx.out();
      87              :         }
      88            0 :         if (PyStatus_IsExit(status)) {
      89            0 :             return format_to(ctx.out(), "Exited with code {}", status.exitcode);
      90              :         }
      91            0 :         if (PyStatus_IsError(status)) {
      92            0 :             auto it = ctx.out();
      93            0 :             it = format_to(it, "Fatal Python error: ");
      94            0 :             if (status.func) {
      95            0 :                 it = format_to(it, "{}: ", status.func);
      96              :             }
      97            0 :             it = format_to(it, "{}", status.err_msg);
      98            0 :             return it;
      99              :         }
     100            0 :         return ctx.out();
     101              :     }
     102              : }; // namespace fmt
     103              : #endif
     104              : 
     105              : namespace EnergyPlus::PluginManagement {
     106              : 
     107            1 : PluginTrendVariable::PluginTrendVariable(const EnergyPlusData &state, std::string _name, int const _numValues, int const _indexOfPluginVariable)
     108            1 :     : name(std::move(_name)), numValues(_numValues), indexOfPluginVariable(_indexOfPluginVariable)
     109              : {
     110              :     // initialize the deque, so it can be queried immediately, even with just zeroes
     111            5 :     for (int i = 1; i <= this->numValues; i++) {
     112            4 :         this->values.push_back(0);
     113            4 :         this->times.push_back(-i * state.dataGlobal->TimeStepZone);
     114              :     }
     115            1 : }
     116              : 
     117           29 : void registerNewCallback(const EnergyPlusData &state, EMSManager::EMSCallFrom const iCalledFrom, const std::function<void(void *)> &f)
     118              : {
     119           29 :     state.dataPluginManager->callbacks[iCalledFrom].push_back(f);
     120           29 : }
     121              : 
     122            2 : void registerUserDefinedCallback(const EnergyPlusData &state, const std::function<void(void *)> &f, const std::string &programNameInInputFile)
     123              : {
     124              :     // internally, E+ will UPPER the program name; we should upper the passed in registration name so it matches
     125            2 :     state.dataPluginManager->userDefinedCallbackNames.push_back(Util::makeUPPER(programNameInInputFile));
     126            2 :     state.dataPluginManager->userDefinedCallbacks.push_back(f);
     127            2 : }
     128              : 
     129           53 : void onBeginEnvironment(const EnergyPlusData &state)
     130              : {
     131              :     // reset vars and trends -- sensors and actuators are reset by EMS
     132           53 :     for (auto &v : state.dataPluginManager->globalVariableValues) {
     133            0 :         v = 0;
     134              :     }
     135              :     // reinitialize trend variables so old data are purged
     136           53 :     for (auto &tr : state.dataPluginManager->trends) {
     137            0 :         tr.reset();
     138              :     }
     139           53 : }
     140              : 
     141         1321 : int PluginManager::numActiveCallbacks(const EnergyPlusData &state)
     142              : {
     143         1321 :     return static_cast<int>(state.dataPluginManager->callbacks.size() + state.dataPluginManager->userDefinedCallbacks.size());
     144              : }
     145              : 
     146      1459177 : void runAnyRegisteredCallbacks(EnergyPlusData &state, EMSManager::EMSCallFrom const iCalledFrom, bool &anyRan)
     147              : {
     148      1459177 :     if (state.dataGlobal->KickOffSimulation) return;
     149      1513001 :     for (auto const &cb : state.dataPluginManager->callbacks[iCalledFrom]) {
     150        55807 :         if (iCalledFrom == EMSManager::EMSCallFrom::UserDefinedComponentModel) {
     151            0 :             continue; // these are called -intentionally- using the runSingleUserDefinedCallback method
     152              :         }
     153        55807 :         cb(&state);
     154        55807 :         anyRan = true;
     155              :     }
     156              : #if LINK_WITH_PYTHON
     157      2044724 :     for (auto &plugin : state.dataPluginManager->plugins) {
     158       587530 :         if (plugin.runDuringWarmup || !state.dataGlobal->WarmupFlag) {
     159       587530 :             if (plugin.run(state, iCalledFrom)) anyRan = true;
     160              :         }
     161              :     }
     162              : #endif
     163              : }
     164              : 
     165              : #if LINK_WITH_PYTHON
     166           55 : std::string pythonStringForUsage(const EnergyPlusData &state)
     167              : {
     168           55 :     if (state.dataGlobal->errorCallback) {
     169            4 :         return "Python Version not accessible during API calls";
     170              :     }
     171           53 :     std::string sVersion = Py_GetVersion();
     172              :     // 3.8.3 (default, Jun  2 2020, 15:25:16) \n[GCC 7.5.0]
     173              :     // Remove the '\n'
     174           53 :     sVersion.erase(std::remove(sVersion.begin(), sVersion.end(), '\n'), sVersion.end());
     175           53 :     return "Linked to Python Version: \"" + sVersion + "\"";
     176           53 : }
     177              : #else
     178              : std::string pythonStringForUsage([[maybe_unused]] const EnergyPlusData &state)
     179              : {
     180              :     return "This version of EnergyPlus not linked to Python library.";
     181              : }
     182              : #endif
     183              : 
     184           73 : void PluginManager::setupOutputVariables([[maybe_unused]] EnergyPlusData &state)
     185              : {
     186              : #if LINK_WITH_PYTHON
     187              :     // with the PythonPlugin:Variables all set in memory, we can now set them up as outputs as needed
     188           73 :     std::string const sOutputVariable = "PythonPlugin:OutputVariable";
     189           73 :     int const outputVarInstances = state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, sOutputVariable);
     190           73 :     if (outputVarInstances > 0) {
     191            0 :         auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(sOutputVariable);
     192            0 :         if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
     193              :             ShowSevereError(state, format("{}: Somehow getNumObjectsFound was > 0 but epJSON.find found 0", sOutputVariable)); // LCOV_EXCL_LINE
     194              :         }
     195            0 :         auto &instancesValue = instances.value();
     196            0 :         for (auto instance = instancesValue.begin(); instance != instancesValue.end(); ++instance) {
     197            0 :             auto const &fields = instance.value();
     198            0 :             std::string const &thisObjectName = instance.key();
     199            0 :             std::string const objNameUC = Util::makeUPPER(thisObjectName);
     200              :             // no need to validate name, the JSON will validate that.
     201            0 :             state.dataInputProcessing->inputProcessor->markObjectAsUsed(sOutputVariable, thisObjectName);
     202            0 :             std::string varName = fields.at("python_plugin_variable_name").get<std::string>();
     203            0 :             std::string avgOrSum = Util::makeUPPER(fields.at("type_of_data_in_variable").get<std::string>());
     204            0 :             std::string updateFreq = Util::makeUPPER(fields.at("update_frequency").get<std::string>());
     205            0 :             std::string units;
     206            0 :             if (fields.find("units") != fields.end()) {
     207            0 :                 units = fields.at("units").get<std::string>();
     208              :             }
     209              :             // get the index of the global variable, fatal if it doesn't mach one
     210              :             // validate type of data, update frequency, and look up units enum value
     211              :             // call setup output variable - variable TYPE is "PythonPlugin:OutputVariable"
     212            0 :             int variableHandle = EnergyPlus::PluginManagement::PluginManager::getGlobalVariableHandle(state, varName);
     213            0 :             if (variableHandle == -1) {
     214            0 :                 ShowSevereError(state, "Failed to match Python Plugin Output Variable");
     215            0 :                 ShowContinueError(state, format("Trying to create output instance for variable name \"{}\"", varName));
     216            0 :                 ShowContinueError(state, "No match found, make sure variable is listed in PythonPlugin:Variables object");
     217            0 :                 ShowFatalError(state, "Python Plugin Output Variable problem causes program termination");
     218              :             }
     219            0 :             bool isMetered = false;
     220            0 :             OutputProcessor::StoreType sAvgOrSum = OutputProcessor::StoreType::Average;
     221            0 :             if (avgOrSum == "SUMMED") {
     222            0 :                 sAvgOrSum = OutputProcessor::StoreType::Sum;
     223            0 :             } else if (avgOrSum == "METERED") {
     224            0 :                 sAvgOrSum = OutputProcessor::StoreType::Sum;
     225            0 :                 isMetered = true;
     226              :             }
     227            0 :             OutputProcessor::TimeStepType sUpdateFreq = OutputProcessor::TimeStepType::Zone;
     228            0 :             if (updateFreq == "SYSTEMTIMESTEP") {
     229            0 :                 sUpdateFreq = OutputProcessor::TimeStepType::System;
     230              :             }
     231            0 :             Constant::Units thisUnit = Constant::Units::None;
     232            0 :             if (!units.empty()) {
     233            0 :                 thisUnit = static_cast<Constant::Units>(getEnumValue(Constant::unitNamesUC, Util::makeUPPER(units)));
     234            0 :                 if (thisUnit == Constant::Units::Invalid) {
     235            0 :                     thisUnit = Constant::Units::customEMS;
     236              :                 }
     237              :             }
     238            0 :             if (!isMetered) {
     239              :                 // regular output variable, ignore the meter/resource stuff and register the variable
     240            0 :                 if (thisUnit != Constant::Units::customEMS) {
     241            0 :                     SetupOutputVariable(state,
     242              :                                         sOutputVariable,
     243              :                                         thisUnit,
     244            0 :                                         state.dataPluginManager->globalVariableValues[variableHandle],
     245              :                                         sUpdateFreq,
     246              :                                         sAvgOrSum,
     247              :                                         thisObjectName);
     248              :                 } else {
     249            0 :                     SetupOutputVariable(state,
     250              :                                         sOutputVariable,
     251              :                                         thisUnit,
     252            0 :                                         state.dataPluginManager->globalVariableValues[variableHandle],
     253              :                                         sUpdateFreq,
     254              :                                         sAvgOrSum,
     255              :                                         thisObjectName,
     256              :                                         Constant::eResource::Invalid,
     257              :                                         OutputProcessor::Group::Invalid,
     258              :                                         OutputProcessor::EndUseCat::Invalid,
     259              :                                         "", // EndUseSub
     260              :                                         "", // Zone
     261              :                                         1,
     262              :                                         1,
     263              :                                         "", // SpaceType
     264              :                                         -999,
     265              :                                         units);
     266              :                 }
     267              :             } else {
     268              :                 // We are doing a metered type, we need to get the extra stuff
     269              :                 // Resource Type
     270            0 :                 if (fields.find("resource_type") == fields.end()) {
     271            0 :                     ShowSevereError(state, format("Input error on PythonPlugin:OutputVariable = {}", thisObjectName));
     272            0 :                     ShowContinueError(state, "The variable was marked as metered, but did not define a resource type");
     273            0 :                     ShowContinueError(state, "For metered variables, the resource type, group type, and end use category must be defined");
     274            0 :                     ShowFatalError(state, "Input error on PythonPlugin:OutputVariable causes program termination");
     275              :                 }
     276            0 :                 std::string const resourceType = EnergyPlus::Util::makeUPPER(fields.at("resource_type").get<std::string>());
     277              :                 Constant::eResource resource;
     278            0 :                 if (resourceType == "WATERUSE") {
     279            0 :                     resource = Constant::eResource::Water;
     280            0 :                 } else if (resourceType == "ONSITEWATERPRODUCED") {
     281            0 :                     resource = Constant::eResource::OnSiteWater;
     282            0 :                 } else if (resourceType == "MAINSWATERSUPPLY") {
     283            0 :                     resource = Constant::eResource::MainsWater;
     284            0 :                 } else if (resourceType == "RAINWATERCOLLECTED") {
     285            0 :                     resource = Constant::eResource::RainWater;
     286            0 :                 } else if (resourceType == "WELLWATERDRAWN") {
     287            0 :                     resource = Constant::eResource::WellWater;
     288            0 :                 } else if (resourceType == "CONDENSATEWATERCOLLECTED") {
     289            0 :                     resource = Constant::eResource::Condensate;
     290            0 :                 } else if (resourceType == "ELECTRICITYPRODUCEDONSITE") {
     291            0 :                     resource = Constant::eResource::ElectricityProduced;
     292            0 :                 } else if (resourceType == "SOLARWATERHEATING") {
     293            0 :                     resource = Constant::eResource::SolarWater;
     294            0 :                 } else if (resourceType == "SOLARAIRHEATING") {
     295            0 :                     resource = Constant::eResource::SolarAir;
     296            0 :                 } else if ((resource = static_cast<Constant::eResource>(getEnumValue(Constant::eResourceNamesUC, resourceType))) ==
     297              :                            Constant::eResource::Invalid) {
     298            0 :                     ShowSevereError(state, format("Invalid input for PythonPlugin:OutputVariable, unexpected Resource Type = {}", resourceType));
     299            0 :                     ShowFatalError(state, "Python plugin output variable input problem causes program termination");
     300              :                 }
     301              : 
     302              :                 // Group Type
     303            0 :                 if (fields.find("group_type") == fields.end()) {
     304            0 :                     ShowSevereError(state, format("Input error on PythonPlugin:OutputVariable = {}", thisObjectName));
     305            0 :                     ShowContinueError(state, "The variable was marked as metered, but did not define a group type");
     306            0 :                     ShowContinueError(state, "For metered variables, the resource type, group type, and end use category must be defined");
     307            0 :                     ShowFatalError(state, "Input error on PythonPlugin:OutputVariable causes program termination");
     308              :                 }
     309            0 :                 std::string const groupType = EnergyPlus::Util::makeUPPER(fields.at("group_type").get<std::string>());
     310            0 :                 auto group = static_cast<OutputProcessor::Group>(getEnumValue(OutputProcessor::groupNamesUC, groupType));
     311            0 :                 if (group == OutputProcessor::Group::Invalid) {
     312            0 :                     ShowSevereError(state, format("Invalid input for PythonPlugin:OutputVariable, unexpected Group Type = {}", groupType));
     313            0 :                     ShowFatalError(state, "Python plugin output variable input problem causes program termination");
     314              :                 }
     315              : 
     316              :                 // End Use Type
     317            0 :                 if (fields.find("end_use_category") == fields.end()) {
     318            0 :                     ShowSevereError(state, format("Input error on PythonPlugin:OutputVariable = {}", thisObjectName));
     319            0 :                     ShowContinueError(state, "The variable was marked as metered, but did not define an end-use category");
     320            0 :                     ShowContinueError(state, "For metered variables, the resource type, group type, and end use category must be defined");
     321            0 :                     ShowFatalError(state, "Input error on PythonPlugin:OutputVariable causes program termination");
     322              :                 }
     323            0 :                 std::string const endUse = EnergyPlus::Util::makeUPPER(fields.at("end_use_category").get<std::string>());
     324            0 :                 auto endUseCat = static_cast<OutputProcessor::EndUseCat>(getEnumValue(OutputProcessor::endUseCatNamesUC, endUse));
     325              : 
     326            0 :                 if (endUseCat == OutputProcessor::EndUseCat::Invalid) {
     327            0 :                     ShowSevereError(state, format("Invalid input for PythonPlugin:OutputVariable, unexpected End-use Subcategory = {}", endUse));
     328            0 :                     ShowFatalError(state, "Python plugin output variable input problem causes program termination");
     329              :                 }
     330              : 
     331              :                 // Additional End Use Types Only Used for EnergyTransfer
     332            0 :                 if ((resource != Constant::eResource::EnergyTransfer) &&
     333            0 :                     (endUseCat == OutputProcessor::EndUseCat::HeatingCoils || endUseCat == OutputProcessor::EndUseCat::CoolingCoils ||
     334            0 :                      endUseCat == OutputProcessor::EndUseCat::Chillers || endUseCat == OutputProcessor::EndUseCat::Boilers ||
     335            0 :                      endUseCat == OutputProcessor::EndUseCat::Baseboard || endUseCat == OutputProcessor::EndUseCat::HeatRecoveryForCooling ||
     336              :                      endUseCat == OutputProcessor::EndUseCat::HeatRecoveryForHeating)) {
     337            0 :                     ShowWarningError(state, format("Inconsistent resource type input for PythonPlugin:OutputVariable = {}", thisObjectName));
     338            0 :                     ShowContinueError(state, format("For end use subcategory = {}, resource type must be EnergyTransfer", endUse));
     339            0 :                     ShowContinueError(state, "Resource type is being reset to EnergyTransfer and the simulation continues...");
     340            0 :                     resource = Constant::eResource::EnergyTransfer;
     341              :                 }
     342              : 
     343            0 :                 std::string sEndUseSubcategory;
     344            0 :                 if (fields.find("end_use_subcategory") != fields.end()) {
     345            0 :                     sEndUseSubcategory = fields.at("end_use_subcategory").get<std::string>();
     346              :                 }
     347              : 
     348            0 :                 if (sEndUseSubcategory.empty()) { // no subcategory
     349            0 :                     SetupOutputVariable(state,
     350              :                                         sOutputVariable,
     351              :                                         thisUnit,
     352            0 :                                         state.dataPluginManager->globalVariableValues[variableHandle],
     353              :                                         sUpdateFreq,
     354              :                                         sAvgOrSum,
     355              :                                         thisObjectName,
     356              :                                         resource,
     357              :                                         group,
     358              :                                         endUseCat);
     359              :                 } else { // has subcategory
     360            0 :                     SetupOutputVariable(state,
     361              :                                         sOutputVariable,
     362              :                                         thisUnit,
     363            0 :                                         state.dataPluginManager->globalVariableValues[variableHandle],
     364              :                                         sUpdateFreq,
     365              :                                         sAvgOrSum,
     366              :                                         thisObjectName,
     367              :                                         resource,
     368              :                                         group,
     369              :                                         endUseCat,
     370              :                                         sEndUseSubcategory);
     371              :                 }
     372            0 :             }
     373            0 :         } // for (instance)
     374              :     }     // if (OutputVarInstances > 0)
     375              : #endif
     376           73 : } // setupOutputVariables()
     377              : 
     378              : #if LINK_WITH_PYTHON
     379            3 : void initPython(EnergyPlusData &state, fs::path const &pathToPythonPackages)
     380              : {
     381              :     PyStatus status;
     382              : 
     383              :     // first pre-config Python so that it can speak UTF-8
     384              :     PyPreConfig preConfig;
     385              :     // This is the other related line that caused Decent CI to start having trouble.  I'm putting it back to
     386              :     // PyPreConfig_InitPythonConfig, even though I think it should be isolated.  Will deal with this after IO freeze.
     387            3 :     PyPreConfig_InitPythonConfig(&preConfig);
     388              :     // PyPreConfig_InitIsolatedConfig(&preConfig);
     389            3 :     preConfig.utf8_mode = 1;
     390            3 :     status = Py_PreInitialize(&preConfig);
     391            3 :     if (PyStatus_Exception(status)) {
     392            0 :         ShowFatalError(state, fmt::format("Could not pre-initialize Python to speak UTF-8... {}", status));
     393              :     }
     394              : 
     395              :     PyConfig config;
     396            3 :     PyConfig_InitIsolatedConfig(&config);
     397            3 :     config.isolated = 1;
     398              : 
     399            3 :     status = PyConfig_SetBytesString(&config, &config.program_name, PluginManagement::programName);
     400            3 :     if (PyStatus_Exception(status)) {
     401            0 :         ShowFatalError(state, fmt::format("Could not initialize program_name on PyConfig... {}", status));
     402              :     }
     403              : 
     404            3 :     status = PyConfig_Read(&config);
     405            3 :     if (PyStatus_Exception(status)) {
     406            0 :         ShowFatalError(state, fmt::format("Could not read back the PyConfig... {}", status));
     407              :     }
     408              : 
     409              :     // ReSharper disable once CppRedundantTypenameKeyword
     410              :     if constexpr (std::is_same_v<typename fs::path::value_type, wchar_t>) {
     411              :         // PyConfig_SetString copies the wide character string str into *config_str.
     412              :         // ReSharper disable once CppDFAUnreachableCode
     413              :         std::wstring const ws = pathToPythonPackages.generic_wstring();
     414              :         const wchar_t *wcharPath = ws.c_str();
     415              : 
     416              :         status = PyConfig_SetString(&config, &config.home, wcharPath);
     417              :         if (PyStatus_Exception(status)) {
     418              :             ShowFatalError(state, fmt::format("Could not set home to {:g} on PyConfig... {}", pathToPythonPackages, status));
     419              :         }
     420              :         status = PyConfig_SetString(&config, &config.base_prefix, wcharPath);
     421              :         if (PyStatus_Exception(status)) {
     422              :             ShowFatalError(state, fmt::format("Could not set base_prefix to {:g} on PyConfig... {}", pathToPythonPackages, status));
     423              :         }
     424              :         config.module_search_paths_set = 1;
     425              :         status = PyWideStringList_Append(&config.module_search_paths, wcharPath);
     426              :         if (PyStatus_Exception(status)) {
     427              :             ShowFatalError(state, fmt::format("Could not add {:g} to module_search_paths on PyConfig... {}", pathToPythonPackages, status));
     428              :         }
     429              : 
     430              :     } else {
     431              :         // PyConfig_SetBytesString takes a `const char * str` and decodes str using Py_DecodeLocale() and set the result into *config_str
     432              :         // But we want to avoid doing it three times, so we PyDecodeLocale manually
     433              :         // Py_DecodeLocale can be called because Python has been PreInitialized.
     434            3 :         wchar_t *wcharPath = Py_DecodeLocale(pathToPythonPackages.generic_string().c_str(), nullptr); // This allocates!
     435              : 
     436            3 :         status = PyConfig_SetString(&config, &config.home, wcharPath);
     437            3 :         if (PyStatus_Exception(status)) {
     438            0 :             ShowFatalError(state, fmt::format("Could not set home to {:g} on PyConfig... {}", pathToPythonPackages, status));
     439              :         }
     440            3 :         status = PyConfig_SetString(&config, &config.base_prefix, wcharPath);
     441            3 :         if (PyStatus_Exception(status)) {
     442            0 :             ShowFatalError(state, fmt::format("Could not set base_prefix to {:g} on PyConfig... {}", pathToPythonPackages, status));
     443              :         }
     444            3 :         config.module_search_paths_set = 1;
     445            3 :         status = PyWideStringList_Append(&config.module_search_paths, wcharPath);
     446            3 :         if (PyStatus_Exception(status)) {
     447            0 :             ShowFatalError(state, fmt::format("Could not add {:g} to module_search_paths on PyConfig... {}", pathToPythonPackages, status));
     448              :         }
     449              : 
     450            3 :         PyMem_RawFree(wcharPath);
     451              :     }
     452              : 
     453              :     // This was Py_InitializeFromConfig(&config), but was giving a seg fault when running inside
     454              :     // another Python instance, for example as part of an API run.  Per the example here:
     455              :     // https://docs.python.org/3/c-api/init_config.html#preinitialize-python-with-pypreconfig
     456              :     // It looks like we don't need to initialize from config again, it should be all set up with
     457              :     // the init calls above, so just initialize and move on.
     458              :     // UPDATE: This worked happily for me on Linux, and also when I build locally on Windows, but not on Decent CI
     459              :     // I suspect a difference in behavior for Python versions.  I'm going to temporarily revert this back to initialize
     460              :     // with config and get IO freeze going, then get back to solving it.
     461              :     // Py_Initialize();
     462            3 :     Py_InitializeFromConfig(&config);
     463            3 : }
     464              : 
     465              : // GilGrabber is an RAII helper that will ensure we release the GIL (including if we end up throwing)
     466              : struct GilGrabber
     467              : {
     468        48963 :     GilGrabber()
     469        48963 :     {
     470        48963 :         gil = PyGILState_Ensure();
     471        48963 :     }
     472        48963 :     ~GilGrabber()
     473              :     {
     474        48963 :         PyGILState_Release(gil);
     475        48963 :     }
     476              : 
     477              :     PyGILState_STATE gil;
     478              : };
     479              : 
     480              : #endif // LINK_WITH_PYTHON
     481              : 
     482           76 : PluginManager::PluginManager(EnergyPlusData &state) : eplusRunningViaPythonAPI(state.dataPluginManager->eplusRunningViaPythonAPI)
     483              : {
     484              :     // Now read all the actual plugins and interpret them
     485              :     // IMPORTANT -- DO NOT CALL setup() UNTIL ALL INSTANCES ARE DONE
     486           76 :     std::string const sPlugins = "PythonPlugin:Instance";
     487           76 :     if (state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, sPlugins) == 0) {
     488           73 :         return;
     489              :     }
     490              : 
     491              : #if LINK_WITH_PYTHON
     492              :     // we'll need the program directory for a few things so get it once here at the top and sanitize it
     493            3 :     fs::path programDir;
     494            3 :     if (state.dataGlobal->installRootOverride) {
     495            1 :         programDir = state.dataStrGlobals->exeDirectoryPath;
     496              :     } else {
     497            2 :         programDir = FileSystem::getParentDirectoryPath(FileSystem::getAbsolutePath(FileSystem::getProgramPath()));
     498              :     }
     499            3 :     fs::path const pathToPythonPackages = programDir / "python_lib";
     500              : 
     501            3 :     initPython(state, pathToPythonPackages);
     502              : 
     503              :     // Take control of the global interpreter lock, which will be released via RAII
     504            3 :     GilGrabber gil_grabber;
     505              : 
     506              :     // call this once to allow us to add to, and report, sys.path later as needed
     507            3 :     PyRun_SimpleString("import sys"); // allows us to report sys.path later
     508              : 
     509              :     // we also need to set an extra import path to find some dynamic library loading stuff, again make it relative to the binary
     510            3 :     addToPythonPath(state, programDir / "python_lib/lib-dynload", false);
     511              : 
     512              :     // now for additional paths:
     513              :     // we'll always want to add the program executable directory to PATH so that Python can find the installed pyenergyplus package
     514              :     // we will then optionally add the current working directory to allow Python to find scripts in the current directory
     515              :     // we will then optionally add the directory of the running IDF to allow Python to find scripts kept next to the IDF
     516              :     // we will then optionally add any additional paths the user specifies on the search paths object
     517              : 
     518              :     // so add the executable directory here
     519            3 :     addToPythonPath(state, programDir, false);
     520              : 
     521              :     // Read all the additional search paths next
     522            3 :     std::string const sPaths = "PythonPlugin:SearchPaths";
     523            3 :     int searchPaths = state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, sPaths);
     524            3 :     if (searchPaths > 0) {
     525            0 :         auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(sPaths);
     526            0 :         if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
     527              :             ShowSevereError(state,                                                                                   // LCOV_EXCL_LINE
     528              :                             "PythonPlugin:SearchPaths: Somehow getNumObjectsFound was > 0 but epJSON.find found 0"); // LCOV_EXCL_LINE
     529              :         }
     530            0 :         auto &instancesValue = instances.value();
     531            0 :         for (auto instance = instancesValue.begin(); instance != instancesValue.end(); ++instance) {
     532              :             // This is a unique object, so we should have one, but this is fine
     533            0 :             auto const &fields = instance.value();
     534            0 :             std::string const &thisObjectName = instance.key();
     535            0 :             state.dataInputProcessing->inputProcessor->markObjectAsUsed(sPaths, thisObjectName);
     536            0 :             std::string workingDirFlagUC = "YES";
     537              :             try {
     538            0 :                 workingDirFlagUC = EnergyPlus::Util::makeUPPER(fields.at("add_current_working_directory_to_search_path").get<std::string>());
     539            0 :             } catch ([[maybe_unused]] nlohmann::json::out_of_range &e) {
     540              :                 // defaulted to YES
     541            0 :             }
     542            0 :             if (workingDirFlagUC == "YES") {
     543            0 :                 addToPythonPath(state, ".", false);
     544              :             }
     545            0 :             std::string inputFileDirFlagUC = "YES";
     546              :             try {
     547            0 :                 inputFileDirFlagUC = EnergyPlus::Util::makeUPPER(fields.at("add_input_file_directory_to_search_path").get<std::string>());
     548            0 :             } catch ([[maybe_unused]] nlohmann::json::out_of_range &e) {
     549              :                 // defaulted to YES
     550            0 :             }
     551            0 :             if (inputFileDirFlagUC == "YES") {
     552            0 :                 addToPythonPath(state, state.dataStrGlobals->inputDirPath, false);
     553              :             }
     554              : 
     555            0 :             std::string epInDirFlagUC = "YES";
     556              :             try {
     557            0 :                 epInDirFlagUC = EnergyPlus::Util::makeUPPER(fields.at("add_epin_environment_variable_to_search_path").get<std::string>());
     558            0 :             } catch ([[maybe_unused]] nlohmann::json::out_of_range &e) {
     559              :                 // defaulted to YES
     560            0 :             }
     561            0 :             if (epInDirFlagUC == "YES") {
     562            0 :                 std::string epin_path; // NOLINT(misc-const-correctness)
     563            0 :                 get_environment_variable("epin", epin_path);
     564            0 :                 fs::path const epinPathObject = fs::path(epin_path);
     565            0 :                 if (epinPathObject.empty()) {
     566            0 :                     ShowWarningMessage(
     567              :                         state,
     568              :                         "PluginManager: Search path inputs requested adding epin variable to Python path, but epin variable was empty, skipping.");
     569              :                 } else {
     570            0 :                     fs::path const epinRootDir = FileSystem::getParentDirectoryPath(fs::path(epinPathObject));
     571            0 :                     if (FileSystem::pathExists(epinRootDir)) {
     572            0 :                         addToPythonPath(state, epinRootDir, true);
     573              :                     } else {
     574            0 :                         ShowWarningMessage(state,
     575              :                                            "PluginManager: Search path inputs requested adding epin variable to Python path, but epin "
     576              :                                            "variable value is not a valid existent path, skipping.");
     577              :                     }
     578            0 :                 }
     579            0 :             }
     580              : 
     581              :             try {
     582            0 :                 auto const &vars = fields.at("py_search_paths");
     583            0 :                 for (const auto &var : vars) {
     584              :                     try {
     585            0 :                         addToPythonPath(state, fs::path(var.at("search_path").get<std::string>()), true);
     586            0 :                     } catch ([[maybe_unused]] nlohmann::json::out_of_range &e) {
     587              :                         // empty entry
     588            0 :                     }
     589              :                 }
     590            0 :             } catch ([[maybe_unused]] nlohmann::json::out_of_range &e) {
     591              :                 // catch when no paths are passed
     592              :                 // nothing to do here
     593            0 :             }
     594            0 :         }
     595              :     } else {
     596              :         // no search path objects in the IDF, just do the default behavior: add the current working dir and the input file dir, + epin env var
     597            3 :         addToPythonPath(state, ".", false);
     598            3 :         addToPythonPath(state, state.dataStrGlobals->inputDirPath, false);
     599              : 
     600            3 :         std::string epin_path; // NOLINT(misc-const-correctness)
     601            9 :         get_environment_variable("epin", epin_path);
     602            3 :         fs::path const epinPathObject = fs::path(epin_path);
     603            3 :         if (!epinPathObject.empty()) {
     604            0 :             fs::path const epinRootDir = FileSystem::getParentDirectoryPath(fs::path(epinPathObject));
     605            0 :             if (FileSystem::pathExists(epinRootDir)) {
     606            0 :                 addToPythonPath(state, epinRootDir, true);
     607              :             }
     608            0 :         }
     609            3 :     }
     610              : 
     611              :     // Now read all the actual plugins and interpret them
     612              :     // IMPORTANT -- DO NOT CALL setup() UNTIL ALL INSTANCES ARE DONE
     613              :     {
     614            3 :         auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(sPlugins);
     615            3 :         if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
     616              :             ShowSevereError(state,                                                                                // LCOV_EXCL_LINE
     617              :                             "PythonPlugin:Instance: Somehow getNumObjectsFound was > 0 but epJSON.find found 0"); // LCOV_EXCL_LINE
     618              :         }
     619            3 :         auto &instancesValue = instances.value();
     620            6 :         for (auto instance = instancesValue.begin(); instance != instancesValue.end(); ++instance) {
     621            3 :             auto const &fields = instance.value();
     622            3 :             std::string const &thisObjectName = instance.key();
     623            3 :             state.dataInputProcessing->inputProcessor->markObjectAsUsed(sPlugins, thisObjectName);
     624            6 :             fs::path modulePath(fields.at("python_module_name").get<std::string>());
     625            6 :             std::string className = fields.at("plugin_class_name").get<std::string>();
     626            3 :             std::string const sWarmup = EnergyPlus::Util::makeUPPER(fields.at("run_during_warmup_days").get<std::string>());
     627            3 :             bool const warmup = (sWarmup == "YES");
     628            3 :             state.dataPluginManager->plugins.emplace_back(modulePath, className, thisObjectName, warmup);
     629            3 :         }
     630              :     }
     631              : 
     632              :     // IMPORTANT - CALL setup() HERE ONCE ALL INSTANCES ARE CONSTRUCTED TO AVOID DESTRUCTOR/MEMORY ISSUES DURING VECTOR RESIZING
     633            5 :     for (auto &plugin : state.dataPluginManager->plugins) {
     634            3 :         plugin.setup(state);
     635              :     }
     636              : 
     637            2 :     std::string const sGlobals = "PythonPlugin:Variables";
     638            2 :     int const globalVarInstances = state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, sGlobals);
     639            2 :     if (globalVarInstances > 0) {
     640            0 :         auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(sGlobals);
     641            0 :         if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
     642              :             ShowSevereError(state, format("{}: Somehow getNumObjectsFound was > 0 but epJSON.find found 0", sGlobals)); // LCOV_EXCL_LINE
     643              :         }
     644            0 :         std::set<std::string> uniqueNames;
     645            0 :         auto &instancesValue = instances.value();
     646            0 :         for (auto instance = instancesValue.begin(); instance != instancesValue.end(); ++instance) {
     647            0 :             auto const &fields = instance.value();
     648            0 :             std::string const &thisObjectName = instance.key();
     649            0 :             state.dataInputProcessing->inputProcessor->markObjectAsUsed(sGlobals, thisObjectName);
     650            0 :             auto const &vars = fields.at("global_py_vars");
     651            0 :             for (const auto &var : vars) {
     652            0 :                 std::string const varNameToAdd = var.at("variable_name").get<std::string>();
     653            0 :                 if (uniqueNames.find(varNameToAdd) == uniqueNames.end()) {
     654            0 :                     this->addGlobalVariable(state, varNameToAdd);
     655            0 :                     uniqueNames.insert(varNameToAdd);
     656              :                 } else {
     657            0 :                     ShowWarningMessage(state,
     658            0 :                                        format("Found duplicate variable name in PythonPLugin:Variables objects, ignoring: \"{}\"", varNameToAdd));
     659              :                 }
     660            0 :             }
     661              :         }
     662            0 :     }
     663              : 
     664              :     // PythonPlugin:TrendVariable,
     665              :     //       \memo This object sets up a Python plugin trend variable from an Python plugin variable
     666              :     //       \memo A trend variable logs values across timesteps
     667              :     //       \min-fields 3
     668              :     //  A1 , \field Name
     669              :     //       \required-field
     670              :     //       \type alpha
     671              :     //  A2 , \field Name of a Python Plugin Variable
     672              :     //       \required-field
     673              :     //       \type alpha
     674              :     //  N1 ; \field Number of Timesteps to be Logged
     675              :     //       \required-field
     676              :     //       \type integer
     677              :     //       \minimum 1
     678            2 :     std::string const sTrends = "PythonPlugin:TrendVariable";
     679            2 :     int const trendInstances = state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, sTrends);
     680            2 :     if (trendInstances > 0) {
     681            0 :         auto const instances = state.dataInputProcessing->inputProcessor->epJSON.find(sTrends);
     682            0 :         if (instances == state.dataInputProcessing->inputProcessor->epJSON.end()) {
     683              :             ShowSevereError(state, format("{}: Somehow getNumObjectsFound was > 0 but epJSON.find found 0", sTrends)); // LCOV_EXCL_LINE
     684              :         }
     685            0 :         auto &instancesValue = instances.value();
     686            0 :         for (auto instance = instancesValue.begin(); instance != instancesValue.end(); ++instance) {
     687            0 :             auto const &fields = instance.value();
     688            0 :             std::string const &thisObjectName = EnergyPlus::Util::makeUPPER(instance.key());
     689            0 :             state.dataInputProcessing->inputProcessor->markObjectAsUsed(sGlobals, thisObjectName);
     690            0 :             std::string variableName = fields.at("name_of_a_python_plugin_variable").get<std::string>();
     691            0 :             int variableIndex = EnergyPlus::PluginManagement::PluginManager::getGlobalVariableHandle(state, variableName);
     692            0 :             int numValues = fields.at("number_of_timesteps_to_be_logged").get<int>();
     693            0 :             state.dataPluginManager->trends.emplace_back(state, thisObjectName, numValues, variableIndex);
     694            0 :             this->maxTrendVariableIndex++;
     695            0 :         }
     696              :     }
     697              : 
     698              :     // Release the global interpreter lock is done via RAII
     699              : 
     700              :     // setting up output variables deferred until later in the simulation setup process
     701              : #else
     702              :     // need to alert only if a plugin instance is found
     703              :     EnergyPlus::ShowFatalError(state, "Python Plugin instance found, but this build of EnergyPlus is not compiled with Python.");
     704              : #endif
     705           80 : }
     706              : 
     707           59 : PluginManager::~PluginManager()
     708              : {
     709              : #if LINK_WITH_PYTHON
     710           59 :     if (!this->eplusRunningViaPythonAPI) {
     711           57 :         if (Py_IsInitialized() != 0) {
     712            2 :             if (Py_FinalizeEx() < 0) {
     713            0 :                 exit(120);
     714              :             }
     715              :         }
     716              :     }
     717              : #endif // LINK_WITH_PYTHON
     718           59 : }
     719              : 
     720            3 : PluginInstance::PluginInstance(const fs::path &_modulePath, const std::string &_className, std::string emsName, bool runPluginDuringWarmup)
     721            3 :     : modulePath(_modulePath), className(_className), emsAlias(std::move(emsName)), runDuringWarmup(runPluginDuringWarmup),
     722            3 :       stringIdentifier(FileSystem::toString(_modulePath) + "." + _className)
     723              : {
     724            3 : }
     725              : 
     726            1 : void PluginInstance::reportPythonError([[maybe_unused]] EnergyPlusData &state)
     727              : {
     728              : #if LINK_WITH_PYTHON
     729            1 :     PyObject *exc_type = nullptr;
     730            1 :     PyObject *exc_value = nullptr;
     731            1 :     PyObject *exc_tb = nullptr;
     732            1 :     PyErr_Fetch(&exc_type, &exc_value, &exc_tb);
     733              :     // Normalizing the exception is needed. Without it, our custom EnergyPlusException go through just fine
     734              :     // but any ctypes built-in exception for eg will have wrong types
     735            1 :     PyErr_NormalizeException(&exc_type, &exc_value, &exc_tb);
     736            1 :     PyObject *str_exc_value = PyObject_Repr(exc_value); // Now a unicode object
     737            1 :     PyObject *pyStr2 = PyUnicode_AsEncodedString(str_exc_value, "utf-8", "Error ~");
     738              :     Py_DECREF(str_exc_value);
     739            1 :     char *strExcValue = PyBytes_AsString(pyStr2); // NOLINT(hicpp-signed-bitwise)
     740              :     Py_DECREF(pyStr2);
     741            2 :     ShowContinueError(state, "Python error description follows: ");
     742            2 :     ShowContinueError(state, strExcValue);
     743              : 
     744              :     // See if we can get a full traceback.
     745              :     // Calls into python, and does the same as capturing the exception in `e`
     746              :     // then `print(traceback.format_exception(e.type, e.value, e.tb))`
     747            1 :     PyObject *pModuleName = PyUnicode_DecodeFSDefault("traceback");
     748            1 :     PyObject *pyth_module = PyImport_Import(pModuleName);
     749              :     Py_DECREF(pModuleName);
     750              : 
     751            1 :     if (pyth_module == nullptr) {
     752            0 :         ShowContinueError(state, "Cannot find 'traceback' module in reportPythonError(), this is weird");
     753            0 :         return;
     754              :     }
     755              : 
     756            1 :     PyObject *pyth_func = PyObject_GetAttrString(pyth_module, "format_exception");
     757              :     Py_DECREF(pyth_module); // PyImport_Import returns a new reference, decrement it
     758              : 
     759            1 :     if (pyth_func || PyCallable_Check(pyth_func)) {
     760              : 
     761            1 :         PyObject *pyth_val = PyObject_CallFunction(pyth_func, "OOO", exc_type, exc_value, exc_tb);
     762              : 
     763              :         // traceback.format_exception returns a list, so iterate on that
     764            1 :         if (!pyth_val || !PyList_Check(pyth_val)) { // NOLINT(hicpp-signed-bitwise)
     765            0 :             ShowContinueError(state, "In reportPythonError(), traceback.format_exception did not return a list.");
     766            0 :             return;
     767              :         }
     768              : 
     769            1 :         Py_ssize_t numVals = PyList_Size(pyth_val);
     770            1 :         if (numVals == 0) {
     771            0 :             ShowContinueError(state, "No traceback available");
     772            0 :             return;
     773              :         }
     774              : 
     775            2 :         ShowContinueError(state, "Python traceback follows: ");
     776            2 :         ShowContinueError(state, "```");
     777              : 
     778            4 :         for (Py_ssize_t itemNum = 0; itemNum < numVals; itemNum++) {
     779            3 :             PyObject *item = PyList_GetItem(pyth_val, itemNum);
     780            3 :             if (PyUnicode_Check(item)) { // NOLINT(hicpp-signed-bitwise) -- something inside Python code causes warning
     781            3 :                 std::string traceback_line = PyUnicode_AsUTF8(item);
     782            3 :                 if (!traceback_line.empty() && traceback_line[traceback_line.length() - 1] == '\n') {
     783            3 :                     traceback_line.erase(traceback_line.length() - 1);
     784              :                 }
     785            3 :                 ShowContinueError(state, format(" >>> {}", traceback_line));
     786            3 :             }
     787              :             // PyList_GetItem returns a borrowed reference, do not decrement
     788              :         }
     789              : 
     790            3 :         ShowContinueError(state, "```");
     791              : 
     792              :         // PyList_Size returns a borrowed reference, do not decrement
     793              :         Py_DECREF(pyth_val); // PyObject_CallFunction returns new reference, decrement
     794              :     }
     795              :     Py_DECREF(pyth_func); // PyObject_GetAttrString returns a new reference, decrement it
     796              : #endif
     797              : }
     798              : 
     799            3 : void PluginInstance::setup([[maybe_unused]] EnergyPlusData &state)
     800              : {
     801              : #if LINK_WITH_PYTHON
     802              :     // this first section is really all about just ultimately getting a full Python class instance
     803              :     // this answer helped with a few things: https://ru.stackoverflow.com/a/785927
     804              : 
     805              :     PyObject *pModuleName;
     806              :     // ReSharper disable once CppRedundantTypenameKeyword
     807              :     if constexpr (std::is_same_v<typename fs::path::value_type, wchar_t>) {
     808              :         // ReSharper disable once CppDFAUnreachableCode
     809              :         const std::wstring ws = this->modulePath.generic_wstring();
     810              :         pModuleName = PyUnicode_FromWideChar(ws.c_str(), static_cast<Py_ssize_t>(ws.size())); // New reference
     811              :     } else {
     812            3 :         const std::string s = this->modulePath.generic_string();
     813            3 :         pModuleName = PyUnicode_FromString(s.c_str()); // New reference
     814            3 :     }
     815            3 :     if (pModuleName == nullptr) {
     816            0 :         ShowFatalError(state, format("Failed to convert the Module Path \"{:g}\" for import", this->modulePath));
     817              :     }
     818            3 :     this->pModule = PyImport_Import(pModuleName);
     819              :     Py_DECREF(pModuleName);
     820              : 
     821            3 :     if (!this->pModule) {
     822            1 :         ShowSevereError(state, format("Failed to import module \"{:g}\"", this->modulePath));
     823            1 :         ShowContinueError(state, format("Current sys.path={}", PluginManager::currentPythonPath()));
     824              :         // ONLY call PyErr_Print if PyErr has occurred, otherwise it will cause other problems
     825            1 :         if (PyErr_Occurred()) {
     826            1 :             reportPythonError(state);
     827              :         } else {
     828            0 :             ShowContinueError(state, "It could be that the module could not be found, or that there was an error in importing");
     829              :         }
     830            3 :         ShowFatalError(state, "Python import error causes program termination");
     831              :     }
     832            2 :     PyObject *pModuleDict = PyModule_GetDict(this->pModule);
     833            2 :     if (!pModuleDict) {
     834            0 :         ShowSevereError(state, format("Failed to read module dictionary from module \"{:g}\"", this->modulePath));
     835            0 :         if (PyErr_Occurred()) {
     836            0 :             reportPythonError(state);
     837              :         } else {
     838            0 :             ShowContinueError(state, "It could be that the module was empty");
     839              :         }
     840            0 :         ShowFatalError(state, "Python module error causes program termination");
     841              :     }
     842            2 :     std::string fileVarName = "__file__";
     843            2 :     PyObject *pFullPath = PyDict_GetItemString(pModuleDict, fileVarName.c_str());
     844            2 :     if (!pFullPath) {
     845              :         // something went really wrong, this should only happen if you do some *weird* python stuff like
     846              :         // import from database or something
     847            0 :         ShowFatalError(state, "Could not get full path");
     848              :     } else {
     849            2 :         const char *zStr = PyUnicode_AsUTF8(pFullPath);
     850            2 :         std::string sHere(zStr);
     851            2 :         ShowMessage(state, format("PythonPlugin: Class {} imported from: {}", className, sHere));
     852            2 :     }
     853            2 :     PyObject *pClass = PyDict_GetItemString(pModuleDict, className.c_str());
     854              :     // Py_DECREF(pModuleDict);  // PyModule_GetDict returns a borrowed reference, DO NOT decrement
     855            2 :     if (!pClass) {
     856            0 :         ShowSevereError(state, format(R"(Failed to get class type "{}" from module "{:g}")", className, modulePath));
     857            0 :         if (PyErr_Occurred()) {
     858            0 :             reportPythonError(state);
     859              :         } else {
     860            0 :             ShowContinueError(state, "It could be the class name is misspelled or missing.");
     861              :         }
     862            0 :         ShowFatalError(state, "Python class import error causes program termination");
     863              :     }
     864            2 :     if (!PyCallable_Check(pClass)) {
     865            0 :         ShowSevereError(state, format("Got class type \"{}\", but it cannot be called/instantiated", className));
     866            0 :         if (PyErr_Occurred()) {
     867            0 :             reportPythonError(state);
     868              :         } else {
     869            0 :             ShowContinueError(state, "Is it possible the class name is actually just a variable?");
     870              :         }
     871            0 :         ShowFatalError(state, "Python class check error causes program termination");
     872              :     }
     873            2 :     this->pClassInstance = PyObject_CallObject(pClass, nullptr);
     874              :     // Py_DECREF(pClass);  // PyDict_GetItemString returns a borrowed reference, DO NOT decrement
     875            2 :     if (!this->pClassInstance) {
     876            0 :         ShowSevereError(state, format("Something went awry calling class constructor for class \"{}\"", className));
     877            0 :         if (PyErr_Occurred()) {
     878            0 :             reportPythonError(state);
     879              :         } else {
     880            0 :             ShowContinueError(state, "It is possible the plugin class constructor takes extra arguments - it shouldn't.");
     881              :         }
     882            0 :         ShowFatalError(state, "Python class constructor error causes program termination");
     883              :     }
     884              :     // PyObject_CallObject returns a new reference, that we need to manage
     885              :     // I think we need to keep it around in memory though so the class methods can be called later on,
     886              :     // so I don't intend on decrementing it, at least not until the manager destructor
     887              :     // In any case, it will be an **extremely** tiny memory use if we hold onto it a bit too long
     888              : 
     889              :     // check which methods are overridden in the derived class
     890            2 :     std::string const detectOverriddenFunctionName = "_detect_overridden";
     891            2 :     PyObject *detectFunction = PyObject_GetAttrString(this->pClassInstance, detectOverriddenFunctionName.c_str());
     892            2 :     if (!detectFunction || !PyCallable_Check(detectFunction)) {
     893            0 :         ShowSevereError(
     894              :             state,
     895            0 :             format(R"(Could not find or call function "{}" on class "{:g}.{}")", detectOverriddenFunctionName, this->modulePath, this->className));
     896            0 :         if (PyErr_Occurred()) {
     897            0 :             reportPythonError(state);
     898              :         } else {
     899            0 :             ShowContinueError(state, "This function should be available on the base class, so this is strange.");
     900              :         }
     901            0 :         ShowFatalError(state, "Python _detect_overridden() function error causes program termination");
     902              :     }
     903            2 :     PyObject *pFunctionResponse = PyObject_CallFunction(detectFunction, nullptr);
     904              :     Py_DECREF(detectFunction); // PyObject_GetAttrString returns a new reference, decrement it
     905            2 :     if (!pFunctionResponse) {
     906            0 :         ShowSevereError(state, format("Call to _detect_overridden() on {} failed!", this->stringIdentifier));
     907            0 :         if (PyErr_Occurred()) {
     908            0 :             reportPythonError(state);
     909              :         } else {
     910            0 :             ShowContinueError(state, "This is available on the base class and should not be overridden...strange.");
     911              :         }
     912            0 :         ShowFatalError(state, format("Program terminates after call to _detect_overridden() on {} failed!", this->stringIdentifier));
     913              :     }
     914            2 :     if (!PyList_Check(pFunctionResponse)) { // NOLINT(hicpp-signed-bitwise)
     915            0 :         ShowFatalError(state, format("Invalid return from _detect_overridden() on class \"{}\", this is weird", this->stringIdentifier));
     916              :     }
     917            2 :     Py_ssize_t numVals = PyList_Size(pFunctionResponse);
     918              :     // at this point we know which base class methods are being overridden by the derived class
     919              :     // we can loop over them and based on the name check the appropriate flag and assign the function pointer
     920            2 :     if (numVals == 0) {
     921            0 :         ShowFatalError(state,
     922            0 :                        format("Python plugin \"{}\" did not override any base class methods; must override at least one", this->stringIdentifier));
     923              :     }
     924            4 :     for (Py_ssize_t itemNum = 0; itemNum < numVals; itemNum++) {
     925            2 :         PyObject *item = PyList_GetItem(pFunctionResponse, itemNum);
     926            2 :         if (PyUnicode_Check(item)) { // NOLINT(hicpp-signed-bitwise) -- something inside Python code causes warning
     927            2 :             std::string functionName = PyUnicode_AsUTF8(item);
     928            2 :             if (functionName == this->sHookBeginNewEnvironment) {
     929            0 :                 this->bHasBeginNewEnvironment = true;
     930            0 :                 this->pBeginNewEnvironment = PyUnicode_FromString(functionName.c_str());
     931            2 :             } else if (functionName == this->sHookBeginZoneTimestepBeforeSetCurrentWeather) {
     932            0 :                 this->bHasBeginZoneTimestepBeforeSetCurrentWeather = true;
     933            0 :                 this->pBeginZoneTimestepBeforeSetCurrentWeather = PyUnicode_FromString(functionName.c_str());
     934            2 :             } else if (functionName == this->sHookAfterNewEnvironmentWarmUpIsComplete) {
     935            0 :                 this->bHasAfterNewEnvironmentWarmUpIsComplete = true;
     936            0 :                 this->pAfterNewEnvironmentWarmUpIsComplete = PyUnicode_FromString(functionName.c_str());
     937            2 :             } else if (functionName == this->sHookBeginZoneTimestepBeforeInitHeatBalance) {
     938            0 :                 this->bHasBeginZoneTimestepBeforeInitHeatBalance = true;
     939            0 :                 this->pBeginZoneTimestepBeforeInitHeatBalance = PyUnicode_FromString(functionName.c_str());
     940            2 :             } else if (functionName == this->sHookBeginZoneTimestepAfterInitHeatBalance) {
     941            0 :                 this->bHasBeginZoneTimestepAfterInitHeatBalance = true;
     942            0 :                 this->pBeginZoneTimestepAfterInitHeatBalance = PyUnicode_FromString(functionName.c_str());
     943            2 :             } else if (functionName == this->sHookBeginTimestepBeforePredictor) {
     944            2 :                 this->bHasBeginTimestepBeforePredictor = true;
     945            2 :                 this->pBeginTimestepBeforePredictor = PyUnicode_FromString(functionName.c_str());
     946            0 :             } else if (functionName == this->sHookAfterPredictorBeforeHVACManagers) {
     947            0 :                 this->bHasAfterPredictorBeforeHVACManagers = true;
     948            0 :                 this->pAfterPredictorBeforeHVACManagers = PyUnicode_FromString(functionName.c_str());
     949            0 :             } else if (functionName == this->sHookAfterPredictorAfterHVACManagers) {
     950            0 :                 this->bHasAfterPredictorAfterHVACManagers = true;
     951            0 :                 this->pAfterPredictorAfterHVACManagers = PyUnicode_FromString(functionName.c_str());
     952            0 :             } else if (functionName == this->sHookInsideHVACSystemIterationLoop) {
     953            0 :                 this->bHasInsideHVACSystemIterationLoop = true;
     954            0 :                 this->pInsideHVACSystemIterationLoop = PyUnicode_FromString(functionName.c_str());
     955            0 :             } else if (functionName == this->sHookEndOfZoneTimestepBeforeZoneReporting) {
     956            0 :                 this->bHasEndOfZoneTimestepBeforeZoneReporting = true;
     957            0 :                 this->pEndOfZoneTimestepBeforeZoneReporting = PyUnicode_FromString(functionName.c_str());
     958            0 :             } else if (functionName == this->sHookEndOfZoneTimestepAfterZoneReporting) {
     959            0 :                 this->bHasEndOfZoneTimestepAfterZoneReporting = true;
     960            0 :                 this->pEndOfZoneTimestepAfterZoneReporting = PyUnicode_FromString(functionName.c_str());
     961            0 :             } else if (functionName == this->sHookEndOfSystemTimestepBeforeHVACReporting) {
     962            0 :                 this->bHasEndOfSystemTimestepBeforeHVACReporting = true;
     963            0 :                 this->pEndOfSystemTimestepBeforeHVACReporting = PyUnicode_FromString(functionName.c_str());
     964            0 :             } else if (functionName == this->sHookEndOfSystemTimestepAfterHVACReporting) {
     965            0 :                 this->bHasEndOfSystemTimestepAfterHVACReporting = true;
     966            0 :                 this->pEndOfSystemTimestepAfterHVACReporting = PyUnicode_FromString(functionName.c_str());
     967            0 :             } else if (functionName == this->sHookEndOfZoneSizing) {
     968            0 :                 this->bHasEndOfZoneSizing = true;
     969            0 :                 this->pEndOfZoneSizing = PyUnicode_FromString(functionName.c_str());
     970            0 :             } else if (functionName == this->sHookEndOfSystemSizing) {
     971            0 :                 this->bHasEndOfSystemSizing = true;
     972            0 :                 this->pEndOfSystemSizing = PyUnicode_FromString(functionName.c_str());
     973            0 :             } else if (functionName == this->sHookAfterComponentInputReadIn) {
     974            0 :                 this->bHasAfterComponentInputReadIn = true;
     975            0 :                 this->pAfterComponentInputReadIn = PyUnicode_FromString(functionName.c_str());
     976            0 :             } else if (functionName == this->sHookUserDefinedComponentModel) {
     977            0 :                 this->bHasUserDefinedComponentModel = true;
     978            0 :                 this->pUserDefinedComponentModel = PyUnicode_FromString(functionName.c_str());
     979            0 :             } else if (functionName == this->sHookUnitarySystemSizing) {
     980            0 :                 this->bHasUnitarySystemSizing = true;
     981            0 :                 this->pUnitarySystemSizing = PyUnicode_FromString(functionName.c_str());
     982              :             } else {
     983              :                 // the Python _detect_function worker is supposed to ignore any other functions so they don't show up at this point
     984              :                 // I don't think it's appropriate to warn here, so just ignore and move on
     985              :             }
     986            2 :         }
     987              :         // PyList_GetItem returns a borrowed reference, do not decrement
     988              :     }
     989              :     // PyList_Size returns a borrowed reference, do not decrement
     990              :     Py_DECREF(pFunctionResponse); // PyObject_CallFunction returns new reference, decrement
     991              : #endif
     992            2 : }
     993              : 
     994            0 : void PluginInstance::shutdown() const
     995              : {
     996              : #if LINK_WITH_PYTHON
     997            0 :     Py_DECREF(this->pClassInstance);
     998            0 :     Py_DECREF(this->pModule); // PyImport_Import returns a new reference, decrement it
     999            0 :     if (this->bHasBeginNewEnvironment) Py_DECREF(this->pBeginNewEnvironment);
    1000            0 :     if (this->bHasAfterNewEnvironmentWarmUpIsComplete) Py_DECREF(this->pAfterNewEnvironmentWarmUpIsComplete);
    1001            0 :     if (this->bHasBeginZoneTimestepBeforeInitHeatBalance) Py_DECREF(this->pBeginZoneTimestepBeforeInitHeatBalance);
    1002            0 :     if (this->bHasBeginZoneTimestepAfterInitHeatBalance) Py_DECREF(this->pBeginZoneTimestepAfterInitHeatBalance);
    1003            0 :     if (this->bHasBeginTimestepBeforePredictor) Py_DECREF(this->pBeginTimestepBeforePredictor);
    1004            0 :     if (this->bHasAfterPredictorBeforeHVACManagers) Py_DECREF(this->pAfterPredictorBeforeHVACManagers);
    1005            0 :     if (this->bHasAfterPredictorAfterHVACManagers) Py_DECREF(this->pAfterPredictorAfterHVACManagers);
    1006            0 :     if (this->bHasInsideHVACSystemIterationLoop) Py_DECREF(this->pInsideHVACSystemIterationLoop);
    1007            0 :     if (this->bHasEndOfZoneTimestepBeforeZoneReporting) Py_DECREF(this->pEndOfZoneTimestepBeforeZoneReporting);
    1008            0 :     if (this->bHasEndOfZoneTimestepAfterZoneReporting) Py_DECREF(this->pEndOfZoneTimestepAfterZoneReporting);
    1009            0 :     if (this->bHasEndOfSystemTimestepBeforeHVACReporting) Py_DECREF(this->pEndOfSystemTimestepBeforeHVACReporting);
    1010            0 :     if (this->bHasEndOfSystemTimestepAfterHVACReporting) Py_DECREF(this->pEndOfSystemTimestepAfterHVACReporting);
    1011            0 :     if (this->bHasEndOfZoneSizing) Py_DECREF(this->pEndOfZoneSizing);
    1012            0 :     if (this->bHasEndOfSystemSizing) Py_DECREF(this->pEndOfSystemSizing);
    1013            0 :     if (this->bHasAfterComponentInputReadIn) Py_DECREF(this->pAfterComponentInputReadIn);
    1014            0 :     if (this->bHasUserDefinedComponentModel) Py_DECREF(this->pUserDefinedComponentModel);
    1015            0 :     if (this->bHasUnitarySystemSizing) Py_DECREF(this->pUnitarySystemSizing);
    1016              : #endif
    1017            0 : }
    1018              : 
    1019              : #if LINK_WITH_PYTHON
    1020       587530 : bool PluginInstance::run(EnergyPlusData &state, EMSManager::EMSCallFrom iCalledFrom) const
    1021              : {
    1022              :     // returns true if a plugin actually ran
    1023       587530 :     PyObject *pFunctionName = nullptr;
    1024       587530 :     const char *functionName = nullptr;
    1025       587530 :     if (iCalledFrom == EMSManager::EMSCallFrom::BeginNewEnvironment) {
    1026            4 :         if (this->bHasBeginNewEnvironment) {
    1027            0 :             pFunctionName = this->pBeginNewEnvironment;
    1028            0 :             functionName = this->sHookBeginNewEnvironment;
    1029              :         }
    1030       587526 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::BeginZoneTimestepBeforeSetCurrentWeather) {
    1031        48960 :         if (this->bHasBeginZoneTimestepBeforeSetCurrentWeather) {
    1032            0 :             pFunctionName = this->pBeginZoneTimestepBeforeSetCurrentWeather;
    1033            0 :             functionName = this->sHookBeginZoneTimestepBeforeSetCurrentWeather;
    1034              :         }
    1035       538566 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::ZoneSizing) {
    1036            0 :         if (this->bHasEndOfZoneSizing) {
    1037            0 :             pFunctionName = this->pEndOfZoneSizing;
    1038            0 :             functionName = this->sHookEndOfZoneSizing;
    1039              :         }
    1040       538566 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::SystemSizing) {
    1041            0 :         if (this->bHasEndOfSystemSizing) {
    1042            0 :             pFunctionName = this->pEndOfSystemSizing;
    1043            0 :             functionName = this->sHookEndOfSystemSizing;
    1044              :         }
    1045       538566 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::BeginNewEnvironmentAfterWarmUp) {
    1046            4 :         if (this->bHasAfterNewEnvironmentWarmUpIsComplete) {
    1047            0 :             pFunctionName = this->pAfterNewEnvironmentWarmUpIsComplete;
    1048            0 :             functionName = this->sHookAfterNewEnvironmentWarmUpIsComplete;
    1049              :         }
    1050       538562 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::BeginTimestepBeforePredictor) {
    1051        48960 :         if (this->bHasBeginTimestepBeforePredictor) {
    1052        48960 :             pFunctionName = this->pBeginTimestepBeforePredictor;
    1053        48960 :             functionName = this->sHookBeginTimestepBeforePredictor;
    1054              :         }
    1055       489602 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::BeforeHVACManagers) {
    1056        48960 :         if (this->bHasAfterPredictorBeforeHVACManagers) {
    1057            0 :             pFunctionName = this->pAfterPredictorBeforeHVACManagers;
    1058            0 :             functionName = this->sHookAfterPredictorBeforeHVACManagers;
    1059              :         }
    1060       440642 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::AfterHVACManagers) {
    1061        48960 :         if (this->bHasAfterPredictorAfterHVACManagers) {
    1062            0 :             pFunctionName = this->pAfterPredictorAfterHVACManagers;
    1063            0 :             functionName = this->sHookAfterPredictorAfterHVACManagers;
    1064              :         }
    1065       391682 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::HVACIterationLoop) {
    1066        97920 :         if (this->bHasInsideHVACSystemIterationLoop) {
    1067            0 :             pFunctionName = this->pInsideHVACSystemIterationLoop;
    1068            0 :             functionName = this->sHookInsideHVACSystemIterationLoop;
    1069              :         }
    1070       293762 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::EndSystemTimestepBeforeHVACReporting) {
    1071        48960 :         if (this->bHasEndOfSystemTimestepBeforeHVACReporting) {
    1072            0 :             pFunctionName = this->pEndOfSystemTimestepBeforeHVACReporting;
    1073            0 :             functionName = this->sHookEndOfSystemTimestepBeforeHVACReporting;
    1074              :         }
    1075       244802 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::EndSystemTimestepAfterHVACReporting) {
    1076        48960 :         if (this->bHasEndOfSystemTimestepAfterHVACReporting) {
    1077            0 :             pFunctionName = this->pEndOfSystemTimestepAfterHVACReporting;
    1078            0 :             functionName = this->sHookEndOfSystemTimestepAfterHVACReporting;
    1079              :         }
    1080       195842 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::EndZoneTimestepBeforeZoneReporting) {
    1081        48960 :         if (this->bHasEndOfZoneTimestepBeforeZoneReporting) {
    1082            0 :             pFunctionName = this->pEndOfZoneTimestepBeforeZoneReporting;
    1083            0 :             functionName = this->sHookEndOfZoneTimestepBeforeZoneReporting;
    1084              :         }
    1085       146882 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::EndZoneTimestepAfterZoneReporting) {
    1086        48960 :         if (this->bHasEndOfZoneTimestepAfterZoneReporting) {
    1087            0 :             pFunctionName = this->pEndOfZoneTimestepAfterZoneReporting;
    1088            0 :             functionName = this->sHookEndOfZoneTimestepAfterZoneReporting;
    1089              :         }
    1090        97922 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::ComponentGetInput) {
    1091            0 :         if (this->bHasAfterComponentInputReadIn) {
    1092            0 :             pFunctionName = this->pAfterComponentInputReadIn;
    1093            0 :             functionName = this->sHookAfterComponentInputReadIn;
    1094              :         }
    1095        97922 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::UserDefinedComponentModel) {
    1096            0 :         if (this->bHasUserDefinedComponentModel) {
    1097            0 :             pFunctionName = this->pUserDefinedComponentModel;
    1098            0 :             functionName = this->sHookUserDefinedComponentModel;
    1099              :         }
    1100        97922 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::UnitarySystemSizing) {
    1101            0 :         if (this->bHasUnitarySystemSizing) {
    1102            0 :             pFunctionName = this->pUnitarySystemSizing;
    1103            0 :             functionName = this->sHookUnitarySystemSizing;
    1104              :         }
    1105        97922 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::BeginZoneTimestepBeforeInitHeatBalance) {
    1106        48960 :         if (this->bHasBeginZoneTimestepBeforeInitHeatBalance) {
    1107            0 :             pFunctionName = this->pBeginZoneTimestepBeforeInitHeatBalance;
    1108            0 :             functionName = this->sHookBeginZoneTimestepBeforeInitHeatBalance;
    1109              :         }
    1110        48962 :     } else if (iCalledFrom == EMSManager::EMSCallFrom::BeginZoneTimestepAfterInitHeatBalance) {
    1111        48960 :         if (this->bHasBeginZoneTimestepAfterInitHeatBalance) {
    1112            0 :             pFunctionName = this->pBeginZoneTimestepAfterInitHeatBalance;
    1113            0 :             functionName = this->sHookBeginZoneTimestepAfterInitHeatBalance;
    1114              :         }
    1115              :     }
    1116              : 
    1117              :     // leave if we didn't find a match
    1118       587530 :     if (!pFunctionName) {
    1119       538570 :         return false;
    1120              :     }
    1121              : 
    1122              :     // Take control of the global interpreter lock, which will be released via RAII
    1123        48960 :     GilGrabber gil_grabber;
    1124              : 
    1125              :     // then call the main function
    1126              :     // static const PyObject oneArgObjFormat = Py_BuildValue)("O");
    1127        48960 :     PyObject *pStateInstance = PyLong_FromVoidPtr(&state);
    1128        48960 :     PyObject *pFunctionResponse = PyObject_CallMethodObjArgs(this->pClassInstance, pFunctionName, pStateInstance, nullptr);
    1129              :     Py_DECREF(pStateInstance);
    1130        48960 :     if (!pFunctionResponse) {
    1131            0 :         std::string const functionNameAsString(functionName); // only convert to string if an error occurs
    1132            0 :         ShowSevereError(state, format("Call to {}() on {} failed!", functionNameAsString, this->stringIdentifier));
    1133            0 :         if (PyErr_Occurred()) {
    1134            0 :             reportPythonError(state);
    1135              :         } else {
    1136            0 :             ShowContinueError(state, "This could happen for any number of reasons, check the plugin code.");
    1137              :         }
    1138            0 :         ShowFatalError(state, format("Program terminates after call to {}() on {} failed!", functionNameAsString, this->stringIdentifier));
    1139            0 :     }
    1140        48960 :     if (PyLong_Check(pFunctionResponse)) { // NOLINT(hicpp-signed-bitwise)
    1141        48960 :         long exitCode = PyLong_AsLong(pFunctionResponse);
    1142        48960 :         if (exitCode == 0) {
    1143              :             // success
    1144            0 :         } else if (exitCode == 1) {
    1145            0 :             ShowFatalError(state, format("Python Plugin \"{}\" returned 1 to indicate EnergyPlus should abort", this->stringIdentifier));
    1146              :         }
    1147              :     } else {
    1148            0 :         std::string const functionNameAsString(functionName); // only convert to string if an error occurs
    1149            0 :         ShowFatalError(
    1150              :             state,
    1151            0 :             format("Invalid return from {}() on class \"{}, make sure it returns an integer exit code, either zero (success) or one (failure)",
    1152              :                    functionNameAsString,
    1153            0 :                    this->stringIdentifier));
    1154            0 :     }
    1155              :     Py_DECREF(pFunctionResponse); // PyObject_CallFunction returns new reference, decrement
    1156        48960 :     if (state.dataPluginManager->apiErrorFlag) {
    1157            0 :         ShowFatalError(state, "API problems encountered while running plugin cause program termination.");
    1158              :     }
    1159        48960 :     return true;
    1160        48960 : }
    1161              : #else
    1162              : bool PluginInstance::run([[maybe_unused]] EnergyPlusData &state, [[maybe_unused]] EMSManager::EMSCallFrom iCalledFrom) const
    1163              : {
    1164              :     return false;
    1165              : }
    1166              : #endif
    1167              : 
    1168              : #if LINK_WITH_PYTHON
    1169            1 : std::vector<std::string> PluginManager::currentPythonPath()
    1170              : {
    1171            1 :     PyObject *sysPath = PySys_GetObject("path"); // Borrowed reference
    1172            1 :     Py_ssize_t const n = PyList_Size(sysPath);   // Py_ssize_t
    1173            1 :     std::vector<std::string> pathLibs(n);
    1174           11 :     for (Py_ssize_t i = 0; i < n; ++i) {
    1175           10 :         PyObject *element = PyList_GetItem(sysPath, i); // Borrowed reference
    1176           30 :         pathLibs[i] = std::string{PyUnicode_AsUTF8(element)};
    1177              :     }
    1178            1 :     return pathLibs;
    1179            0 : }
    1180              : 
    1181           12 : void PluginManager::addToPythonPath(EnergyPlusData &state, const fs::path &includePath, bool userDefinedPath)
    1182              : {
    1183           12 :     if (includePath.empty()) {
    1184            0 :         return;
    1185              :     }
    1186              : 
    1187              :     // We use generic_string / generic_wstring here, which will always use a forward slash as directory separator even on windows
    1188              :     // This doesn't handle the (very strange, IMHO) case were on unix you have backlashes (which are VALID filenames on Unix!)
    1189              :     // Could use FileSystem::makeNativePath first to convert the backslashes to forward slashes on Unix
    1190              :     PyObject *unicodeIncludePath;
    1191              :     // ReSharper disable once CppRedundantTypenameKeyword
    1192              :     if constexpr (std::is_same_v<typename fs::path::value_type, wchar_t>) {
    1193              :         // ReSharper disable once CppDFAUnreachableCode
    1194              :         const std::wstring ws = includePath.generic_wstring();
    1195              :         unicodeIncludePath = PyUnicode_FromWideChar(ws.c_str(), static_cast<Py_ssize_t>(ws.size())); // New reference
    1196              :     } else {
    1197           12 :         const std::string s = includePath.generic_string();
    1198           12 :         unicodeIncludePath = PyUnicode_FromString(s.c_str()); // New reference
    1199           12 :     }
    1200           12 :     if (unicodeIncludePath == nullptr) {
    1201            0 :         ShowFatalError(state, format("ERROR converting the path \"{:g}\" for addition to the sys.path in Python", includePath));
    1202              :     }
    1203              : 
    1204           12 :     PyObject *sysPath = PySys_GetObject("path"); // Borrowed reference
    1205           12 :     int const ret = PyList_Insert(sysPath, 0, unicodeIncludePath);
    1206              :     Py_DECREF(unicodeIncludePath);
    1207              : 
    1208           12 :     if (ret != 0) {
    1209            0 :         if (PyErr_Occurred()) {
    1210            0 :             PluginInstance::reportPythonError(state);
    1211              :         }
    1212            0 :         ShowFatalError(state, format("ERROR adding \"{:g}\" to the sys.path in Python", includePath));
    1213              :     }
    1214              : 
    1215           12 :     if (userDefinedPath) {
    1216            0 :         ShowMessage(state, format("Successfully added path \"{:g}\" to the sys.path in Python", includePath));
    1217              :     }
    1218              : 
    1219              :     // PyRun_SimpleString)("print(' EPS : ' + str(sys.path))");
    1220              : }
    1221              : #else
    1222              : std::vector<std::string> PluginManager::currentPythonPath()
    1223              : {
    1224              :     return {};
    1225              : }
    1226              : void PluginManager::addToPythonPath([[maybe_unused]] EnergyPlusData &state,
    1227              :                                     [[maybe_unused]] const fs::path &path,
    1228              :                                     [[maybe_unused]] bool userDefinedPath)
    1229              : {
    1230              : }
    1231              : #endif
    1232              : 
    1233              : #if LINK_WITH_PYTHON
    1234            1 : void PluginManager::addGlobalVariable(const EnergyPlusData &state, const std::string &name)
    1235              : {
    1236            1 :     std::string const varNameUC = EnergyPlus::Util::makeUPPER(name);
    1237            1 :     state.dataPluginManager->globalVariableNames.push_back(varNameUC);
    1238            1 :     state.dataPluginManager->globalVariableValues.push_back(Real64());
    1239            1 :     this->maxGlobalVariableIndex++;
    1240            1 : }
    1241              : #else
    1242              : void PluginManager::addGlobalVariable([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] const std::string &name)
    1243              : {
    1244              : }
    1245              : #endif
    1246              : 
    1247              : #if LINK_WITH_PYTHON
    1248            1 : int PluginManager::getGlobalVariableHandle(EnergyPlusData &state, const std::string &name, bool const suppress_warning)
    1249              : { // note zero is a valid handle
    1250            1 :     std::string const varNameUC = EnergyPlus::Util::makeUPPER(name);
    1251            1 :     auto const &gVarNames = state.dataPluginManager->globalVariableNames;
    1252            1 :     auto const it = std::find(gVarNames.begin(), gVarNames.end(), varNameUC);
    1253            1 :     if (it != gVarNames.end()) {
    1254            2 :         return static_cast<int>(std::distance(gVarNames.begin(), it));
    1255              :     }
    1256            0 :     if (suppress_warning) {
    1257            0 :         return -1;
    1258              :     }
    1259            0 :     ShowSevereError(state, "Tried to retrieve handle for a nonexistent plugin global variable");
    1260            0 :     ShowContinueError(state, format("Name looked up: \"{}\", available names: ", varNameUC));
    1261            0 :     for (auto const &gvName : gVarNames) {
    1262            0 :         ShowContinueError(state, format("    \"{}\"", gvName));
    1263              :     }
    1264            0 :     ShowFatalError(state, "Plugin global variable problem causes program termination");
    1265            0 :     return -1; // hush the compiler warning
    1266            1 : }
    1267              : #else
    1268              : int PluginManager::getGlobalVariableHandle([[maybe_unused]] EnergyPlusData &state,
    1269              :                                            [[maybe_unused]] const std::string &name,
    1270              :                                            [[maybe_unused]] bool const suppress_warning)
    1271              : {
    1272              :     return -1;
    1273              : }
    1274              : #endif
    1275              : 
    1276              : #if LINK_WITH_PYTHON
    1277            1 : int PluginManager::getTrendVariableHandle(const EnergyPlusData &state, const std::string &name)
    1278              : {
    1279            1 :     std::string const varNameUC = Util::makeUPPER(name);
    1280            1 :     for (size_t i = 0; i < state.dataPluginManager->trends.size(); i++) {
    1281            1 :         auto &thisTrend = state.dataPluginManager->trends[i];
    1282            1 :         if (thisTrend.name == varNameUC) {
    1283            1 :             return static_cast<int>(i);
    1284              :         }
    1285              :     }
    1286            0 :     return -1;
    1287            1 : }
    1288              : #else
    1289              : int PluginManager::getTrendVariableHandle([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] const std::string &name)
    1290              : {
    1291              :     return -1;
    1292              : }
    1293              : #endif
    1294              : 
    1295              : #if LINK_WITH_PYTHON
    1296            8 : Real64 PluginManager::getTrendVariableValue(const EnergyPlusData &state, int handle, int timeIndex)
    1297              : {
    1298            8 :     return state.dataPluginManager->trends[handle].values[timeIndex];
    1299              : }
    1300              : #else
    1301              : Real64 PluginManager::getTrendVariableValue([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] int handle, [[maybe_unused]] int timeIndex)
    1302              : {
    1303              :     return 0.0;
    1304              : }
    1305              : #endif
    1306              : 
    1307              : #if LINK_WITH_PYTHON
    1308            0 : Real64 PluginManager::getTrendVariableAverage(const EnergyPlusData &state, int handle, int count)
    1309              : {
    1310            0 :     Real64 sum = 0;
    1311            0 :     for (int i = 0; i < count; i++) {
    1312            0 :         sum += state.dataPluginManager->trends[handle].values[i];
    1313              :     }
    1314            0 :     return sum / count;
    1315              : }
    1316              : #else
    1317              : Real64 PluginManager::getTrendVariableAverage([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] int handle, [[maybe_unused]] int count)
    1318              : {
    1319              :     return 0.0;
    1320              : }
    1321              : #endif
    1322              : 
    1323              : #if LINK_WITH_PYTHON
    1324            0 : Real64 PluginManager::getTrendVariableMin(const EnergyPlusData &state, int handle, int count)
    1325              : {
    1326            0 :     Real64 minimumValue = 9999999999999;
    1327            0 :     for (int i = 0; i < count; i++) {
    1328            0 :         if (state.dataPluginManager->trends[handle].values[i] < minimumValue) {
    1329            0 :             minimumValue = state.dataPluginManager->trends[handle].values[i];
    1330              :         }
    1331              :     }
    1332            0 :     return minimumValue;
    1333              : }
    1334              : #else
    1335              : Real64 PluginManager::getTrendVariableMin([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] int handle, [[maybe_unused]] int count)
    1336              : {
    1337              :     return 0.0;
    1338              : }
    1339              : #endif
    1340              : 
    1341              : #if LINK_WITH_PYTHON
    1342            0 : Real64 PluginManager::getTrendVariableMax(const EnergyPlusData &state, int handle, int count)
    1343              : {
    1344            0 :     Real64 maximumValue = -9999999999999;
    1345            0 :     for (int i = 0; i < count; i++) {
    1346            0 :         if (state.dataPluginManager->trends[handle].values[i] > maximumValue) {
    1347            0 :             maximumValue = state.dataPluginManager->trends[handle].values[i];
    1348              :         }
    1349              :     }
    1350            0 :     return maximumValue;
    1351              : }
    1352              : #else
    1353              : Real64 PluginManager::getTrendVariableMax([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] int handle, [[maybe_unused]] int count)
    1354              : {
    1355              :     return 0.0;
    1356              : }
    1357              : #endif
    1358              : 
    1359              : #if LINK_WITH_PYTHON
    1360            0 : Real64 PluginManager::getTrendVariableSum(const EnergyPlusData &state, int handle, int count)
    1361              : {
    1362            0 :     Real64 sum = 0.0;
    1363            0 :     for (int i = 0; i < count; i++) {
    1364            0 :         sum += state.dataPluginManager->trends[handle].values[i];
    1365              :     }
    1366            0 :     return sum;
    1367              : }
    1368              : #else
    1369              : Real64 PluginManager::getTrendVariableSum([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] int handle, [[maybe_unused]] int count)
    1370              : {
    1371              :     return 0.0;
    1372              : }
    1373              : #endif
    1374              : 
    1375              : #if LINK_WITH_PYTHON
    1376            0 : Real64 PluginManager::getTrendVariableDirection(const EnergyPlusData &state, int handle, int count)
    1377              : {
    1378            0 :     auto &trend = state.dataPluginManager->trends[handle];
    1379            0 :     Real64 timeSum = 0.0;
    1380            0 :     Real64 valueSum = 0.0;
    1381            0 :     Real64 crossSum = 0.0;
    1382            0 :     Real64 powSum = 0.0;
    1383            0 :     for (int i = 0; i < count; i++) {
    1384            0 :         timeSum += trend.times[i];
    1385            0 :         valueSum += trend.values[i];
    1386            0 :         crossSum += trend.times[i] * trend.values[i];
    1387            0 :         powSum += pow2(trend.times[i]);
    1388              :     }
    1389            0 :     Real64 numerator = timeSum * valueSum - count * crossSum;
    1390            0 :     Real64 denominator = pow_2(timeSum) - count * powSum;
    1391            0 :     return numerator / denominator;
    1392              : }
    1393              : #else
    1394              : Real64 PluginManager::getTrendVariableDirection([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] int handle, [[maybe_unused]] int count)
    1395              : {
    1396              :     return 0.0;
    1397              : }
    1398              : #endif
    1399              : 
    1400              : #if LINK_WITH_PYTHON
    1401            1 : size_t PluginManager::getTrendVariableHistorySize(const EnergyPlusData &state, int handle)
    1402              : {
    1403            1 :     return state.dataPluginManager->trends[handle].values.size();
    1404              : }
    1405              : #else
    1406              : size_t PluginManager::getTrendVariableHistorySize([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] int handle)
    1407              : {
    1408              :     return 0;
    1409              : }
    1410              : #endif
    1411              : 
    1412       249948 : void PluginManager::updatePluginValues([[maybe_unused]] EnergyPlusData &state)
    1413              : {
    1414              : #if LINK_WITH_PYTHON
    1415       249951 :     for (auto &trend : state.dataPluginManager->trends) {
    1416            3 :         Real64 newVarValue = getGlobalVariableValue(state, trend.indexOfPluginVariable);
    1417            3 :         trend.values.push_front(newVarValue);
    1418            3 :         trend.values.pop_back();
    1419              :     }
    1420              : #endif
    1421       249948 : }
    1422              : 
    1423              : #if LINK_WITH_PYTHON
    1424            3 : Real64 PluginManager::getGlobalVariableValue(EnergyPlusData &state, int handle)
    1425              : {
    1426            3 :     if (state.dataPluginManager->globalVariableValues.empty()) {
    1427            0 :         ShowFatalError(
    1428              :             state,
    1429              :             "Tried to access plugin global variable but it looks like there aren't any; use the PythonPlugin:Variables object to declare them.");
    1430              :     }
    1431              :     try {
    1432            3 :         return state.dataPluginManager->globalVariableValues[handle]; // TODO: This won't be caught as an exception I think
    1433              :     } catch (...) {
    1434              :         ShowSevereError(state, format("Tried to access plugin global variable value at index {}", handle));
    1435              :         ShowContinueError(state, format("Available handles range from 0 to {}", state.dataPluginManager->globalVariableValues.size() - 1));
    1436              :         ShowFatalError(state, "Plugin global variable problem causes program termination");
    1437              :     }
    1438              :     return 0.0;
    1439              : }
    1440              : #else
    1441              : Real64 PluginManager::getGlobalVariableValue([[maybe_unused]] EnergyPlusData &state, [[maybe_unused]] int handle)
    1442              : {
    1443              :     return 0.0;
    1444              : }
    1445              : #endif
    1446              : 
    1447              : #if LINK_WITH_PYTHON
    1448            3 : void PluginManager::setGlobalVariableValue(EnergyPlusData &state, int handle, Real64 value)
    1449              : {
    1450            3 :     if (state.dataPluginManager->globalVariableValues.empty()) {
    1451            0 :         ShowFatalError(state,
    1452              :                        "Tried to set plugin global variable but it looks like there aren't any; use the PythonPlugin:GlobalVariables "
    1453              :                        "object to declare them.");
    1454              :     }
    1455              :     try {
    1456            3 :         state.dataPluginManager->globalVariableValues[handle] = value; // TODO: This won't be caught as an exception I think
    1457              :     } catch (...) {
    1458              :         ShowSevereError(state, format("Tried to set plugin global variable value at index {}", handle));
    1459              :         ShowContinueError(state, format("Available handles range from 0 to {}", state.dataPluginManager->globalVariableValues.size() - 1));
    1460              :         ShowFatalError(state, "Plugin global variable problem causes program termination");
    1461              :     }
    1462            3 : }
    1463              : #else
    1464              : void PluginManager::setGlobalVariableValue([[maybe_unused]] EnergyPlusData &state, [[maybe_unused]] int handle, [[maybe_unused]] Real64 value)
    1465              : {
    1466              : }
    1467              : #endif
    1468              : 
    1469              : #if LINK_WITH_PYTHON
    1470            1 : int PluginManager::getLocationOfUserDefinedPlugin(const EnergyPlusData &state, std::string const &_programName)
    1471              : {
    1472            1 :     for (size_t handle = 0; handle < state.dataPluginManager->plugins.size(); handle++) {
    1473            0 :         auto const &thisPlugin = state.dataPluginManager->plugins[handle];
    1474            0 :         if (Util::makeUPPER(thisPlugin.emsAlias) == Util::makeUPPER(_programName)) {
    1475            0 :             return static_cast<int>(handle);
    1476              :         }
    1477              :     }
    1478            1 :     return -1;
    1479              : }
    1480              : #else
    1481              : int PluginManager::getLocationOfUserDefinedPlugin([[maybe_unused]] const EnergyPlusData &state, [[maybe_unused]] std::string const &_programName)
    1482              : {
    1483              :     return -1;
    1484              : }
    1485              : #endif
    1486              : 
    1487              : #if LINK_WITH_PYTHON
    1488            0 : void PluginManager::runSingleUserDefinedPlugin(EnergyPlusData &state, int index)
    1489              : {
    1490            0 :     state.dataPluginManager->plugins[index].run(state, EMSManager::EMSCallFrom::UserDefinedComponentModel);
    1491            0 : }
    1492              : #else
    1493              : void PluginManager::runSingleUserDefinedPlugin([[maybe_unused]] EnergyPlusData &state, [[maybe_unused]] int index)
    1494              : {
    1495              : }
    1496              : #endif
    1497              : 
    1498            1 : int PluginManager::getUserDefinedCallbackIndex(const EnergyPlusData &state, const std::string &callbackProgramName)
    1499              : {
    1500            1 :     for (int i = 0; i < static_cast<int>(state.dataPluginManager->userDefinedCallbackNames.size()); i++) {
    1501            1 :         if (state.dataPluginManager->userDefinedCallbackNames[i] == callbackProgramName) {
    1502            1 :             return i;
    1503              :         }
    1504              :     }
    1505            0 :     return -1;
    1506              : }
    1507              : 
    1508         4097 : void PluginManager::runSingleUserDefinedCallback(EnergyPlusData &state, int index)
    1509              : {
    1510         4097 :     if (state.dataGlobal->KickOffSimulation) return;              // Maybe?
    1511         4083 :     state.dataPluginManager->userDefinedCallbacks[index](&state); // Check Index first
    1512              : }
    1513              : 
    1514              : #if LINK_WITH_PYTHON
    1515            0 : bool PluginManager::anyUnexpectedPluginObjects(EnergyPlusData &state)
    1516              : {
    1517            0 :     int numTotalThings = 0;
    1518            0 :     for (std::string const &objToFind : state.dataPluginManager->objectsToFind) {
    1519            0 :         int instances = state.dataInputProcessing->inputProcessor->getNumObjectsFound(state, objToFind);
    1520            0 :         numTotalThings += instances;
    1521            0 :         if (numTotalThings == 1) {
    1522            0 :             ShowSevereMessage(state, "Found PythonPlugin objects in an IDF that is running in an API/Library workflow...this is invalid");
    1523              :         }
    1524            0 :         if (instances > 0) {
    1525            0 :             ShowContinueError(state, format("Invalid PythonPlugin object type: {}", objToFind));
    1526              :         }
    1527              :     }
    1528            0 :     return numTotalThings > 0;
    1529              : }
    1530              : #else
    1531              : bool PluginManager::anyUnexpectedPluginObjects([[maybe_unused]] EnergyPlusData &state)
    1532              : {
    1533              :     return false;
    1534              : }
    1535              : #endif
    1536              : 
    1537              : } // namespace EnergyPlus::PluginManagement
        

Generated by: LCOV version 2.0-1