Repository: NREL/OpenStudio (language: python)

Issue:
Support EnergyPlus Python plugins (Python EMS)
New EnergyPlus release will have preliminary Python plugin capabilities.

@Myoldmopar 


Discussion / hints:
I'd like for this to be implemented. Could we discuss putting it on the roadmap? 

Supporting the PythonPlugin as a replacement to EMS would be actually quite simple (unless I'm mistaken), and very similar to ScheduleFile, it's just a matter of moving the referenced python file around when running the sim.

**edit**: thanks to Shorowit for opening my eyes to the fact that the deal is to reference OSM objects correctly
+1
FWIW, Edwin had created a [proposed design doc](https://docs.google.com/document/d/1plh0t1_9cKNT5m6l3H4apo9iAKBHvJ-QHHF-eteWUqM/edit#heading=h.q6w40yz5atu1).

There was also some past discussion of using embedded ruby (ERB) and create your python EMS in an ERB template (🤢) or, longer term, to just support python EMS via python OS measures (when available).
@shorowit Thanks for the design doc, that's helpful.

Fix patch:
diff --git a/resources/CMakeLists.txt b/resources/CMakeLists.txt
index edc6ebb6c08..caa47fa3d89 100644
--- a/resources/CMakeLists.txt
+++ b/resources/CMakeLists.txt
@@ -139,7 +139,7 @@ set(ENERGYPLUS_OUTPUTS "")
 set(model_resources_src
   model/OpenStudio.idd
   model/schedulefile.csv
-
+  model/PythonPluginThermochromicWindow.py
   model/two_stories_pre_intersect.osm
   model/floorplan_school.osm
   model/7-7_Windows_Complete.osm
diff --git a/resources/model/OpenStudio.idd b/resources/model/OpenStudio.idd
index 276d48a2d31..26e69b1d6b9 100644
--- a/resources/model/OpenStudio.idd
+++ b/resources/model/OpenStudio.idd
@@ -34973,6 +34973,174 @@ OS:EnergyManagementSystem:ConstructionIndexVariable,
        \object-list ConstructionNames
        \required-field
 
+\group Python Plugin System
+
+OS:PythonPlugin:Instance,
+       \memo A single plugin to be executed during the simulation, which can contain multiple calling points
+       \memo for the same class instance by overriding multiple calling point methods.
+       \min-fields 5
+  A1,  \field Handle
+       \type handle
+       \required-field
+  A2,  \field Name
+       \required-field
+       \type alpha
+       \reference ProgramNames
+       \note This is the name used to represent this plugin in traditional EMS fields
+  A3,  \field Run During Warmup Days
+       \note required-field disabled as it has a default
+       \note If this field is enabled, the plugin will be executed during warmup days, otherwise it
+       \note will only be executed once warmup is completed and the actual run period begins
+       \type choice
+       \key Yes
+       \key No
+       \default No
+  A4,  \field External File Name
+       \type object-list
+       \object-list ExternalFileNames
+  A5;  \field Plugin Class Name
+       \required-field
+       \type alpha
+       \note This is the name of the class to be executed as a plugin during a simulation
+       \note The class must inherit the EnergyPlusPlugin base class
+
+OS:PythonPlugin:Variable,
+       \memo This object defines custom Python Plugin variable data that should be shared
+       \memo among all running Python Plugins.
+       \memo all PythonPlugin:Variable objects will be translated onto a single E+ PythonPlugin:Variables
+       \min-fields 3
+  A1,  \field Handle
+       \type handle
+       \required-field
+  A2;  \field Name
+       \required-field
+       \type alpha
+       \reference PythonPluginVariables
+       \note This variable is used to identify and create a shared variable in EnergyPlus.
+       \note This variable can then be used by all running Python Plugin instances.
+
+OS:PythonPlugin:TrendVariable,
+       \memo This object sets up a Python plugin trend variable from an Python plugin variable
+       \memo A trend variable logs values across timesteps
+       \min-fields 4
+  A1,  \field Handle
+       \type handle
+       \required-field
+  A2 , \field Name
+       \required-field
+       \type alpha
+       \reference PythonPluginVariables
+  A3 , \field Name of a Python Plugin Variable
+       \required-field
+       \type object-list
+       \object-list PythonPluginVariables
+  N1 ; \field Number of Timesteps to be Logged
+       \required-field
+       \type integer
+       \minimum 1
+
+OS:PythonPlugin:OutputVariable,
+       \memo This object sets up an EnergyPlus output variable from a Python Plugin variable
+       \min-fields 5
+   A1, \field Handle
+       \type handle
+       \required-field
+   A2, \field Name
+       \required-field
+       \type alpha
+       \reference PythonPluginVariables
+   A3, \field Python Plugin Variable Name
+       \required-field
+       \type object-list
+       \object-list PythonPluginVariables
+   A4, \field Type of Data in Variable
+       \required-field
+       \type choice
+       \key Averaged
+       \key Summed
+       \key Metered
+       \note If Metered is selected, the variable is automatically set to a "Summed" type, and the
+       \note Resource Type, Group Type, and End-Use Subcategory fields on this object are required
+   A5, \field Update Frequency
+       \required-field
+       \type choice
+       \key ZoneTimestep
+       \key SystemTimestep
+   A6, \field Units
+       \note optional but will result in dimensionless units for blank
+       \note EnergyPlus units are standard SI units
+       \type alpha
+   A7, \field Resource Type
+       \note This field is optional for regular output variables with "Type of Data in Variable" set to
+       \note either Averaged or Summed. For Metered variables, this field is required.
+       \note Choose the type of fuel, water, electricity, pollution or heat rate that should be metered.
+       \type choice
+       \key Electricity
+       \key NaturalGas
+       \key Gasoline
+       \key Diesel
+       \key Coal
+       \key FuelOilNo1
+       \key FuelOilNo2
+       \key Propane
+       \key OtherFuel1
+       \key OtherFuel2
+       \key WaterUse
+       \key OnSiteWaterProduced
+       \key MainsWaterSupply
+       \key RainWaterCollected
+       \key WellWaterDrawn
+       \key CondensateWaterCollected
+       \key EnergyTransfer
+       \key Steam
+       \key DistrictCooling
+       \key DistrictHeating
+       \key ElectricityProducedOnSite
+       \key SolarWaterHeating
+       \key SolarAirHeating
+   A8, \field Group Type
+       \note This field is optional for regular output variables with "Type of Data in Variable" set to
+       \note either Averaged or Summed. For Metered variables, this field is required.
+       \note Choose a general classification, building (internal services), HVAC (air systems), or plant (hydronic systems), or system
+       \type choice
+       \key Building
+       \key HVAC
+       \key Plant
+       \key System
+   A9, \field End-Use Category
+       \note This field is optional for regular output variables with "Type of Data in Variable" set to
+       \note either Averaged or Summed. For Metered variables, this field is required.
+       \note Choose how the metered output should be classified for end-use category
+       \type choice
+       \key Heating
+       \key Cooling
+       \key InteriorLights
+       \key ExteriorLights
+       \key InteriorEquipment
+       \key ExteriorEquipment
+       \key Fans
+       \key Pumps
+       \key HeatRejection
+       \key Humidifier
+       \key HeatRecovery
+       \key WaterSystems
+       \key Refrigeration
+       \key OnSiteGeneration
+       \key HeatingCoils
+       \key CoolingCoils
+       \key Chillers
+       \key Boilers
+       \key Baseboard
+       \key HeatRecoveryForCooling
+       \key HeatRecoveryForHeating
+  A10; \field End-Use Subcategory
+       \note This field is always optional. For regular output variables with "Type of Data in Variable" set to
+       \note either Averaged or Summed, this field is completely ignored. For Metered variables, this field is
+       \note optional, but allows custom categorization of the end-uses in the ABUPS End Uses by Subcategory table.
+       \note Enter a user-defined subcategory for this metered output
+       \type alpha
+       \retaincase
+
 \group AirflowNetwork
 
 OS:AirflowNetworkSimulationControl,
diff --git a/resources/model/PythonPluginThermochromicWindow.py b/resources/model/PythonPluginThermochromicWindow.py
new file mode 100644
index 00000000000..ddcd00b1230
--- /dev/null
+++ b/resources/model/PythonPluginThermochromicWindow.py
@@ -0,0 +1,195 @@
+# EnergyPlus, Copyright (c) 1996-2022, The Board of Trustees of the University
+# of Illinois, The Regents of the University of California, through Lawrence
+# Berkeley National Laboratory (subject to receipt of any required approvals
+# from the U.S. Dept. of Energy), Oak Ridge National Laboratory, managed by UT-
+# Battelle, Alliance for Sustainable Energy, LLC, and other contributors. All
+# rights reserved.
+#
+# NOTICE: This Software was developed under funding from the U.S. Department of
+# Energy and the U.S. Government consequently retains certain rights. As such,
+# the U.S. Government has been granted for itself and others acting on its
+# behalf a paid-up, nonexclusive, irrevocable, worldwide license in the
+# Software to reproduce, distribute copies to the public, prepare derivative
+# works, and perform publicly and display publicly, and to permit others to do
+# so.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# (1) Redistributions of source code must retain the above copyright notice,
+#     this list of conditions and the following disclaimer.
+#
+# (2) Redistributions in binary form must reproduce the above copyright notice,
+#     this list of conditions and the following disclaimer in the documentation
+#     and/or other materials provided with the distribution.
+#
+# (3) Neither the name of the University of California, Lawrence Berkeley
+#     National Laboratory, the University of Illinois, U.S. Dept. of Energy nor
+#     the names of its contributors may be used to endorse or promote products
+#     derived from this software without specific prior written permission.
+#
+# (4) Use of EnergyPlus(TM) Name. If Licensee (i) distributes the software in
+#     stand-alone form without changes from the version obtained under this
+#     License, or (ii) Licensee makes a reference solely to the software
+#     portion of its product, Licensee must refer to the software as
+#     "EnergyPlus version X" software, where "X" is the version number Licensee
+#     obtained under this License and may not use a different name for the
+#     software. Except as specifically required in this Section (4), Licensee
+#     shall not use in a company name, a product name, in advertising,
+#     publicity, or other promotional activities any name, trade name,
+#     trademark, logo, or other designation of "EnergyPlus", "E+", "e+" or
+#     confusingly similar designation, without the U.S. Department of Energy's
+#     prior written consent.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+from pyenergyplus.plugin import EnergyPlusPlugin
+
+
+class MySupportClassThatDoesntDeriveEnergyPlusPlugin:
+    pass
+
+
+class ZN_1_wall_south_Window_1_Control(EnergyPlusPlugin):
+
+    def __init__(self):
+        # init parent class
+        super().__init__()
+        self.need_to_get_handles = True
+        self.Win1_Tout_handle = None
+        self.Win1_Construct_handle = None
+        self.TCwindow_25_handle = None
+        self.TCwindow_27_handle = None
+        self.TCwindow_29_handle = None
+        self.TCwindow_31_handle = None
+        self.TCwindow_33_handle = None
+        self.TCwindow_35_handle = None
+        self.TCwindow_37_handle = None
+        self.TCwindow_39_handle = None
+        self.TCwindow_41_handle = None
+        self.TCwindow_43_handle = None
+        self.TCwindow_45_handle = None
+        self.TCwindow_50_handle = None
+        self.TCwindow_55_handle = None
+        self.TCwindow_60_handle = None
+        self.TCwindow_65_handle = None
+        self.TCwindow_70_handle = None
+        self.TCwindow_80_handle = None
+        self.TCwindow_85_handle = None
+
+    def on_begin_timestep_before_predictor(self, state) -> int:
+
+        # api is ready to execute
+        if self.api.exchange.api_data_fully_ready(state):
+
+            # get variable handles if needed
+            if self.need_to_get_handles:
+                self.Win1_Tout_handle = self.api.exchange.get_variable_handle(
+                    state,
+                    "Surface Outside Face Temperature",
+                    "Perimeter_ZN_1_wall_south_Window_1")
+
+                self.Win1_Construct_handle = self.api.exchange.get_actuator_handle(state,
+                                                                                   "Surface",
+                                                                                   "Construction State",
+                                                                                   "Perimeter_ZN_1_wall_south_Window_1")
+
+                self.TCwindow_25_handle = self.api.exchange.get_construction_handle(state, "TCwindow_25")
+
+                self.TCwindow_27_handle = self.api.exchange.get_construction_handle(state, "TCwindow_27")
+
+                self.TCwindow_29_handle = self.api.exchange.get_construction_handle(state, "TCwindow_29")
+
+                self.TCwindow_31_handle = self.api.exchange.get_construction_handle(state, "TCwindow_31")
+
+                self.TCwindow_33_handle = self.api.exchange.get_construction_handle(state, "TCwindow_33")
+
+                self.TCwindow_35_handle = self.api.exchange.get_construction_handle(state, "TCwindow_35")
+
+                self.TCwindow_37_handle = self.api.exchange.get_construction_handle(state, "TCwindow_37")
+
+                self.TCwindow_39_handle = self.api.exchange.get_construction_handle(state, "TCwindow_39")
+
+                self.TCwindow_41_handle = self.api.exchange.get_construction_handle(state, "TCwindow_41")
+
+                self.TCwindow_43_handle = self.api.exchange.get_construction_handle(state, "TCwindow_43")
+
+                self.TCwindow_45_handle = self.api.exchange.get_construction_handle(state, "TCwindow_45")
+
+                self.TCwindow_50_handle = self.api.exchange.get_construction_handle(state, "TCwindow_50")
+
+                self.TCwindow_55_handle = self.api.exchange.get_construction_handle(state, "TCwindow_55")
+
+                self.TCwindow_60_handle = self.api.exchange.get_construction_handle(state, "TCwindow_60")
+
+                self.TCwindow_65_handle = self.api.exchange.get_construction_handle(state, "TCwindow_65")
+
+                self.TCwindow_70_handle = self.api.exchange.get_construction_handle(state, "TCwindow_70")
+
+                self.TCwindow_75_handle = self.api.exchange.get_construction_handle(state, "TCwindow_75")
+
+                self.TCwindow_80_handle = self.api.exchange.get_construction_handle(state, "TCwindow_80")
+
+                self.TCwindow_85_handle = self.api.exchange.get_construction_handle(state, "TCwindow_85")
+
+                self.need_to_get_handles = False
+
+            # calculate
+            if self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 26.0:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_25_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 28.0:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_27_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 30.0:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_29_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 32.0:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_31_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 34.0:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_33_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 36.0:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_35_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 38.0:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_37_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 40.0:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_39_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 42.0:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_41_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 44.0:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_43_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 47.5:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_45_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 52.5:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_50_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 57.5:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_55_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 62.5:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_60_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 67.5:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_65_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 72.5:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_70_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 77.5:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_75_handle)
+            elif self.api.exchange.get_variable_value(state, self.Win1_Tout_handle) <= 82.5:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_80_handle)
+            else:
+                self.api.exchange.set_actuator_value(state, self.Win1_Construct_handle, self.TCwindow_85_handle)
+
+            return 0
+
+        else:
+            # api not ready, return
+            return 0
+
+
+class AnotherDummyEnergyPlusPluginClass(EnergyPlusPlugin):
+    pass
diff --git a/src/energyplus/CMakeLists.txt b/src/energyplus/CMakeLists.txt
index d441f66ca9d..5cc169ab01e 100644
--- a/src/energyplus/CMakeLists.txt
+++ b/src/energyplus/CMakeLists.txt
@@ -281,6 +281,10 @@ set(${target_name}_src
   ForwardTranslator/ForwardTranslatePortList.cpp
   ForwardTranslator/ForwardTranslatePumpConstantSpeed.cpp
   ForwardTranslator/ForwardTranslatePumpVariableSpeed.cpp
+  ForwardTranslator/ForwardTranslatePythonPluginInstance.cpp
+  ForwardTranslator/ForwardTranslatePythonPluginVariable.cpp
+  ForwardTranslator/ForwardTranslatePythonPluginTrendVariable.cpp
+  ForwardTranslator/ForwardTranslatePythonPluginOutputVariable.cpp
   ForwardTranslator/ForwardTranslateRefractionExtinctionGlazing.cpp
   ForwardTranslator/ForwardTranslateRefrigerationAirChiller.cpp
   ForwardTranslator/ForwardTranslateRefrigerationCase.cpp
@@ -715,6 +719,10 @@ set(${target_name}_test_src
   Test/PerformancePrecisionTradeoffs_GTest.cpp
   Test/PlantEquipmentOperationSchemes_GTest.cpp
   Test/PlantComponentUserDefined_GTest.cpp
+  Test/PythonPluginInstance_GTest.cpp
+  Test/PythonPluginVariable_GTest.cpp
+  Test/PythonPluginOutputVariable_GTest.cpp
+  Test/PythonPluginTrendVariable_GTest.cpp
   Test/RunPeriod_GTest.cpp
   Test/RunPeriodControlDaylightSavingTime_GTest.cpp
   Test/RunPeriodControlSpecialDays_GTest.cpp
diff --git a/src/energyplus/ForwardTranslator.cpp b/src/energyplus/ForwardTranslator.cpp
index 0794cf18f02..527521a7321 100644
--- a/src/energyplus/ForwardTranslator.cpp
+++ b/src/energyplus/ForwardTranslator.cpp
@@ -2377,6 +2377,26 @@ namespace energyplus {
         LOG(Warn, "OS_ProgramControl is not currently translated");
         break;
       }
+      case openstudio::IddObjectType::OS_PythonPlugin_Instance: {
+        model::PythonPluginInstance obj = modelObject.cast<PythonPluginInstance>();
+        retVal = translatePythonPluginInstance(obj);
+        break;
+      }
+      case openstudio::IddObjectType::OS_PythonPlugin_Variable: {
+        auto obj = modelObject.cast<PythonPluginVariable>();
+        retVal = translatePythonPluginVariable(obj);
+        break;
+      }
+      case openstudio::IddObjectType::OS_PythonPlugin_TrendVariable: {
+        auto obj = modelObject.cast<PythonPluginTrendVariable>();
+        retVal = translatePythonPluginTrendVariable(obj);
+        break;
+      }
+      case openstudio::IddObjectType::OS_PythonPlugin_OutputVariable: {
+        auto obj = modelObject.cast<PythonPluginOutputVariable>();
+        retVal = translatePythonPluginOutputVariable(obj);
+        break;
+      }
       case openstudio::IddObjectType::OS_RadianceParameters: {
         // no-op
         break;
@@ -3499,6 +3519,11 @@ namespace energyplus {
     result.push_back(IddObjectType::OS_ExternalInterface_FunctionalMockupUnitImport_To_Actuator);
     result.push_back(IddObjectType::OS_ExternalInterface_FunctionalMockupUnitImport_To_Schedule);
     result.push_back(IddObjectType::OS_ExternalInterface_FunctionalMockupUnitImport_To_Variable);
+
+    result.push_back(IddObjectType::OS_PythonPlugin_Instance);
+    result.push_back(IddObjectType::OS_PythonPlugin_Variable);
+    result.push_back(IddObjectType::OS_PythonPlugin_TrendVariable);
+    result.push_back(IddObjectType::OS_PythonPlugin_OutputVariable);
     return result;
   }
 
diff --git a/src/energyplus/ForwardTranslator.hpp b/src/energyplus/ForwardTranslator.hpp
index dbbab258906..8d9a91877dc 100644
--- a/src/energyplus/ForwardTranslator.hpp
+++ b/src/energyplus/ForwardTranslator.hpp
@@ -337,6 +337,10 @@ namespace model {
   class PortList;
   class PumpConstantSpeed;
   class PumpVariableSpeed;
+  class PythonPluginInstance;
+  class PythonPluginVariable;
+  class PythonPluginTrendVariable;
+  class PythonPluginOutputVariable;
   class RefractionExtinctionGlazing;
   class RefrigerationAirChiller;
   class RefrigerationCase;
@@ -1198,6 +1202,14 @@ namespace energyplus {
 
     boost::optional<IdfObject> translatePumpVariableSpeed(model::PumpVariableSpeed& modelObject);
 
+    boost::optional<IdfObject> translatePythonPluginInstance(model::PythonPluginInstance& modelObject);
+
+    boost::optional<IdfObject> translatePythonPluginVariable(model::PythonPluginVariable& modelObject);
+
+    boost::optional<IdfObject> translatePythonPluginTrendVariable(model::PythonPluginTrendVariable& modelObject);
+
+    boost::optional<IdfObject> translatePythonPluginOutputVariable(model::PythonPluginOutputVariable& modelObject);
+
     boost::optional<IdfObject> translateRefractionExtinctionGlazing(model::RefractionExtinctionGlazing& modelObject);
 
     boost::optional<IdfObject> translateRefrigerationAirChiller(model::RefrigerationAirChiller& modelObject);
diff --git a/src/energyplus/ForwardTranslator/ForwardTranslatePythonPluginInstance.cpp b/src/energyplus/ForwardTranslator/ForwardTranslatePythonPluginInstance.cpp
new file mode 100644
index 00000000000..c09f17684a8
--- /dev/null
+++ b/src/energyplus/ForwardTranslator/ForwardTranslatePythonPluginInstance.cpp
@@ -0,0 +1,129 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include "../ForwardTranslator.hpp"
+
+#include "../../model/Model.hpp"
+#include "../../model/PythonPluginInstance.hpp"
+#include "../../model/ExternalFile.hpp"
+
+#include "../../utilities/idf/IdfExtensibleGroup.hpp"
+
+#include <utilities/idd/PythonPlugin_Instance_FieldEnums.hxx>
+#include <utilities/idd/PythonPlugin_SearchPaths_FieldEnums.hxx>
+
+#include "../../utilities/idd/IddEnums.hpp"
+#include <utilities/idd/IddEnums.hxx>
+#include <utilities/idd/IddFactory.hxx>
+
+#include <algorithm>
+
+constexpr static auto pythonSearchPathsName = "Python Plugin Search Paths";
+
+using namespace openstudio::model;
+
+namespace openstudio {
+
+namespace energyplus {
+
+  boost::optional<IdfObject> ForwardTranslator::translatePythonPluginInstance(PythonPluginInstance& modelObject) {
+
+    path filePath = modelObject.externalFile().filePath();
+    if (!openstudio::filesystem::exists(filePath)) {
+      LOG(Warn, modelObject.briefDescription() << "will not be translated, cannot find the referenced file '" << filePath << "'");
+      return boost::none;
+    }
+
+    // make the path correct for this system
+    filePath = openstudio::filesystem::system_complete(filePath);
+
+    // If the plugin class name can't be found in the referenced external file, then it shouldn't be translated
+    // We do it here and inside model, because between the time the user created the model object and now, the python file may have been modified
+    std::string pluginClassName = modelObject.pluginClassName();
+    if (!modelObject.findPluginClassNameInFile(pluginClassName)) {
+      LOG(Warn, "PythonPluginInstance " << modelObject.name().get() << " does not have plugin class name '" << pluginClassName
+                                        << "' contained in referenced external file");
+      return boost::none;
+    }
+
+    // Name
+    IdfObject idfObject = createRegisterAndNameIdfObject(openstudio::IddObjectType::PythonPlugin_Instance, modelObject);
+
+    // Run During Warmup Days
+    if (modelObject.runDuringWarmupDays()) {
+      idfObject.setString(openstudio::PythonPlugin_InstanceFields::RunDuringWarmupDays, "Yes");
+    } else {
+      idfObject.setString(openstudio::PythonPlugin_InstanceFields::RunDuringWarmupDays, "No");
+    }
+
+    // Python Module Name
+    std::string pythonModuleName = toString(filePath.stem());
+    idfObject.setString(openstudio::PythonPlugin_InstanceFields::PythonModuleName, pythonModuleName);
+
+    // Python Class Name
+    idfObject.setString(openstudio::PythonPlugin_InstanceFields::PluginClassName, pluginClassName);
+
+    // PythonPlugin:SearchPaths
+    std::string searchPath = toString(filePath.parent_path());
+
+    auto it = std::find_if(m_idfObjects.begin(), m_idfObjects.end(), [](auto& idfObject) { return idfObject.nameString() == pythonSearchPathsName; });
+    if (it == m_idfObjects.end()) {
+      // First time: create it
+      auto& pythonPluginSearchPaths = m_idfObjects.emplace_back(IddObjectType::PythonPlugin_SearchPaths);
+
+      pythonPluginSearchPaths.setName(pythonSearchPathsName);
+      // These are IDD defaults
+      pythonPluginSearchPaths.setString(PythonPlugin_SearchPathsFields::AddCurrentWorkingDirectorytoSearchPath, "Yes");
+      pythonPluginSearchPaths.setString(PythonPlugin_SearchPathsFields::AddInputFileDirectorytoSearchPath, "Yes");
+      pythonPluginSearchPaths.setString(PythonPlugin_SearchPathsFields::AddepinEnvironmentVariabletoSearchPath, "Yes");
+
+      // Just push the searchPath to it
+      pythonPluginSearchPaths.pushExtensibleGroup({searchPath});
+    } else {
+
+      // Try to locate it before adding it
+      bool newSearchPath = true;
+      for (const IdfExtensibleGroup& eg : it->extensibleGroups()) {
+        if (searchPath == eg.getString(0)) {
+          newSearchPath = false;
+          break;
+        }
+      }
+
+      if (newSearchPath) {
+        it->pushExtensibleGroup({searchPath});
+      }
+    }
+
+    return idfObject;
+  }
+
+}  // namespace energyplus
+
+}  // namespace openstudio
diff --git a/src/energyplus/ForwardTranslator/ForwardTranslatePythonPluginOutputVariable.cpp b/src/energyplus/ForwardTranslator/ForwardTranslatePythonPluginOutputVariable.cpp
new file mode 100644
index 00000000000..758888f21ce
--- /dev/null
+++ b/src/energyplus/ForwardTranslator/ForwardTranslatePythonPluginOutputVariable.cpp
@@ -0,0 +1,90 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include "../ForwardTranslator.hpp"
+#include "../../model/Model.hpp"
+
+#include "../../model/PythonPluginOutputVariable.hpp"
+#include "../../model/PythonPluginVariable.hpp"
+
+#include <utilities/idd/PythonPlugin_OutputVariable_FieldEnums.hxx>
+// #include "../../utilities/idd/IddEnums.hpp"
+#include <utilities/idd/IddEnums.hxx>
+
+using namespace openstudio::model;
+
+namespace openstudio {
+
+namespace energyplus {
+
+  boost::optional<IdfObject> ForwardTranslator::translatePythonPluginOutputVariable(model::PythonPluginOutputVariable& modelObject) {
+    // Instantiate an IdfObject of the class to store the values
+    IdfObject idfObject = createRegisterAndNameIdfObject(openstudio::IddObjectType::PythonPlugin_OutputVariable, modelObject);
+
+    // Python Plugin Variable Name: Required String
+    idfObject.setString(PythonPlugin_OutputVariableFields::PythonPluginVariableName, modelObject.pythonPluginVariable().nameString());
+
+    // Type of Data in Variable: Required String
+    std::string typeofDatainVariable = modelObject.typeofDatainVariable();
+    idfObject.setString(PythonPlugin_OutputVariableFields::TypeofDatainVariable, typeofDatainVariable);
+
+    // Update Frequency: Required String
+    std::string updateFrequency = modelObject.updateFrequency();
+    idfObject.setString(PythonPlugin_OutputVariableFields::UpdateFrequency, updateFrequency);
+
+    // Units: boost::optional<std::string>
+    if (boost::optional<std::string> _units = modelObject.units()) {
+      idfObject.setString(PythonPlugin_OutputVariableFields::Units, _units.get());
+    }
+
+    // Resource Type: boost::optional<std::string>
+    if (boost::optional<std::string> _resourceType = modelObject.resourceType()) {
+      idfObject.setString(PythonPlugin_OutputVariableFields::ResourceType, _resourceType.get());
+    }
+
+    // Group Type: boost::optional<std::string>
+    if (boost::optional<std::string> _groupType = modelObject.groupType()) {
+      idfObject.setString(PythonPlugin_OutputVariableFields::GroupType, _groupType.get());
+    }
+
+    // End-Use Category: boost::optional<std::string>
+    if (boost::optional<std::string> _endUseCategory = modelObject.endUseCategory()) {
+      idfObject.setString(PythonPlugin_OutputVariableFields::EndUseCategory, _endUseCategory.get());
+    }
+
+    // End-Use Subcategory: boost::optional<std::string>
+    if (boost::optional<std::string> _endUseSubcategory = modelObject.endUseSubcategory()) {
+      idfObject.setString(PythonPlugin_OutputVariableFields::EndUseSubcategory, _endUseSubcategory.get());
+    }
+
+    return idfObject;
+  }  // End of translate function
+
+}  // end namespace energyplus
+}  // end namespace openstudio
diff --git a/src/energyplus/ForwardTranslator/ForwardTranslatePythonPluginTrendVariable.cpp b/src/energyplus/ForwardTranslator/ForwardTranslatePythonPluginTrendVariable.cpp
new file mode 100644
index 00000000000..e3de4bab379
--- /dev/null
+++ b/src/energyplus/ForwardTranslator/ForwardTranslatePythonPluginTrendVariable.cpp
@@ -0,0 +1,61 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include "../ForwardTranslator.hpp"
+#include "../../model/Model.hpp"
+
+#include "../../model/PythonPluginTrendVariable.hpp"
+#include "../../model/PythonPluginVariable.hpp"
+
+#include <utilities/idd/PythonPlugin_TrendVariable_FieldEnums.hxx>
+// #include "../../utilities/idd/IddEnums.hpp"
+#include <utilities/idd/IddEnums.hxx>
+
+using namespace openstudio::model;
+
+namespace openstudio {
+
+namespace energyplus {
+
+  boost::optional<IdfObject> ForwardTranslator::translatePythonPluginTrendVariable(model::PythonPluginTrendVariable& modelObject) {
+    // Instantiate an IdfObject of the class to store the values
+    IdfObject idfObject = createRegisterAndNameIdfObject(openstudio::IddObjectType::PythonPlugin_TrendVariable, modelObject);
+
+    // Name of a Python Plugin Variable: Required String
+    idfObject.setString(PythonPlugin_TrendVariableFields::NameofaPythonPluginVariable, modelObject.pythonPluginVariable().nameString());
+
+    // Number of Timesteps to be Logged: Required Integer
+    int numberofTimestepstobeLogged = modelObject.numberofTimestepstobeLogged();
+    idfObject.setInt(PythonPlugin_TrendVariableFields::NumberofTimestepstobeLogged, numberofTimestepstobeLogged);
+
+    return idfObject;
+  }  // End of translate function
+
+}  // end namespace energyplus
+}  // end namespace openstudio
diff --git a/src/energyplus/ForwardTranslator/ForwardTranslatePythonPluginVariable.cpp b/src/energyplus/ForwardTranslator/ForwardTranslatePythonPluginVariable.cpp
new file mode 100644
index 00000000000..4b5854770f8
--- /dev/null
+++ b/src/energyplus/ForwardTranslator/ForwardTranslatePythonPluginVariable.cpp
@@ -0,0 +1,68 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include "../ForwardTranslator.hpp"
+#include "../../model/Model.hpp"
+
+#include "../../model/PythonPluginVariable.hpp"
+
+#include "../../utilities/idf/IdfExtensibleGroup.hpp"
+
+#include <utilities/idd/PythonPlugin_Variables_FieldEnums.hxx>
+// #include "../../utilities/idd/IddEnums.hpp"
+#include <utilities/idd/IddEnums.hxx>
+
+#include <algorithm>
+
+using namespace openstudio::model;
+
+constexpr static auto pythonVariablesName = "Python Plugin Variables";
+
+namespace openstudio {
+
+namespace energyplus {
+
+  boost::optional<IdfObject> ForwardTranslator::translatePythonPluginVariable(model::PythonPluginVariable& modelObject) {
+
+    // Our objects are all translated to a single E+ PythonPlugin:Variables object which is extensible
+
+    auto it = std::find_if(m_idfObjects.begin(), m_idfObjects.end(), [](auto& idfObject) { return idfObject.nameString() == pythonVariablesName; });
+    if (it == m_idfObjects.end()) {
+      auto& pluginVariables = m_idfObjects.emplace_back(openstudio::IddObjectType::PythonPlugin_Variables);
+      pluginVariables.setName(pythonVariablesName);
+      pluginVariables.pushExtensibleGroup({modelObject.nameString()});
+      return pluginVariables;
+    }
+
+    it->pushExtensibleGroup({modelObject.nameString()});
+    return *it;
+  }  // End of translate function
+
+}  // end namespace energyplus
+}  // end namespace openstudio
diff --git a/src/energyplus/Test/PythonPluginInstance_GTest.cpp b/src/energyplus/Test/PythonPluginInstance_GTest.cpp
new file mode 100644
index 00000000000..b3fd0d35cc5
--- /dev/null
+++ b/src/energyplus/Test/PythonPluginInstance_GTest.cpp
@@ -0,0 +1,143 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include <gtest/gtest.h>
+#include "EnergyPlusFixture.hpp"
+
+#include "../ForwardTranslator.hpp"
+#include "../ReverseTranslator.hpp"
+
+#include "../../model/Model.hpp"
+#include "../../model/ExternalFile.hpp"
+#include "../../model/ExternalFile_Impl.hpp"
+#include "../../model/PythonPluginInstance.hpp"
+#include "../../model/PythonPluginInstance_Impl.hpp"
+
+#include "../../utilities/idf/Workspace.hpp"
+#include "../../utilities/idf/IdfObject.hpp"
+#include "../../utilities/idf/WorkspaceObject.hpp"
+#include "../../utilities/idf/IdfExtensibleGroup.hpp"
+
+#include "../../utilities/core/PathHelpers.hpp"
+
+#include <utilities/idd/IddEnums.hxx>
+#include <utilities/idd/PythonPlugin_Instance_FieldEnums.hxx>
+#include <utilities/idd/PythonPlugin_SearchPaths_FieldEnums.hxx>
+#include <utilities/idd/OS_PythonPlugin_Instance_FieldEnums.hxx>
+
+#include <resources.hxx>
+
+#include <sstream>
+
+using namespace openstudio::energyplus;
+using namespace openstudio::model;
+using namespace openstudio;
+
+TEST_F(EnergyPlusFixture, ForwardTranslator_PythonPluginInstance) {
+  Model model;
+
+  openstudio::path p = resourcesPath() / toPath("model/PythonPluginThermochromicWindow.py");
+  ASSERT_TRUE(exists(p));
+
+  // This is to avoid issues where if you change the original OpenStudio/resources/model/PythonPluginThermochromicWindow.py it will throw in
+  // getExternalFile because the file exists and it doesn't have the same content
+  path expectedDestDir;
+  std::vector<path> absoluteFilePaths = model.workflowJSON().absoluteFilePaths();
+  if (absoluteFilePaths.empty()) {
+    expectedDestDir = model.workflowJSON().absoluteRootDir();
+  } else {
+    expectedDestDir = absoluteFilePaths[0];
+  }
+
+  if (exists(expectedDestDir)) {
+    removeDirectory(expectedDestDir);
+  }
+  ASSERT_FALSE(exists(expectedDestDir));
+
+  boost::optional<ExternalFile> externalfile = ExternalFile::getExternalFile(model, openstudio::toString(p));
+  ASSERT_TRUE(externalfile) << "Path doesn't exist: '" << p << "'";
+
+  PythonPluginInstance pythonPluginInstance1(*externalfile, "ZN_1_wall_south_Window_1_Control");
+  pythonPluginInstance1.setName("PythonPlugin1");
+  PythonPluginInstance pythonPluginInstance2(*externalfile, "AnotherDummyEnergyPlusPluginClass");
+  pythonPluginInstance2.setName("PythonPlugin2");
+
+  pythonPluginInstance2.setRunDuringWarmupDays(true);
+
+  // Mimic the case where a user originally had set a correct plugin class name, but then went and modified the python script: we still want to catch
+  // this during FT
+  PythonPluginInstance ppInstanceOriginallyOK(*externalfile, "ZN_1_wall_south_Window_1_Control");
+  ppInstanceOriginallyOK.setName("PythonPlugin_Outdated");
+  EXPECT_TRUE(ppInstanceOriginallyOK.setString(OS_PythonPlugin_InstanceFields::PluginClassName, "SomethingOutdated"));
+
+  ForwardTranslator ft;
+  Workspace workspace = ft.translateModel(model);
+
+  std::string errors;
+  for (const auto& error : ft.errors()) {
+    errors += error.logMessage() + "\n";
+  }
+  EXPECT_EQ(0, ft.errors().size()) << errors;
+
+  std::string warnings;
+  for (const auto& warning : ft.warnings()) {
+    warnings += warning.logMessage() + "\n";
+  }
+  ASSERT_EQ(1, ft.warnings().size()) << warnings;
+  EXPECT_EQ("PythonPluginInstance PythonPlugin_Outdated does not have plugin class name 'SomethingOutdated' contained in referenced external file",
+            ft.warnings().front().logMessage());
+
+  std::vector<WorkspaceObject> instanceObjects = workspace.getObjectsByType(IddObjectType::PythonPlugin_Instance);
+  // The ppInstanceOriginallyOK never got translated
+  ASSERT_EQ(2, instanceObjects.size());
+  std::sort(instanceObjects.begin(), instanceObjects.end(), IdfObjectNameLess());
+  WorkspaceObject woInstance1(instanceObjects[0]);
+  WorkspaceObject woInstance2(instanceObjects[1]);
+
+  ASSERT_EQ(pythonPluginInstance1.name().get(), woInstance1.getString(PythonPlugin_InstanceFields::Name, false).get());
+  EXPECT_EQ("No", woInstance1.getString(PythonPlugin_InstanceFields::RunDuringWarmupDays, false).get());
+  EXPECT_EQ("PythonPluginThermochromicWindow", woInstance1.getString(PythonPlugin_InstanceFields::PythonModuleName, false).get());
+  EXPECT_EQ("ZN_1_wall_south_Window_1_Control", woInstance1.getString(PythonPlugin_InstanceFields::PluginClassName, false).get());
+
+  ASSERT_EQ(pythonPluginInstance2.name().get(), woInstance2.getString(PythonPlugin_InstanceFields::Name, false).get());
+  EXPECT_EQ("Yes", woInstance2.getString(PythonPlugin_InstanceFields::RunDuringWarmupDays, false).get());
+  EXPECT_EQ("PythonPluginThermochromicWindow", woInstance2.getString(PythonPlugin_InstanceFields::PythonModuleName, false).get());
+  EXPECT_EQ("AnotherDummyEnergyPlusPluginClass", woInstance2.getString(PythonPlugin_InstanceFields::PluginClassName, false).get());
+
+  std::vector<WorkspaceObject> searchPathObjects = workspace.getObjectsByType(IddObjectType::PythonPlugin_SearchPaths);
+  ASSERT_EQ(1, searchPathObjects.size());
+  WorkspaceObject woSearchPaths(searchPathObjects[0]);
+  EXPECT_EQ("Python Plugin Search Paths", woSearchPaths.nameString());
+  EXPECT_EQ("Yes", woSearchPaths.getString(PythonPlugin_SearchPathsFields::AddCurrentWorkingDirectorytoSearchPath).get());
+  EXPECT_EQ("Yes", woSearchPaths.getString(PythonPlugin_SearchPathsFields::AddInputFileDirectorytoSearchPath).get());
+  EXPECT_EQ("Yes", woSearchPaths.getString(PythonPlugin_SearchPathsFields::AddepinEnvironmentVariabletoSearchPath).get());
+  ASSERT_EQ(1, woSearchPaths.numExtensibleGroups());
+  EXPECT_EQ(openstudio::toString(externalfile->filePath().parent_path()),
+            woSearchPaths.extensibleGroups()[0].getString(PythonPlugin_SearchPathsExtensibleFields::SearchPath).get());
+}
diff --git a/src/energyplus/Test/PythonPluginOutputVariable_GTest.cpp b/src/energyplus/Test/PythonPluginOutputVariable_GTest.cpp
new file mode 100644
index 00000000000..e976d99ca00
--- /dev/null
+++ b/src/energyplus/Test/PythonPluginOutputVariable_GTest.cpp
@@ -0,0 +1,90 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include <gtest/gtest.h>
+#include "EnergyPlusFixture.hpp"
+
+#include "../ForwardTranslator.hpp"
+#include "../ReverseTranslator.hpp"
+
+#include "../../model/Model.hpp"
+#include "../../model/PythonPluginOutputVariable.hpp"
+#include "../../model/PythonPluginVariable.hpp"
+
+#include "../../utilities/idf/Workspace.hpp"
+#include "../../utilities/idf/IdfObject.hpp"
+#include "../../utilities/idf/WorkspaceObject.hpp"
+#include "../../utilities/idf/IdfExtensibleGroup.hpp"
+
+#include <utilities/idd/PythonPlugin_Variables_FieldEnums.hxx>
+#include <utilities/idd/PythonPlugin_OutputVariable_FieldEnums.hxx>
+
+#include <utilities/idd/IddEnums.hxx>
+
+using namespace openstudio::energyplus;
+using namespace openstudio::model;
+using namespace openstudio;
+
+TEST_F(EnergyPlusFixture, ForwardTranslator_PythonPluginOutputVariable) {
+
+  ForwardTranslator ft;
+
+  // Create a model
+  Model m;
+
+  PythonPluginVariable pyVar(m);
+  pyVar.setName("PythonPluginVariable1");
+
+  PythonPluginOutputVariable pyOutVar(pyVar);
+  pyOutVar.setName("Chiller Electricity Consumption");
+  EXPECT_TRUE(pyOutVar.setTypeofDatainVariable("Metered"));
+  EXPECT_TRUE(pyOutVar.setUpdateFrequency("SystemTimestep"));
+  EXPECT_TRUE(pyOutVar.setUnits("J"));
+  EXPECT_TRUE(pyOutVar.setResourceType("Electricity"));
+  EXPECT_TRUE(pyOutVar.setGroupType("HVAC"));
+  EXPECT_TRUE(pyOutVar.setEndUseCategory("Cooling"));
+  EXPECT_TRUE(pyOutVar.setEndUseSubcategory("Custom air cooled chiller"));
+
+  Workspace w = ft.translateModel(m);
+  EXPECT_EQ(1, w.getObjectsByType(IddObjectType::PythonPlugin_Variables).size());
+  WorkspaceObjectVector idfObjs = w.getObjectsByType(IddObjectType::PythonPlugin_OutputVariable);
+  ASSERT_EQ(1u, idfObjs.size());
+  auto idfObj = idfObjs.front();
+
+  EXPECT_EQ(pyOutVar.nameString(), idfObj.nameString());
+
+  EXPECT_EQ("PythonPluginVariable1", idfObj.getString(PythonPlugin_OutputVariableFields::PythonPluginVariableName).get());
+  EXPECT_EQ("Metered", idfObj.getString(PythonPlugin_OutputVariableFields::TypeofDatainVariable).get());
+  EXPECT_EQ("SystemTimestep", idfObj.getString(PythonPlugin_OutputVariableFields::UpdateFrequency).get());
+  EXPECT_EQ("J", idfObj.getString(PythonPlugin_OutputVariableFields::Units).get());
+  EXPECT_EQ("Electricity", idfObj.getString(PythonPlugin_OutputVariableFields::ResourceType).get());
+  EXPECT_EQ("HVAC", idfObj.getString(PythonPlugin_OutputVariableFields::GroupType).get());
+  EXPECT_EQ("Cooling", idfObj.getString(PythonPlugin_OutputVariableFields::EndUseCategory).get());
+  EXPECT_EQ("Custom air cooled chiller", idfObj.getString(PythonPlugin_OutputVariableFields::EndUseSubcategory).get());
+}
diff --git a/src/energyplus/Test/PythonPluginTrendVariable_GTest.cpp b/src/energyplus/Test/PythonPluginTrendVariable_GTest.cpp
new file mode 100644
index 00000000000..be0afc4eb30
--- /dev/null
+++ b/src/energyplus/Test/PythonPluginTrendVariable_GTest.cpp
@@ -0,0 +1,77 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include <gtest/gtest.h>
+#include "EnergyPlusFixture.hpp"
+
+#include "../ForwardTranslator.hpp"
+#include "../ReverseTranslator.hpp"
+
+#include "../../model/Model.hpp"
+#include "../../model/PythonPluginTrendVariable.hpp"
+#include "../../model/PythonPluginVariable.hpp"
+
+#include "../../utilities/idf/Workspace.hpp"
+#include "../../utilities/idf/IdfObject.hpp"
+#include "../../utilities/idf/WorkspaceObject.hpp"
+#include "../../utilities/idf/IdfExtensibleGroup.hpp"
+
+#include <utilities/idd/PythonPlugin_Variables_FieldEnums.hxx>
+#include <utilities/idd/PythonPlugin_TrendVariable_FieldEnums.hxx>
+
+#include <utilities/idd/IddEnums.hxx>
+
+using namespace openstudio::energyplus;
+using namespace openstudio::model;
+using namespace openstudio;
+
+TEST_F(EnergyPlusFixture, ForwardTranslator_PythonPluginTrendVariable) {
+
+  ForwardTranslator ft;
+
+  // Create a model
+  Model m;
+
+  PythonPluginVariable pyVar(m);
+  pyVar.setName("PythonPluginVariable1");
+
+  PythonPluginTrendVariable pyTrendVar(pyVar);
+  pyTrendVar.setName("PythonPluginTrendVariable");
+  EXPECT_TRUE(pyTrendVar.setNumberofTimestepstobeLogged(10));
+
+  Workspace w = ft.translateModel(m);
+  EXPECT_EQ(1, w.getObjectsByType(IddObjectType::PythonPlugin_Variables).size());
+  WorkspaceObjectVector idfObjs = w.getObjectsByType(IddObjectType::PythonPlugin_TrendVariable);
+  ASSERT_EQ(1u, idfObjs.size());
+  auto idfObj = idfObjs.front();
+
+  EXPECT_EQ(pyTrendVar.nameString(), idfObj.getString(PythonPlugin_TrendVariableFields::Name).get());
+  EXPECT_EQ(pyVar.nameString(), idfObj.getString(PythonPlugin_TrendVariableFields::NameofaPythonPluginVariable).get());
+  EXPECT_EQ(10, idfObj.getInt(PythonPlugin_TrendVariableFields::NumberofTimestepstobeLogged).get());
+}
diff --git a/src/energyplus/Test/PythonPluginVariable_GTest.cpp b/src/energyplus/Test/PythonPluginVariable_GTest.cpp
new file mode 100644
index 00000000000..dda01780eb1
--- /dev/null
+++ b/src/energyplus/Test/PythonPluginVariable_GTest.cpp
@@ -0,0 +1,85 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include <gtest/gtest.h>
+#include "EnergyPlusFixture.hpp"
+
+#include "../ForwardTranslator.hpp"
+#include "../ReverseTranslator.hpp"
+
+#include "../../model/Model.hpp"
+#include "../../model/PythonPluginVariable.hpp"
+
+#include "../../utilities/idf/Workspace.hpp"
+#include "../../utilities/idf/IdfObject.hpp"
+#include "../../utilities/idf/WorkspaceObject.hpp"
+#include "../../utilities/idf/IdfExtensibleGroup.hpp"
+
+#include <utilities/idd/PythonPlugin_Variables_FieldEnums.hxx>
+
+#include <utilities/idd/IddEnums.hxx>
+
+using namespace openstudio::energyplus;
+using namespace openstudio::model;
+using namespace openstudio;
+
+TEST_F(EnergyPlusFixture, ForwardTranslator_PythonPluginVariable) {
+
+  ForwardTranslator ft;
+
+  // Create a model
+  Model m;
+
+  PythonPluginVariable pyVar1(m);
+  pyVar1.setName("PythonPluginVariable1");
+
+  PythonPluginVariable pyVar2(m);
+  pyVar1.setName("PythonPluginVariable2");
+
+  Workspace w = ft.translateModel(m);
+  WorkspaceObjectVector idfObjs = w.getObjectsByType(IddObjectType::PythonPlugin_Variables);
+  ASSERT_EQ(1u, idfObjs.size());
+  auto idfObj = idfObjs.front();
+  auto egs = idfObj.extensibleGroups();
+  ASSERT_EQ(2, egs.size());
+  std::vector<std::string> writtenVarNames;
+  bool pyVar1Found = false;
+  bool pyVar2Found = false;
+
+  for (const auto& eg : egs) {
+    auto name = eg.getString(0).get();
+    if (name == pyVar1.nameString()) {
+      pyVar1Found = true;
+    } else if (name == pyVar2.nameString()) {
+      pyVar2Found = true;
+    }
+  }
+  EXPECT_TRUE(pyVar1Found);
+  EXPECT_TRUE(pyVar2Found);
+}
diff --git a/src/model/CMakeLists.txt b/src/model/CMakeLists.txt
index 699e6d4fc1e..f0e535399a4 100644
--- a/src/model/CMakeLists.txt
+++ b/src/model/CMakeLists.txt
@@ -1157,6 +1157,18 @@ set(${target_name}_src
   PumpVariableSpeed.hpp
   PumpVariableSpeed_Impl.hpp
   PumpVariableSpeed.cpp
+  PythonPluginInstance.hpp
+  PythonPluginInstance_Impl.hpp
+  PythonPluginInstance.cpp
+  PythonPluginVariable.hpp
+  PythonPluginVariable_Impl.hpp
+  PythonPluginVariable.cpp
+  PythonPluginTrendVariable.hpp
+  PythonPluginTrendVariable_Impl.hpp
+  PythonPluginTrendVariable.cpp
+  PythonPluginOutputVariable.hpp
+  PythonPluginOutputVariable_Impl.hpp
+  PythonPluginOutputVariable.cpp
   RadianceParameters.hpp
   RadianceParameters_Impl.hpp
   RadianceParameters.cpp
@@ -2106,6 +2118,10 @@ set(${target_name}_test_src
   test/Plenums_GTest.cpp
   test/PumpConstantSpeed_GTest.cpp
   test/PumpVariableSpeed_GTest.cpp
+  test/PythonPluginInstance_GTest.cpp
+  test/PythonPluginVariable_GTest.cpp
+  test/PythonPluginTrendVariable_GTest.cpp
+  test/PythonPluginOutputVariable_GTest.cpp
   test/RadianceParameters_GTest.cpp
   test/RefractionExtinctionGlazing_GTest.cpp
   test/RefrigerationAirChiller_GTest.cpp
diff --git a/src/model/ConcreteModelObjects.hpp b/src/model/ConcreteModelObjects.hpp
index 3808ce7089d..5680af5803a 100644
--- a/src/model/ConcreteModelObjects.hpp
+++ b/src/model/ConcreteModelObjects.hpp
@@ -375,6 +375,10 @@
 #include "ProgramControl.hpp"
 #include "PumpConstantSpeed.hpp"
 #include "PumpVariableSpeed.hpp"
+#include "PythonPluginInstance.hpp"
+#include "PythonPluginVariable.hpp"
+#include "PythonPluginTrendVariable.hpp"
+#include "PythonPluginOutputVariable.hpp"
 #include "RadianceParameters.hpp"
 #include "RefractionExtinctionGlazing.hpp"
 #include "RefrigerationAirChiller.hpp"
@@ -898,6 +902,10 @@
 #include "ProgramControl_Impl.hpp"
 #include "PumpConstantSpeed_Impl.hpp"
 #include "PumpVariableSpeed_Impl.hpp"
+#include "PythonPluginInstance_Impl.hpp"
+#include "PythonPluginVariable_Impl.hpp"
+#include "PythonPluginTrendVariable_Impl.hpp"
+#include "PythonPluginOutputVariable_Impl.hpp"
 #include "RadianceParameters_Impl.hpp"
 #include "RefractionExtinctionGlazing_Impl.hpp"
 #include "RefrigerationAirChiller_Impl.hpp"
diff --git a/src/model/ExternalFile.cpp b/src/model/ExternalFile.cpp
index c50e483ecd0..a9af885da47 100644
--- a/src/model/ExternalFile.cpp
+++ b/src/model/ExternalFile.cpp
@@ -32,6 +32,7 @@
 
 #include "Model.hpp"
 #include "ScheduleFile.hpp"
+#include "PythonPluginInstance.hpp"
 
 #include <utilities/idd/IddFactory.hxx>
 #include <utilities/idd/IddEnums.hxx>
@@ -44,6 +45,7 @@
 
 #include <unordered_map>
 #include "ScheduleFile_Impl.hpp"
+#include "PythonPluginInstance_Impl.hpp"
 
 namespace openstudio {
 namespace model {
@@ -97,12 +99,21 @@ namespace model {
       }
 
       std::vector<openstudio::IdfObject> tmp;
+
+      // ScheduleFile
       std::vector<ScheduleFile> sfs = scheduleFiles();
       for (auto& scheduleFile : sfs) {
         std::vector<openstudio::IdfObject> tmp2 = scheduleFile.remove();
         tmp.insert(tmp.end(), tmp2.begin(), tmp2.end());
       }
 
+      // PythonPluginInstance
+      std::vector<PythonPluginInstance> ppis = pythonPluginInstances();
+      for (auto& pythonPluginInstance : ppis) {
+        std::vector<openstudio::IdfObject> tmp2 = pythonPluginInstance.remove();
+        tmp.insert(tmp.end(), tmp2.begin(), tmp2.end());
+      }
+
       std::vector<openstudio::IdfObject> idfObjects = ModelObject_Impl::remove();
       idfObjects.insert(idfObjects.end(), tmp.begin(), tmp.end());
 
@@ -176,6 +187,11 @@ namespace model {
       return result;
     }
 
+    std::vector<PythonPluginInstance> ExternalFile_Impl::pythonPluginInstances() const {
+      std::vector<PythonPluginInstance> result = getObject<ExternalFile>().getModelObjectSources<PythonPluginInstance>();
+      return result;
+    }
+
   }  // namespace detail
 
   boost::optional<ExternalFile> ExternalFile::getExternalFile(const Model& model, const std::string& filename) {
@@ -293,6 +309,10 @@ namespace model {
     return getImpl<detail::ExternalFile_Impl>()->scheduleFiles();
   }
 
+  std::vector<PythonPluginInstance> ExternalFile::pythonPluginInstances() const {
+    return getImpl<detail::ExternalFile_Impl>()->pythonPluginInstances();
+  }
+
   /// @cond
   ExternalFile::ExternalFile(std::shared_ptr<detail::ExternalFile_Impl> impl) : ResourceObject(impl) {}
   /// @endcond
diff --git a/src/model/ExternalFile.hpp b/src/model/ExternalFile.hpp
index eec409a3b01..8f6e525a6bd 100644
--- a/src/model/ExternalFile.hpp
+++ b/src/model/ExternalFile.hpp
@@ -40,6 +40,7 @@ namespace openstudio {
 namespace model {
 
   class ScheduleFile;
+  class PythonPluginInstance;
 
   namespace detail {
 
@@ -91,6 +92,8 @@ namespace model {
 
     std::vector<ScheduleFile> scheduleFiles() const;
 
+    std::vector<PythonPluginInstance> pythonPluginInstances() const;
+
     //@}
    protected:
     /// @cond
diff --git a/src/model/ExternalFile_Impl.hpp b/src/model/ExternalFile_Impl.hpp
index 46361295e7e..cfcf53cadac 100644
--- a/src/model/ExternalFile_Impl.hpp
+++ b/src/model/ExternalFile_Impl.hpp
@@ -39,6 +39,7 @@ namespace model {
 
   class ExternalFile;
   class ScheduleFile;
+  class PythonPluginInstance;
 
   namespace detail {
 
@@ -98,6 +99,8 @@ namespace model {
 
       std::vector<ScheduleFile> scheduleFiles() const;
 
+      std::vector<PythonPluginInstance> pythonPluginInstances() const;
+
       //@}
      protected:
       bool setFileName(const std::string& fileName);
diff --git a/src/model/Model.cpp b/src/model/Model.cpp
index 958a7cb8226..67387f1ebb1 100644
--- a/src/model/Model.cpp
+++ b/src/model/Model.cpp
@@ -4072,6 +4072,10 @@ namespace model {
     REGISTER_CONSTRUCTOR(ProgramControl);
     REGISTER_CONSTRUCTOR(PumpConstantSpeed);
     REGISTER_CONSTRUCTOR(PumpVariableSpeed);
+    REGISTER_CONSTRUCTOR(PythonPluginInstance);
+    REGISTER_CONSTRUCTOR(PythonPluginVariable);
+    REGISTER_CONSTRUCTOR(PythonPluginTrendVariable);
+    REGISTER_CONSTRUCTOR(PythonPluginOutputVariable);
     REGISTER_CONSTRUCTOR(RadianceParameters);
     REGISTER_CONSTRUCTOR(RefractionExtinctionGlazing);
     REGISTER_CONSTRUCTOR(RefrigerationAirChiller);
@@ -4603,6 +4607,10 @@ namespace model {
     REGISTER_COPYCONSTRUCTORS(ProgramControl);
     REGISTER_COPYCONSTRUCTORS(PumpConstantSpeed);
     REGISTER_COPYCONSTRUCTORS(PumpVariableSpeed);
+    REGISTER_COPYCONSTRUCTORS(PythonPluginInstance);
+    REGISTER_COPYCONSTRUCTORS(PythonPluginVariable);
+    REGISTER_COPYCONSTRUCTORS(PythonPluginTrendVariable);
+    REGISTER_COPYCONSTRUCTORS(PythonPluginOutputVariable);
     REGISTER_COPYCONSTRUCTORS(RadianceParameters);
     REGISTER_COPYCONSTRUCTORS(RefractionExtinctionGlazing);
     REGISTER_COPYCONSTRUCTORS(RefrigerationAirChiller);
diff --git a/src/model/ModelResources.i b/src/model/ModelResources.i
index b1bf1b68768..d84b508bff6 100644
--- a/src/model/ModelResources.i
+++ b/src/model/ModelResources.i
@@ -91,6 +91,10 @@ MODELOBJECT_TEMPLATES(ScheduleInterval);
 MODELOBJECT_TEMPLATES(ScheduleFixedInterval);
 MODELOBJECT_TEMPLATES(ExternalFile);
 MODELOBJECT_TEMPLATES(ScheduleFile);
+MODELOBJECT_TEMPLATES(PythonPluginInstance);
+MODELOBJECT_TEMPLATES(PythonPluginVariable);
+MODELOBJECT_TEMPLATES(PythonPluginTrendVariable);
+MODELOBJECT_TEMPLATES(PythonPluginOutputVariable);
 MODELOBJECT_TEMPLATES(ScheduleVariableInterval);
 MODELOBJECT_TEMPLATES(ScheduleCompact);
 MODELOBJECT_TEMPLATES(ScheduleConstant);
@@ -184,6 +188,10 @@ MODELOBJECT_TEMPLATES(HeatExchangerDesiccantBalancedFlowPerformanceDataType1);
 
 SWIG_MODELOBJECT(ScheduleInterval, 0);
 SWIG_MODELOBJECT(ScheduleFile, 1);
+SWIG_MODELOBJECT(PythonPluginInstance, 1);
+SWIG_MODELOBJECT(PythonPluginVariable, 1);
+SWIG_MODELOBJECT(PythonPluginTrendVariable, 1);
+SWIG_MODELOBJECT(PythonPluginOutputVariable, 1);
 SWIG_MODELOBJECT(ExternalFile, 1);
 SWIG_MODELOBJECT(ScheduleFixedInterval, 1);
 SWIG_MODELOBJECT(ScheduleVariableInterval, 1);
diff --git a/src/model/PythonPluginInstance.cpp b/src/model/PythonPluginInstance.cpp
new file mode 100644
index 00000000000..450e5fe72c6
--- /dev/null
+++ b/src/model/PythonPluginInstance.cpp
@@ -0,0 +1,242 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include "PythonPluginInstance.hpp"
+#include "PythonPluginInstance_Impl.hpp"
+
+#include "ExternalFile.hpp"
+#include "ExternalFile_Impl.hpp"
+#include "Model.hpp"
+#include "Model_Impl.hpp"
+
+#include <utilities/idd/IddFactory.hxx>
+#include <utilities/idd/IddEnums.hxx>
+#include <utilities/idd/OS_PythonPlugin_Instance_FieldEnums.hxx>
+
+#include "../utilities/units/Unit.hpp"
+#include "../utilities/core/Assert.hpp"
+
+#include <boost/regex.hpp>
+
+#include <algorithm>
+#include <sstream>
+#include <string>
+#include <vector>
+
+namespace openstudio {
+namespace model {
+
+  namespace detail {
+
+    PythonPluginInstance_Impl::PythonPluginInstance_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle)
+      : ResourceObject_Impl(idfObject, model, keepHandle) {
+      OS_ASSERT(idfObject.iddObject().type() == PythonPluginInstance::iddObjectType());
+    }
+
+    PythonPluginInstance_Impl::PythonPluginInstance_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model, bool keepHandle)
+      : ResourceObject_Impl(other, model, keepHandle) {
+      OS_ASSERT(other.iddObject().type() == PythonPluginInstance::iddObjectType());
+    }
+
+    PythonPluginInstance_Impl::PythonPluginInstance_Impl(const PythonPluginInstance_Impl& other, Model_Impl* model, bool keepHandle)
+      : ResourceObject_Impl(other, model, keepHandle) {}
+
+    const std::vector<std::string>& PythonPluginInstance_Impl::outputVariableNames() const {
+      static const std::vector<std::string> result;
+      if (result.empty()) {
+      }
+      return result;
+    }
+
+    IddObjectType PythonPluginInstance_Impl::iddObjectType() const {
+      return PythonPluginInstance::iddObjectType();
+    }
+
+    std::vector<ResourceObject> PythonPluginInstance_Impl::resources() const {
+      return {externalFile()};
+    }
+
+    ExternalFile PythonPluginInstance_Impl::externalFile() const {
+      auto value = getObject<ModelObject>().getModelObjectTarget<ExternalFile>(OS_PythonPlugin_InstanceFields::ExternalFileName);
+      OS_ASSERT(value);
+      return value.get();
+    }
+
+    bool PythonPluginInstance_Impl::runDuringWarmupDays() const {
+      boost::optional<std::string> value = getString(OS_PythonPlugin_InstanceFields::RunDuringWarmupDays, true);
+      OS_ASSERT(value);
+      return openstudio::istringEqual(value.get(), "Yes");
+    }
+
+    bool PythonPluginInstance_Impl::isRunDuringWarmupDaysDefaulted() const {
+      return isEmpty(OS_PythonPlugin_InstanceFields::RunDuringWarmupDays);
+    }
+
+    std::string PythonPluginInstance_Impl::pluginClassName() const {
+      boost::optional<std::string> value = getString(OS_PythonPlugin_InstanceFields::PluginClassName, true);
+      OS_ASSERT(value);
+      return value.get();
+    }
+
+    bool PythonPluginInstance_Impl::setRunDuringWarmupDays(bool runDuringWarmupDays) {
+      bool result = false;
+      if (runDuringWarmupDays) {
+        result = setString(OS_PythonPlugin_InstanceFields::RunDuringWarmupDays, "Yes");
+      } else {
+        result = setString(OS_PythonPlugin_InstanceFields::RunDuringWarmupDays, "No");
+      }
+      OS_ASSERT(result);
+      return result;
+    }
+
+    void PythonPluginInstance_Impl::resetRunDuringWarmupDays() {
+      bool result = setString(OS_PythonPlugin_InstanceFields::RunDuringWarmupDays, "");
+      OS_ASSERT(result);
+    }
+
+    bool PythonPluginInstance_Impl::setPluginClassName(const std::string& pluginClassName) {
+      if (!findPluginClassNameInFile(pluginClassName)) {
+        return false;
+      }
+
+      bool result = setString(OS_PythonPlugin_InstanceFields::PluginClassName, pluginClassName);
+      return result;
+    }
+
+    bool PythonPluginInstance_Impl::findPluginClassNameInFile(const std::string& pluginClassName) const {
+      auto validClassNames = validPluginClassNamesInFile();
+      auto it = std::find(validClassNames.cbegin(), validClassNames.cend(), pluginClassName);
+      if (it == validClassNames.cend()) {
+        std::stringstream ss;
+        ss << "[";
+        bool firstTime = true;
+        for (const auto& className : validClassNames) {
+          if (firstTime) {
+            ss << "\"" << className << "\"";
+            firstTime = false;
+          } else {
+            ss << ", \"" << className << "\"";
+          }
+        }
+        ss << "]";
+        LOG(Warn, pluginClassName << " not found. validClassNames=" << ss.str());
+        return false;
+      }
+
+      return true;
+    }
+
+    std::vector<std::string> PythonPluginInstance_Impl::validPluginClassNamesInFile() const {
+      std::vector<std::string> result;
+
+      // Regexes are expensive, so static duration makes sense
+      static const boost::regex re(R"(^class\s+(\w+)\s*\(\s*EnergyPlusPlugin)");
+      boost::smatch matches;
+
+      auto filePath = openstudio::filesystem::system_complete(externalFile().filePath());
+      openstudio::filesystem::ifstream ifs(filePath);
+      std::string line;
+      while (std::getline(ifs, line)) {
+        if (boost::regex_search(line, matches, re)) {
+          result.emplace_back(matches[1].first, matches[1].second);
+        }
+      }
+      return result;
+    }
+
+  }  // namespace detail
+
+  PythonPluginInstance::PythonPluginInstance(const ExternalFile& externalfile, const std::string& pluginClassName)
+    : ResourceObject(PythonPluginInstance::iddObjectType(), externalfile.model()) {
+    OS_ASSERT(getImpl<detail::PythonPluginInstance_Impl>());
+
+    auto filePath = externalfile.filePath();
+    bool ok = (toString(filePath.extension()) == ".py");
+    if (!ok) {
+      remove();
+      LOG_AND_THROW("External file must have a .py extension, got externalfile='" << filePath << "'.");
+    }
+
+    ok = setPointer(OS_PythonPlugin_InstanceFields::ExternalFileName, externalfile.handle());
+    OS_ASSERT(ok);
+
+    // This must occur after setPointer externalfile
+    ok = setPluginClassName(pluginClassName);
+    if (!ok) {
+      remove();
+      LOG_AND_THROW("Cannot find the Plugin Class Name '" << pluginClassName << "' in external file '" << filePath << "'.");
+    }
+  }
+
+  IddObjectType PythonPluginInstance::iddObjectType() {
+    return {IddObjectType::OS_PythonPlugin_Instance};
+  }
+
+  ExternalFile PythonPluginInstance::externalFile() const {
+    return getImpl<detail::PythonPluginInstance_Impl>()->externalFile();
+  }
+
+  bool PythonPluginInstance::runDuringWarmupDays() const {
+    return getImpl<detail::PythonPluginInstance_Impl>()->runDuringWarmupDays();
+  }
+
+  bool PythonPluginInstance::isRunDuringWarmupDaysDefaulted() const {
+    return getImpl<detail::PythonPluginInstance_Impl>()->isRunDuringWarmupDaysDefaulted();
+  }
+
+  std::string PythonPluginInstance::pluginClassName() const {
+    return getImpl<detail::PythonPluginInstance_Impl>()->pluginClassName();
+  }
+
+  bool PythonPluginInstance::setRunDuringWarmupDays(bool runDuringWarmupDays) {
+    return getImpl<detail::PythonPluginInstance_Impl>()->setRunDuringWarmupDays(runDuringWarmupDays);
+  }
+
+  void PythonPluginInstance::resetRunDuringWarmupDays() {
+    getImpl<detail::PythonPluginInstance_Impl>()->resetRunDuringWarmupDays();
+  }
+
+  bool PythonPluginInstance::setPluginClassName(const std::string& pluginClassName) {
+    return getImpl<detail::PythonPluginInstance_Impl>()->setPluginClassName(pluginClassName);
+  }
+
+  bool PythonPluginInstance::findPluginClassNameInFile(const std::string& pluginClassName) const {
+    return getImpl<detail::PythonPluginInstance_Impl>()->findPluginClassNameInFile(pluginClassName);
+  }
+
+  std::vector<std::string> PythonPluginInstance::validPluginClassNamesInFile() const {
+    return getImpl<detail::PythonPluginInstance_Impl>()->validPluginClassNamesInFile();
+  }
+
+  /// @cond
+  PythonPluginInstance::PythonPluginInstance(std::shared_ptr<detail::PythonPluginInstance_Impl> impl) : ResourceObject(impl) {}
+  /// @endcond
+
+}  // namespace model
+}  // namespace openstudio
diff --git a/src/model/PythonPluginInstance.hpp b/src/model/PythonPluginInstance.hpp
new file mode 100644
index 00000000000..9ab5aedb9cf
--- /dev/null
+++ b/src/model/PythonPluginInstance.hpp
@@ -0,0 +1,115 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#ifndef MODEL_PYTHONPLUGININSTANCE_HPP
+#define MODEL_PYTHONPLUGININSTANCE_HPP
+
+#include "ModelAPI.hpp"
+#include "ResourceObject.hpp"
+
+namespace openstudio {
+
+namespace model {
+
+  class ExternalFile;
+
+  namespace detail {
+
+    class PythonPluginInstance_Impl;
+
+  }  // namespace detail
+
+  /** PythonPluginInstance is a ResourceObject that wraps the OpenStudio IDD object 'OS:PythonPlugin:Instance'. */
+  class MODEL_API PythonPluginInstance : public ResourceObject
+  {
+   public:
+    /** @name Constructors and Destructors */
+    //@{
+
+    PythonPluginInstance(const ExternalFile& externalfile, const std::string& pluginClassName);
+
+    virtual ~PythonPluginInstance() {}
+
+    //@}
+
+    static IddObjectType iddObjectType();
+
+    /** @name Getters */
+    //@{
+
+    ExternalFile externalFile() const;
+
+    bool runDuringWarmupDays() const;
+    bool isRunDuringWarmupDaysDefaulted() const;
+
+    std::string pluginClassName() const;
+
+    //@}
+    /** @name Setters */
+    //@{
+
+    bool setRunDuringWarmupDays(bool runDuringWarmupDays);
+    void resetRunDuringWarmupDays();
+
+    bool setPluginClassName(const std::string& pluginClassName);
+
+    //@}
+    /** @name Other */
+    //@{
+
+    /** Helper that will return whether the pluginClassName can be found in the linked file */
+    bool findPluginClassNameInFile(const std::string& pluginClassName) const;
+
+    std::vector<std::string> validPluginClassNamesInFile() const;
+
+    //@}
+   protected:
+    /// @cond
+    using ImplType = detail::PythonPluginInstance_Impl;
+
+    explicit PythonPluginInstance(std::shared_ptr<detail::PythonPluginInstance_Impl> impl);
+
+    friend class Model;
+    friend class IdfObject;
+    friend class openstudio::detail::IdfObject_Impl;
+    /// @endcond
+   private:
+    REGISTER_LOGGER("openstudio.model.PythonPluginInstance");
+  };
+
+  /** \relates PythonPluginInstance*/
+  using OptionalPythonPluginInstance = boost::optional<PythonPluginInstance>;
+
+  /** \relates PythonPluginInstance*/
+  using PythonPluginInstanceVector = std::vector<PythonPluginInstance>;
+
+}  // namespace model
+}  // namespace openstudio
+
+#endif  // MODEL_PYTHONPLUGININSTANCE_HPP
diff --git a/src/model/PythonPluginInstance_Impl.hpp b/src/model/PythonPluginInstance_Impl.hpp
new file mode 100644
index 00000000000..97839bc62a7
--- /dev/null
+++ b/src/model/PythonPluginInstance_Impl.hpp
@@ -0,0 +1,107 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#ifndef MODEL_PYTHONPLUGININSTANCE_IMPL_HPP
+#define MODEL_PYTHONPLUGININSTANCE_IMPL_HPP
+
+#include "ModelAPI.hpp"
+#include "ResourceObject_Impl.hpp"
+
+namespace openstudio {
+namespace model {
+
+  class ExternalFile;
+
+  namespace detail {
+
+    /** PythonPluginInstance_Impl is a ResourceObject_Impl that is the implementation class for PythonPluginInstance.*/
+    class MODEL_API PythonPluginInstance_Impl : public ResourceObject_Impl
+    {
+     public:
+      /** @name Constructors and Destructors */
+      //@{
+
+      PythonPluginInstance_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle);
+
+      PythonPluginInstance_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model, bool keepHandle);
+
+      PythonPluginInstance_Impl(const PythonPluginInstance_Impl& other, Model_Impl* model, bool keepHandle);
+
+      virtual ~PythonPluginInstance_Impl() {}
+
+      //@}
+      /** @name Virtual Methods */
+      //@{
+
+      virtual const std::vector<std::string>& outputVariableNames() const override;
+
+      virtual IddObjectType iddObjectType() const override;
+
+      virtual std::vector<ResourceObject> resources() const override;
+
+      //@}
+      /** @name Getters */
+      //@{
+
+      ExternalFile externalFile() const;
+
+      bool runDuringWarmupDays() const;
+      bool isRunDuringWarmupDaysDefaulted() const;
+
+      std::string pluginClassName() const;
+
+      //@}
+      /** @name Setters */
+      //@{
+
+      bool setRunDuringWarmupDays(bool runDuringWarmupDays);
+      void resetRunDuringWarmupDays();
+
+      bool setPluginClassName(const std::string& pluginClassName);
+
+      //@}
+      /** @name Other */
+      //@{
+
+      bool findPluginClassNameInFile(const std::string& pluginClassName) const;
+
+      std::vector<std::string> validPluginClassNamesInFile() const;
+
+      //@}
+     protected:
+     private:
+      REGISTER_LOGGER("openstudio.model.PythonPluginInstance");
+    };
+
+  }  // namespace detail
+
+}  // namespace model
+}  // namespace openstudio
+
+#endif  // MODEL_PYTHONPLUGININSTANCE_IMPL_HPP
diff --git a/src/model/PythonPluginOutputVariable.cpp b/src/model/PythonPluginOutputVariable.cpp
new file mode 100644
index 00000000000..c777517af03
--- /dev/null
+++ b/src/model/PythonPluginOutputVariable.cpp
@@ -0,0 +1,314 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include "PythonPluginOutputVariable.hpp"
+#include "PythonPluginOutputVariable_Impl.hpp"
+
+#include "PythonPluginVariable.hpp"
+#include "PythonPluginVariable_Impl.hpp"
+
+#include "Model.hpp"
+
+#include <utilities/idd/IddFactory.hxx>
+#include <utilities/idd/IddEnums.hxx>
+#include <utilities/idd/OS_PythonPlugin_OutputVariable_FieldEnums.hxx>
+
+#include "../utilities/core/Assert.hpp"
+
+namespace openstudio {
+namespace model {
+
+  namespace detail {
+
+    PythonPluginOutputVariable_Impl::PythonPluginOutputVariable_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle)
+      : ModelObject_Impl(idfObject, model, keepHandle) {
+      OS_ASSERT(idfObject.iddObject().type() == PythonPluginOutputVariable::iddObjectType());
+    }
+
+    PythonPluginOutputVariable_Impl::PythonPluginOutputVariable_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model,
+                                                                     bool keepHandle)
+      : ModelObject_Impl(other, model, keepHandle) {
+      OS_ASSERT(other.iddObject().type() == PythonPluginOutputVariable::iddObjectType());
+    }
+
+    PythonPluginOutputVariable_Impl::PythonPluginOutputVariable_Impl(const PythonPluginOutputVariable_Impl& other, Model_Impl* model, bool keepHandle)
+      : ModelObject_Impl(other, model, keepHandle) {}
+
+    const std::vector<std::string>& PythonPluginOutputVariable_Impl::outputVariableNames() const {
+      static std::vector<std::string> result;
+      if (result.empty()) {
+      }
+      return result;
+    }
+
+    IddObjectType PythonPluginOutputVariable_Impl::iddObjectType() const {
+      return PythonPluginOutputVariable::iddObjectType();
+    }
+
+    PythonPluginVariable PythonPluginOutputVariable_Impl::pythonPluginVariable() const {
+      boost::optional<PythonPluginVariable> value = optionalPythonPluginVariable();
+      if (!value) {
+        LOG_AND_THROW(briefDescription() << " does not have an Python Plugin Variable attached.");
+      }
+      return value.get();
+    }
+
+    std::string PythonPluginOutputVariable_Impl::typeofDatainVariable() const {
+      boost::optional<std::string> value = getString(OS_PythonPlugin_OutputVariableFields::TypeofDatainVariable, true);
+      OS_ASSERT(value);
+      return value.get();
+    }
+
+    std::string PythonPluginOutputVariable_Impl::updateFrequency() const {
+      boost::optional<std::string> value = getString(OS_PythonPlugin_OutputVariableFields::UpdateFrequency, true);
+      OS_ASSERT(value);
+      return value.get();
+    }
+
+    boost::optional<std::string> PythonPluginOutputVariable_Impl::units() const {
+      return getString(OS_PythonPlugin_OutputVariableFields::Units, true);
+    }
+
+    boost::optional<std::string> PythonPluginOutputVariable_Impl::resourceType() const {
+      return getString(OS_PythonPlugin_OutputVariableFields::ResourceType, true);
+    }
+
+    boost::optional<std::string> PythonPluginOutputVariable_Impl::groupType() const {
+      return getString(OS_PythonPlugin_OutputVariableFields::GroupType, true);
+    }
+
+    boost::optional<std::string> PythonPluginOutputVariable_Impl::endUseCategory() const {
+      return getString(OS_PythonPlugin_OutputVariableFields::EndUseCategory, true);
+    }
+
+    boost::optional<std::string> PythonPluginOutputVariable_Impl::endUseSubcategory() const {
+      return getString(OS_PythonPlugin_OutputVariableFields::EndUseSubcategory, true);
+    }
+
+    bool PythonPluginOutputVariable_Impl::setPythonPluginVariable(const PythonPluginVariable& pythonPluginVariable) {
+      bool result = setPointer(OS_PythonPlugin_OutputVariableFields::PythonPluginVariableName, pythonPluginVariable.handle());
+      return result;
+    }
+
+    bool PythonPluginOutputVariable_Impl::setTypeofDatainVariable(const std::string& typeofDatainVariable) {
+      bool result = setString(OS_PythonPlugin_OutputVariableFields::TypeofDatainVariable, typeofDatainVariable);
+      return result;
+    }
+
+    bool PythonPluginOutputVariable_Impl::setUpdateFrequency(const std::string& updateFrequency) {
+      bool result = setString(OS_PythonPlugin_OutputVariableFields::UpdateFrequency, updateFrequency);
+      return result;
+    }
+
+    bool PythonPluginOutputVariable_Impl::setUnits(const std::string& units) {
+      bool result = setString(OS_PythonPlugin_OutputVariableFields::Units, units);
+      OS_ASSERT(result);
+      return result;
+    }
+
+    void PythonPluginOutputVariable_Impl::resetUnits() {
+      bool result = setString(OS_PythonPlugin_OutputVariableFields::Units, "");
+      OS_ASSERT(result);
+    }
+
+    bool PythonPluginOutputVariable_Impl::setResourceType(const std::string& resourceType) {
+      bool result = setString(OS_PythonPlugin_OutputVariableFields::ResourceType, resourceType);
+      return result;
+    }
+
+    void PythonPluginOutputVariable_Impl::resetResourceType() {
+      bool result = setString(OS_PythonPlugin_OutputVariableFields::ResourceType, "");
+      OS_ASSERT(result);
+    }
+
+    bool PythonPluginOutputVariable_Impl::setGroupType(const std::string& groupType) {
+      bool result = setString(OS_PythonPlugin_OutputVariableFields::GroupType, groupType);
+      return result;
+    }
+
+    void PythonPluginOutputVariable_Impl::resetGroupType() {
+      bool result = setString(OS_PythonPlugin_OutputVariableFields::GroupType, "");
+      OS_ASSERT(result);
+    }
+
+    bool PythonPluginOutputVariable_Impl::setEndUseCategory(const std::string& endUseCategory) {
+      bool result = setString(OS_PythonPlugin_OutputVariableFields::EndUseCategory, endUseCategory);
+      return result;
+    }
+
+    void PythonPluginOutputVariable_Impl::resetEndUseCategory() {
+      bool result = setString(OS_PythonPlugin_OutputVariableFields::EndUseCategory, "");
+      OS_ASSERT(result);
+    }
+
+    bool PythonPluginOutputVariable_Impl::setEndUseSubcategory(const std::string& endUseSubcategory) {
+      bool result = setString(OS_PythonPlugin_OutputVariableFields::EndUseSubcategory, endUseSubcategory);
+      OS_ASSERT(result);
+      return result;
+    }
+
+    void PythonPluginOutputVariable_Impl::resetEndUseSubcategory() {
+      bool result = setString(OS_PythonPlugin_OutputVariableFields::EndUseSubcategory, "");
+      OS_ASSERT(result);
+    }
+
+    boost::optional<PythonPluginVariable> PythonPluginOutputVariable_Impl::optionalPythonPluginVariable() const {
+      return getObject<ModelObject>().getModelObjectTarget<PythonPluginVariable>(OS_PythonPlugin_OutputVariableFields::PythonPluginVariableName);
+    }
+
+  }  // namespace detail
+
+  PythonPluginOutputVariable::PythonPluginOutputVariable(const PythonPluginVariable& pythonPluginVariable)
+    : ModelObject(PythonPluginOutputVariable::iddObjectType(), pythonPluginVariable.model()) {
+    OS_ASSERT(getImpl<detail::PythonPluginOutputVariable_Impl>());
+
+    bool ok = true;
+    ok = setPythonPluginVariable(pythonPluginVariable);
+    OS_ASSERT(ok);
+    ok = setTypeofDatainVariable("Averaged");
+    OS_ASSERT(ok);
+    ok = setUpdateFrequency("ZoneTimestep");
+    OS_ASSERT(ok);
+  }
+
+  IddObjectType PythonPluginOutputVariable::iddObjectType() {
+    return {IddObjectType::OS_PythonPlugin_OutputVariable};
+  }
+
+  std::vector<std::string> PythonPluginOutputVariable::typeofDatainVariableValues() {
+    return getIddKeyNames(IddFactory::instance().getObject(iddObjectType()).get(), OS_PythonPlugin_OutputVariableFields::TypeofDatainVariable);
+  }
+
+  std::vector<std::string> PythonPluginOutputVariable::updateFrequencyValues() {
+    return getIddKeyNames(IddFactory::instance().getObject(iddObjectType()).get(), OS_PythonPlugin_OutputVariableFields::UpdateFrequency);
+  }
+
+  std::vector<std::string> PythonPluginOutputVariable::resourceTypeValues() {
+    return getIddKeyNames(IddFactory::instance().getObject(iddObjectType()).get(), OS_PythonPlugin_OutputVariableFields::ResourceType);
+  }
+
+  std::vector<std::string> PythonPluginOutputVariable::groupTypeValues() {
+    return getIddKeyNames(IddFactory::instance().getObject(iddObjectType()).get(), OS_PythonPlugin_OutputVariableFields::GroupType);
+  }
+
+  std::vector<std::string> PythonPluginOutputVariable::endUseCategoryValues() {
+    return getIddKeyNames(IddFactory::instance().getObject(iddObjectType()).get(), OS_PythonPlugin_OutputVariableFields::EndUseCategory);
+  }
+
+  PythonPluginVariable PythonPluginOutputVariable::pythonPluginVariable() const {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->pythonPluginVariable();
+  }
+
+  std::string PythonPluginOutputVariable::typeofDatainVariable() const {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->typeofDatainVariable();
+  }
+
+  std::string PythonPluginOutputVariable::updateFrequency() const {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->updateFrequency();
+  }
+
+  boost::optional<std::string> PythonPluginOutputVariable::units() const {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->units();
+  }
+
+  boost::optional<std::string> PythonPluginOutputVariable::resourceType() const {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->resourceType();
+  }
+
+  boost::optional<std::string> PythonPluginOutputVariable::groupType() const {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->groupType();
+  }
+
+  boost::optional<std::string> PythonPluginOutputVariable::endUseCategory() const {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->endUseCategory();
+  }
+
+  boost::optional<std::string> PythonPluginOutputVariable::endUseSubcategory() const {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->endUseSubcategory();
+  }
+
+  bool PythonPluginOutputVariable::setPythonPluginVariable(const PythonPluginVariable& pythonPluginVariable) {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->setPythonPluginVariable(pythonPluginVariable);
+  }
+
+  bool PythonPluginOutputVariable::setTypeofDatainVariable(const std::string& typeofDatainVariable) {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->setTypeofDatainVariable(typeofDatainVariable);
+  }
+
+  bool PythonPluginOutputVariable::setUpdateFrequency(const std::string& updateFrequency) {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->setUpdateFrequency(updateFrequency);
+  }
+
+  bool PythonPluginOutputVariable::setUnits(const std::string& units) {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->setUnits(units);
+  }
+
+  void PythonPluginOutputVariable::resetUnits() {
+    getImpl<detail::PythonPluginOutputVariable_Impl>()->resetUnits();
+  }
+
+  bool PythonPluginOutputVariable::setResourceType(const std::string& resourceType) {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->setResourceType(resourceType);
+  }
+
+  void PythonPluginOutputVariable::resetResourceType() {
+    getImpl<detail::PythonPluginOutputVariable_Impl>()->resetResourceType();
+  }
+
+  bool PythonPluginOutputVariable::setGroupType(const std::string& groupType) {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->setGroupType(groupType);
+  }
+
+  void PythonPluginOutputVariable::resetGroupType() {
+    getImpl<detail::PythonPluginOutputVariable_Impl>()->resetGroupType();
+  }
+
+  bool PythonPluginOutputVariable::setEndUseCategory(const std::string& endUseCategory) {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->setEndUseCategory(endUseCategory);
+  }
+
+  void PythonPluginOutputVariable::resetEndUseCategory() {
+    getImpl<detail::PythonPluginOutputVariable_Impl>()->resetEndUseCategory();
+  }
+
+  bool PythonPluginOutputVariable::setEndUseSubcategory(const std::string& endUseSubcategory) {
+    return getImpl<detail::PythonPluginOutputVariable_Impl>()->setEndUseSubcategory(endUseSubcategory);
+  }
+
+  void PythonPluginOutputVariable::resetEndUseSubcategory() {
+    getImpl<detail::PythonPluginOutputVariable_Impl>()->resetEndUseSubcategory();
+  }
+
+  /// @cond
+  PythonPluginOutputVariable::PythonPluginOutputVariable(std::shared_ptr<detail::PythonPluginOutputVariable_Impl> impl)
+    : ModelObject(std::move(impl)) {}
+  /// @endcond
+
+}  // namespace model
+}  // namespace openstudio
diff --git a/src/model/PythonPluginOutputVariable.hpp b/src/model/PythonPluginOutputVariable.hpp
new file mode 100644
index 00000000000..80c3b389d6f
--- /dev/null
+++ b/src/model/PythonPluginOutputVariable.hpp
@@ -0,0 +1,150 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#ifndef MODEL_PYTHONPLUGINOUTPUTVARIABLE_HPP
+#define MODEL_PYTHONPLUGINOUTPUTVARIABLE_HPP
+
+#include <model/ModelAPI.hpp>
+#include "ModelObject.hpp"
+
+namespace openstudio {
+namespace model {
+
+  class PythonPluginVariable;
+
+  namespace detail {
+
+    class PythonPluginOutputVariable_Impl;
+
+  }  // namespace detail
+
+  /** PythonPluginOutputVariable is a ModelObject that wraps the OpenStudio IDD object 'OS:PythonPlugin:OutputVariable'. */
+  class MODEL_API PythonPluginOutputVariable : public ModelObject
+  {
+   public:
+    /** @name Constructors and Destructors */
+    //@{
+
+    explicit PythonPluginOutputVariable(const PythonPluginVariable& pythonPluginVariable);
+
+    virtual ~PythonPluginOutputVariable() = default;
+
+    //@}
+
+    static IddObjectType iddObjectType();
+
+    static std::vector<std::string> typeofDatainVariableValues();
+
+    static std::vector<std::string> updateFrequencyValues();
+
+    static std::vector<std::string> resourceTypeValues();
+
+    static std::vector<std::string> groupTypeValues();
+
+    static std::vector<std::string> endUseCategoryValues();
+
+    /** @name Getters */
+    //@{
+
+    PythonPluginVariable pythonPluginVariable() const;
+
+    std::string typeofDatainVariable() const;
+
+    std::string updateFrequency() const;
+
+    boost::optional<std::string> units() const;
+
+    boost::optional<std::string> resourceType() const;
+
+    boost::optional<std::string> groupType() const;
+
+    boost::optional<std::string> endUseCategory() const;
+
+    boost::optional<std::string> endUseSubcategory() const;
+
+    //@}
+    /** @name Setters */
+    //@{
+
+    bool setPythonPluginVariable(const PythonPluginVariable& pythonPluginVariable);
+
+    bool setTypeofDatainVariable(const std::string& typeofDatainVariable);
+
+    bool setUpdateFrequency(const std::string& updateFrequency);
+
+    bool setUnits(const std::string& units);
+
+    void resetUnits();
+
+    bool setResourceType(const std::string& resourceType);
+
+    void resetResourceType();
+
+    bool setGroupType(const std::string& groupType);
+
+    void resetGroupType();
+
+    bool setEndUseCategory(const std::string& endUseCategory);
+
+    void resetEndUseCategory();
+
+    bool setEndUseSubcategory(const std::string& endUseSubcategory);
+
+    void resetEndUseSubcategory();
+
+    //@}
+    /** @name Other */
+    //@{
+
+    //@}
+   protected:
+    /// @cond
+    using ImplType = detail::PythonPluginOutputVariable_Impl;
+
+    explicit PythonPluginOutputVariable(std::shared_ptr<detail::PythonPluginOutputVariable_Impl> impl);
+
+    friend class detail::PythonPluginOutputVariable_Impl;
+    friend class Model;
+    friend class IdfObject;
+    friend class openstudio::detail::IdfObject_Impl;
+    /// @endcond
+   private:
+    REGISTER_LOGGER("openstudio.model.PythonPluginOutputVariable");
+  };
+
+  /** \relates PythonPluginOutputVariable*/
+  using OptionalPythonPluginOutputVariable = boost::optional<PythonPluginOutputVariable>;
+
+  /** \relates PythonPluginOutputVariable*/
+  using PythonPluginOutputVariableVector = std::vector<PythonPluginOutputVariable>;
+
+}  // namespace model
+}  // namespace openstudio
+
+#endif  // MODEL_PYTHONPLUGINOUTPUTVARIABLE_HPP
diff --git a/src/model/PythonPluginOutputVariable_Impl.hpp b/src/model/PythonPluginOutputVariable_Impl.hpp
new file mode 100644
index 00000000000..5490f001262
--- /dev/null
+++ b/src/model/PythonPluginOutputVariable_Impl.hpp
@@ -0,0 +1,136 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#ifndef MODEL_PYTHONPLUGINOUTPUTVARIABLE_IMPL_HPP
+#define MODEL_PYTHONPLUGINOUTPUTVARIABLE_IMPL_HPP
+
+#include <model/ModelAPI.hpp>
+#include "ModelObject_Impl.hpp"
+
+namespace openstudio {
+namespace model {
+
+  class PythonPluginVariable;
+
+  namespace detail {
+
+    /** PythonPluginOutputVariable_Impl is a ModelObject_Impl that is the implementation class for PythonPluginOutputVariable.*/
+    class MODEL_API PythonPluginOutputVariable_Impl : public ModelObject_Impl
+    {
+     public:
+      /** @name Constructors and Destructors */
+      //@{
+
+      PythonPluginOutputVariable_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle);
+
+      PythonPluginOutputVariable_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model, bool keepHandle);
+
+      PythonPluginOutputVariable_Impl(const PythonPluginOutputVariable_Impl& other, Model_Impl* model, bool keepHandle);
+
+      virtual ~PythonPluginOutputVariable_Impl() = default;
+
+      //@}
+      /** @name Virtual Methods */
+      //@{
+
+      virtual const std::vector<std::string>& outputVariableNames() const override;
+
+      virtual IddObjectType iddObjectType() const override;
+
+      //@}
+      /** @name Getters */
+      //@{
+
+      PythonPluginVariable pythonPluginVariable() const;
+
+      std::string typeofDatainVariable() const;
+
+      std::string updateFrequency() const;
+
+      boost::optional<std::string> units() const;
+
+      boost::optional<std::string> resourceType() const;
+
+      boost::optional<std::string> groupType() const;
+
+      boost::optional<std::string> endUseCategory() const;
+
+      boost::optional<std::string> endUseSubcategory() const;
+
+      //@}
+      /** @name Setters */
+      //@{
+
+      bool setPythonPluginVariable(const PythonPluginVariable& pythonPluginVariable);
+
+      bool setTypeofDatainVariable(const std::string& typeofDatainVariable);
+
+      bool setUpdateFrequency(const std::string& updateFrequency);
+
+      bool setUnits(const std::string& units);
+
+      void resetUnits();
+
+      bool setResourceType(const std::string& resourceType);
+
+      void resetResourceType();
+
+      bool setGroupType(const std::string& groupType);
+
+      void resetGroupType();
+
+      bool setEndUseCategory(const std::string& endUseCategory);
+
+      void resetEndUseCategory();
+
+      bool setEndUseSubcategory(const std::string& endUseSubcategory);
+
+      void resetEndUseSubcategory();
+
+      //@}
+      /** @name Other */
+      //@{
+
+      //@}
+     protected:
+     private:
+      REGISTER_LOGGER("openstudio.model.PythonPluginOutputVariable");
+
+      // Optional getters for use by methods like children() so can remove() if the constructor fails.
+      // There are other ways for the public versions of these getters to fail--perhaps all required
+      // objects should be returned as boost::optionals
+      boost::optional<PythonPluginVariable> optionalPythonPluginVariable() const;
+    };
+
+  }  // namespace detail
+
+}  // namespace model
+}  // namespace openstudio
+
+#endif  // MODEL_PYTHONPLUGINOUTPUTVARIABLE_IMPL_HPP
diff --git a/src/model/PythonPluginTrendVariable.cpp b/src/model/PythonPluginTrendVariable.cpp
new file mode 100644
index 00000000000..2499dd14d7b
--- /dev/null
+++ b/src/model/PythonPluginTrendVariable.cpp
@@ -0,0 +1,139 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include "PythonPluginTrendVariable.hpp"
+#include "PythonPluginTrendVariable_Impl.hpp"
+
+#include "PythonPluginVariable.hpp"
+#include "PythonPluginVariable_Impl.hpp"
+
+#include "Model.hpp"
+
+#include <utilities/idd/IddEnums.hxx>
+#include <utilities/idd/OS_PythonPlugin_TrendVariable_FieldEnums.hxx>
+
+#include "../utilities/core/Assert.hpp"
+
+namespace openstudio {
+namespace model {
+
+  namespace detail {
+
+    PythonPluginTrendVariable_Impl::PythonPluginTrendVariable_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle)
+      : ModelObject_Impl(idfObject, model, keepHandle) {
+      OS_ASSERT(idfObject.iddObject().type() == PythonPluginTrendVariable::iddObjectType());
+    }
+
+    PythonPluginTrendVariable_Impl::PythonPluginTrendVariable_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model,
+                                                                   bool keepHandle)
+      : ModelObject_Impl(other, model, keepHandle) {
+      OS_ASSERT(other.iddObject().type() == PythonPluginTrendVariable::iddObjectType());
+    }
+
+    PythonPluginTrendVariable_Impl::PythonPluginTrendVariable_Impl(const PythonPluginTrendVariable_Impl& other, Model_Impl* model, bool keepHandle)
+      : ModelObject_Impl(other, model, keepHandle) {}
+
+    const std::vector<std::string>& PythonPluginTrendVariable_Impl::outputVariableNames() const {
+      static std::vector<std::string> result;
+      if (result.empty()) {
+      }
+      return result;
+    }
+
+    IddObjectType PythonPluginTrendVariable_Impl::iddObjectType() const {
+      return PythonPluginTrendVariable::iddObjectType();
+    }
+
+    boost::optional<PythonPluginVariable> PythonPluginTrendVariable_Impl::optionalPythonPluginVariable() const {
+      return getObject<ModelObject>().getModelObjectTarget<PythonPluginVariable>(OS_PythonPlugin_TrendVariableFields::NameofaPythonPluginVariable);
+    }
+
+    PythonPluginVariable PythonPluginTrendVariable_Impl::pythonPluginVariable() const {
+      boost::optional<PythonPluginVariable> value = optionalPythonPluginVariable();
+      if (!value) {
+        LOG_AND_THROW(briefDescription() << " does not have a PythonPluginVariable attached.");
+      }
+      return value.get();
+    }
+
+    int PythonPluginTrendVariable_Impl::numberofTimestepstobeLogged() const {
+      boost::optional<int> value = getInt(OS_PythonPlugin_TrendVariableFields::NumberofTimestepstobeLogged, true);
+      OS_ASSERT(value);
+      return value.get();
+    }
+
+    bool PythonPluginTrendVariable_Impl::setPythonPluginVariable(const PythonPluginVariable& pythonPluginVariable) {
+      bool result = setPointer(OS_PythonPlugin_TrendVariableFields::NameofaPythonPluginVariable, pythonPluginVariable.handle());
+      return result;
+    }
+
+    bool PythonPluginTrendVariable_Impl::setNumberofTimestepstobeLogged(int numberofTimestepstobeLogged) {
+      bool result = setInt(OS_PythonPlugin_TrendVariableFields::NumberofTimestepstobeLogged, numberofTimestepstobeLogged);
+      return result;
+    }
+
+  }  // namespace detail
+
+  PythonPluginTrendVariable::PythonPluginTrendVariable(const PythonPluginVariable& pythonPluginVariable)
+    : ModelObject(PythonPluginTrendVariable::iddObjectType(), pythonPluginVariable.model()) {
+    OS_ASSERT(getImpl<detail::PythonPluginTrendVariable_Impl>());
+
+    bool ok = true;
+    ok = setPythonPluginVariable(pythonPluginVariable);
+    OS_ASSERT(ok);
+    ok = setNumberofTimestepstobeLogged(4);
+    OS_ASSERT(ok);
+  }
+
+  IddObjectType PythonPluginTrendVariable::iddObjectType() {
+    return {IddObjectType::OS_PythonPlugin_TrendVariable};
+  }
+
+  PythonPluginVariable PythonPluginTrendVariable::pythonPluginVariable() const {
+    return getImpl<detail::PythonPluginTrendVariable_Impl>()->pythonPluginVariable();
+  }
+
+  int PythonPluginTrendVariable::numberofTimestepstobeLogged() const {
+    return getImpl<detail::PythonPluginTrendVariable_Impl>()->numberofTimestepstobeLogged();
+  }
+
+  bool PythonPluginTrendVariable::setPythonPluginVariable(const PythonPluginVariable& pythonPluginVariable) {
+    return getImpl<detail::PythonPluginTrendVariable_Impl>()->setPythonPluginVariable(pythonPluginVariable);
+  }
+
+  bool PythonPluginTrendVariable::setNumberofTimestepstobeLogged(int numberofTimestepstobeLogged) {
+    return getImpl<detail::PythonPluginTrendVariable_Impl>()->setNumberofTimestepstobeLogged(numberofTimestepstobeLogged);
+  }
+
+  /// @cond
+  PythonPluginTrendVariable::PythonPluginTrendVariable(std::shared_ptr<detail::PythonPluginTrendVariable_Impl> impl) : ModelObject(std::move(impl)) {}
+  /// @endcond
+
+}  // namespace model
+}  // namespace openstudio
diff --git a/src/model/PythonPluginTrendVariable.hpp b/src/model/PythonPluginTrendVariable.hpp
new file mode 100644
index 00000000000..c79f962053d
--- /dev/null
+++ b/src/model/PythonPluginTrendVariable.hpp
@@ -0,0 +1,108 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#ifndef MODEL_PYTHONPLUGINTRENDVARIABLE_HPP
+#define MODEL_PYTHONPLUGINTRENDVARIABLE_HPP
+
+#include <model/ModelAPI.hpp>
+#include "ModelObject.hpp"
+
+namespace openstudio {
+namespace model {
+
+  class PythonPluginVariable;
+
+  namespace detail {
+
+    class PythonPluginTrendVariable_Impl;
+
+  }  // namespace detail
+
+  /** PythonPluginTrendVariable is a ModelObject that wraps the OpenStudio IDD object 'OS:PythonPlugin:TrendVariable'.
+    * While this object is fully functional, it is not the recommended way to do trending:
+    * just use a storage variable (eg: a list) in the python script */
+  class MODEL_API PythonPluginTrendVariable : public ModelObject
+  {
+   public:
+    /** @name Constructors and Destructors */
+    //@{
+
+    explicit PythonPluginTrendVariable(const PythonPluginVariable& pythonPluginVariable);
+
+    virtual ~PythonPluginTrendVariable() = default;
+
+    //@}
+
+    static IddObjectType iddObjectType();
+
+    /** @name Getters */
+    //@{
+
+    PythonPluginVariable pythonPluginVariable() const;
+
+    int numberofTimestepstobeLogged() const;
+
+    //@}
+    /** @name Setters */
+    //@{
+
+    bool setPythonPluginVariable(const PythonPluginVariable& pythonPluginVariable);
+
+    bool setNumberofTimestepstobeLogged(int numberofTimestepstobeLogged);
+
+    //@}
+    /** @name Other */
+    //@{
+
+    //@}
+   protected:
+    /// @cond
+    using ImplType = detail::PythonPluginTrendVariable_Impl;
+
+    explicit PythonPluginTrendVariable(std::shared_ptr<detail::PythonPluginTrendVariable_Impl> impl);
+
+    friend class detail::PythonPluginTrendVariable_Impl;
+    friend class Model;
+    friend class IdfObject;
+    friend class openstudio::detail::IdfObject_Impl;
+    /// @endcond
+   private:
+    REGISTER_LOGGER("openstudio.model.PythonPluginTrendVariable");
+  };
+
+  /** \relates PythonPluginTrendVariable*/
+  using OptionalPythonPluginTrendVariable = boost::optional<PythonPluginTrendVariable>;
+
+  /** \relates PythonPluginTrendVariable*/
+  using PythonPluginTrendVariableVector = std::vector<PythonPluginTrendVariable>;
+
+}  // namespace model
+}  // namespace openstudio
+
+#endif  // MODEL_PYTHONPLUGINTRENDVARIABLE_HPP
diff --git a/src/model/PythonPluginTrendVariable_Impl.hpp b/src/model/PythonPluginTrendVariable_Impl.hpp
new file mode 100644
index 00000000000..8089bffbf9e
--- /dev/null
+++ b/src/model/PythonPluginTrendVariable_Impl.hpp
@@ -0,0 +1,102 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#ifndef MODEL_PYTHONPLUGINTRENDVARIABLE_IMPL_HPP
+#define MODEL_PYTHONPLUGINTRENDVARIABLE_IMPL_HPP
+
+#include <model/ModelAPI.hpp>
+#include "ModelObject_Impl.hpp"
+
+namespace openstudio {
+namespace model {
+
+  class PythonPluginVariable;
+
+  namespace detail {
+
+    /** PythonPluginTrendVariable_Impl is a ModelObject_Impl that is the implementation class for PythonPluginTrendVariable.*/
+    class MODEL_API PythonPluginTrendVariable_Impl : public ModelObject_Impl
+    {
+     public:
+      /** @name Constructors and Destructors */
+      //@{
+
+      PythonPluginTrendVariable_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle);
+
+      PythonPluginTrendVariable_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model, bool keepHandle);
+
+      PythonPluginTrendVariable_Impl(const PythonPluginTrendVariable_Impl& other, Model_Impl* model, bool keepHandle);
+
+      virtual ~PythonPluginTrendVariable_Impl() {}
+
+      //@}
+      /** @name Virtual Methods */
+      //@{
+
+      virtual const std::vector<std::string>& outputVariableNames() const override;
+
+      virtual IddObjectType iddObjectType() const override;
+
+      //@}
+      /** @name Getters */
+      //@{
+
+      PythonPluginVariable pythonPluginVariable() const;
+
+      int numberofTimestepstobeLogged() const;
+
+      //@}
+      /** @name Setters */
+      //@{
+
+      bool setPythonPluginVariable(const PythonPluginVariable& pythonPluginVariable);
+
+      bool setNumberofTimestepstobeLogged(int numberofTimestepstobeLogged);
+
+      //@}
+      /** @name Other */
+      //@{
+
+      //@}
+     protected:
+     private:
+      REGISTER_LOGGER("openstudio.model.PythonPluginTrendVariable");
+
+      // Optional getters for use by methods like children() so can remove() if the constructor fails.
+      // There are other ways for the public versions of these getters to fail--perhaps all required
+      // objects should be returned as boost::optionals
+      boost::optional<PythonPluginVariable> optionalPythonPluginVariable() const;
+    };
+
+  }  // namespace detail
+
+}  // namespace model
+}  // namespace openstudio
+
+#endif  // MODEL_PYTHONPLUGINTRENDVARIABLE_IMPL_HPP
diff --git a/src/model/PythonPluginVariable.cpp b/src/model/PythonPluginVariable.cpp
new file mode 100644
index 00000000000..0aacac09450
--- /dev/null
+++ b/src/model/PythonPluginVariable.cpp
@@ -0,0 +1,136 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include "PythonPluginVariable.hpp"
+#include "PythonPluginVariable_Impl.hpp"
+
+#include "PythonPluginTrendVariable.hpp"
+#include "PythonPluginTrendVariable_Impl.hpp"
+#include "PythonPluginOutputVariable.hpp"
+#include "PythonPluginOutputVariable_Impl.hpp"
+
+#include "Model.hpp"
+
+#include <utilities/idd/IddEnums.hxx>
+#include <utilities/idd/OS_PythonPlugin_Variable_FieldEnums.hxx>
+
+#include "../utilities/core/Assert.hpp"
+
+namespace openstudio {
+namespace model {
+
+  namespace detail {
+
+    PythonPluginVariable_Impl::PythonPluginVariable_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle)
+      : ModelObject_Impl(idfObject, model, keepHandle) {
+      OS_ASSERT(idfObject.iddObject().type() == PythonPluginVariable::iddObjectType());
+    }
+
+    PythonPluginVariable_Impl::PythonPluginVariable_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model, bool keepHandle)
+      : ModelObject_Impl(other, model, keepHandle) {
+      OS_ASSERT(other.iddObject().type() == PythonPluginVariable::iddObjectType());
+    }
+
+    PythonPluginVariable_Impl::PythonPluginVariable_Impl(const PythonPluginVariable_Impl& other, Model_Impl* model, bool keepHandle)
+      : ModelObject_Impl(other, model, keepHandle) {}
+
+    const std::vector<std::string>& PythonPluginVariable_Impl::outputVariableNames() const {
+      static std::vector<std::string> result;
+      if (result.empty()) {
+      }
+      return result;
+    }
+
+    IddObjectType PythonPluginVariable_Impl::iddObjectType() const {
+      return PythonPluginVariable::iddObjectType();
+    }
+
+    std::vector<PythonPluginTrendVariable> PythonPluginVariable_Impl::pythonPluginTrendVariables() const {
+      return getObject<ModelObject>().getModelObjectSources<PythonPluginTrendVariable>(PythonPluginTrendVariable::iddObjectType());
+    }
+
+    std::vector<PythonPluginOutputVariable> PythonPluginVariable_Impl::pythonPluginOutputVariables() const {
+      return getObject<ModelObject>().getModelObjectSources<PythonPluginOutputVariable>(PythonPluginOutputVariable::iddObjectType());
+    }
+
+    ModelObject PythonPluginVariable_Impl::clone(Model model) const {
+      auto newVar = ModelObject_Impl::clone(model).cast<PythonPluginVariable>();
+
+      for (const auto& child : pythonPluginTrendVariables()) {
+        child.clone(model).cast<PythonPluginTrendVariable>().setPythonPluginVariable(newVar);
+      }
+
+      for (const auto& child : pythonPluginOutputVariables()) {
+        child.clone(model).cast<PythonPluginOutputVariable>().setPythonPluginVariable(newVar);
+      }
+
+      return newVar;
+    }
+
+    std::vector<IdfObject> PythonPluginVariable_Impl::remove() {
+      std::vector<IdfObject> result;
+      auto trends = pythonPluginTrendVariables();
+      auto outputs = pythonPluginOutputVariables();
+      result.reserve(trends.size() + outputs.size() + 1);
+
+      for (auto& child : trends) {
+        result.emplace_back(child.remove().front());
+      }
+
+      for (auto& child : outputs) {
+        result.emplace_back(child.remove().front());
+      }
+
+      result.emplace_back(ModelObject_Impl::remove().front());
+      return result;
+    }
+
+  }  // namespace detail
+
+  PythonPluginVariable::PythonPluginVariable(const Model& model) : ModelObject(PythonPluginVariable::iddObjectType(), model) {
+    OS_ASSERT(getImpl<detail::PythonPluginVariable_Impl>());
+  }
+
+  IddObjectType PythonPluginVariable::iddObjectType() {
+    return {IddObjectType::OS_PythonPlugin_Variable};
+  }
+
+  std::vector<PythonPluginTrendVariable> PythonPluginVariable::pythonPluginTrendVariables() const {
+    return getImpl<detail::PythonPluginVariable_Impl>()->pythonPluginTrendVariables();
+  }
+  std::vector<PythonPluginOutputVariable> PythonPluginVariable::pythonPluginOutputVariables() const {
+    return getImpl<detail::PythonPluginVariable_Impl>()->pythonPluginOutputVariables();
+  }
+
+  /// @cond
+  PythonPluginVariable::PythonPluginVariable(std::shared_ptr<detail::PythonPluginVariable_Impl> impl) : ModelObject(std::move(impl)) {}
+  /// @endcond
+
+}  // namespace model
+}  // namespace openstudio
diff --git a/src/model/PythonPluginVariable.hpp b/src/model/PythonPluginVariable.hpp
new file mode 100644
index 00000000000..a88e8d00b1f
--- /dev/null
+++ b/src/model/PythonPluginVariable.hpp
@@ -0,0 +1,102 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#ifndef MODEL_PYTHONPLUGINVARIABLE_HPP
+#define MODEL_PYTHONPLUGINVARIABLE_HPP
+
+#include <model/ModelAPI.hpp>
+#include "ModelObject.hpp"
+
+namespace openstudio {
+namespace model {
+
+  class PythonPluginTrendVariable;
+  class PythonPluginOutputVariable;
+
+  namespace detail {
+
+    class PythonPluginVariable_Impl;
+
+  }  // namespace detail
+
+  /** PythonPluginVariable is a ModelObject that wraps the OpenStudio IDD object 'OS:PythonPlugin:Variable'. */
+  class MODEL_API PythonPluginVariable : public ModelObject
+  {
+   public:
+    /** @name Constructors and Destructors */
+    //@{
+
+    explicit PythonPluginVariable(const Model& model);
+
+    virtual ~PythonPluginVariable() = default;
+
+    //@}
+
+    static IddObjectType iddObjectType();
+
+    /** @name Getters */
+    //@{
+
+    //@}
+    /** @name Setters */
+    //@{
+
+    //@}
+    /** @name Other */
+    //@{
+
+    std::vector<PythonPluginTrendVariable> pythonPluginTrendVariables() const;
+    std::vector<PythonPluginOutputVariable> pythonPluginOutputVariables() const;
+
+    //@}
+   protected:
+    /// @cond
+    using ImplType = detail::PythonPluginVariable_Impl;
+
+    explicit PythonPluginVariable(std::shared_ptr<detail::PythonPluginVariable_Impl> impl);
+
+    friend class detail::PythonPluginVariable_Impl;
+    friend class Model;
+    friend class IdfObject;
+    friend class openstudio::detail::IdfObject_Impl;
+    /// @endcond
+   private:
+    REGISTER_LOGGER("openstudio.model.PythonPluginVariable");
+  };
+
+  /** \relates PythonPluginVariable*/
+  using OptionalPythonPluginVariable = boost::optional<PythonPluginVariable>;
+
+  /** \relates PythonPluginVariable*/
+  using PythonPluginVariableVector = std::vector<PythonPluginVariable>;
+
+}  // namespace model
+}  // namespace openstudio
+
+#endif  // MODEL_PYTHONPLUGINVARIABLE_HPP
diff --git a/src/model/PythonPluginVariable_Impl.hpp b/src/model/PythonPluginVariable_Impl.hpp
new file mode 100644
index 00000000000..18fc08e4310
--- /dev/null
+++ b/src/model/PythonPluginVariable_Impl.hpp
@@ -0,0 +1,99 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#ifndef MODEL_PYTHONPLUGINVARIABLE_IMPL_HPP
+#define MODEL_PYTHONPLUGINVARIABLE_IMPL_HPP
+
+#include <model/ModelAPI.hpp>
+#include "ModelObject_Impl.hpp"
+
+namespace openstudio {
+namespace model {
+
+  class PythonPluginTrendVariable;
+  class PythonPluginOutputVariable;
+
+  namespace detail {
+
+    /** PythonPluginVariable_Impl is a ModelObject_Impl that is the implementation class for PythonPluginVariable.*/
+    class MODEL_API PythonPluginVariable_Impl : public ModelObject_Impl
+    {
+     public:
+      /** @name Constructors and Destructors */
+      //@{
+
+      PythonPluginVariable_Impl(const IdfObject& idfObject, Model_Impl* model, bool keepHandle);
+
+      PythonPluginVariable_Impl(const openstudio::detail::WorkspaceObject_Impl& other, Model_Impl* model, bool keepHandle);
+
+      PythonPluginVariable_Impl(const PythonPluginVariable_Impl& other, Model_Impl* model, bool keepHandle);
+
+      virtual ~PythonPluginVariable_Impl() = default;
+
+      /** Removes all PythonPluginTrendVariable and PythonPluginOutputVariable attached to it */
+      virtual std::vector<IdfObject> remove() override;
+
+      /** Also clones all PythonPluginTrendVariable and PythonPluginOutputVariable attached to it */
+      virtual ModelObject clone(Model model) const override;
+
+      //@}
+      /** @name Virtual Methods */
+      //@{
+
+      virtual const std::vector<std::string>& outputVariableNames() const override;
+
+      virtual IddObjectType iddObjectType() const override;
+
+      //@}
+      /** @name Getters */
+      //@{
+
+      //@}
+      /** @name Setters */
+      //@{
+
+      //@}
+      /** @name Other */
+      //@{
+
+      std::vector<PythonPluginTrendVariable> pythonPluginTrendVariables() const;
+      std::vector<PythonPluginOutputVariable> pythonPluginOutputVariables() const;
+
+      //@}
+     protected:
+     private:
+      REGISTER_LOGGER("openstudio.model.PythonPluginVariable");
+    };
+
+  }  // namespace detail
+
+}  // namespace model
+}  // namespace openstudio
+
+#endif  // MODEL_PYTHONPLUGINVARIABLE_IMPL_HPP


Test patch:
diff --git a/src/model/test/PythonPluginInstance_GTest.cpp b/src/model/test/PythonPluginInstance_GTest.cpp
new file mode 100644
index 00000000000..798b70f0116
--- /dev/null
+++ b/src/model/test/PythonPluginInstance_GTest.cpp
@@ -0,0 +1,153 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include <gtest/gtest.h>
+
+#include "ModelFixture.hpp"
+#include "../ExternalFile.hpp"
+#include "../ExternalFile_Impl.hpp"
+#include "../PythonPluginInstance.hpp"
+#include "../PythonPluginInstance_Impl.hpp"
+
+#include "../../utilities/core/PathHelpers.hpp"
+
+using namespace openstudio::model;
+using namespace openstudio;
+
+TEST_F(ModelFixture, PythonPluginInstance) {
+  Model model;
+  EXPECT_EQ(0u, model.getConcreteModelObjects<ExternalFile>().size());
+  EXPECT_EQ(0u, model.getConcreteModelObjects<PythonPluginInstance>().size());
+
+  path p = resourcesPath() / toPath("model/PythonPluginThermochromicWindow.py");
+  EXPECT_TRUE(exists(p));
+
+  path expectedDestDir;
+  std::vector<path> absoluteFilePaths = model.workflowJSON().absoluteFilePaths();
+  if (absoluteFilePaths.empty()) {
+    expectedDestDir = model.workflowJSON().absoluteRootDir();
+  } else {
+    expectedDestDir = absoluteFilePaths[0];
+  }
+
+  if (exists(expectedDestDir)) {
+    removeDirectory(expectedDestDir);
+  }
+  ASSERT_FALSE(exists(expectedDestDir));
+
+  boost::optional<ExternalFile> externalfile = ExternalFile::getExternalFile(model, openstudio::toString(p));
+  ASSERT_TRUE(externalfile);
+  EXPECT_EQ(1u, model.getConcreteModelObjects<ExternalFile>().size());
+  EXPECT_EQ(0u, externalfile->pythonPluginInstances().size());
+  EXPECT_EQ(openstudio::toString(p.filename()), externalfile->fileName());
+  EXPECT_TRUE(equivalent(expectedDestDir / externalfile->fileName(), externalfile->filePath()));
+  EXPECT_TRUE(exists(externalfile->filePath()));
+  EXPECT_NE(p, externalfile->filePath());
+
+  PythonPluginInstance plugin(*externalfile, "ZN_1_wall_south_Window_1_Control");
+  EXPECT_EQ(1u, model.getConcreteModelObjects<PythonPluginInstance>().size());
+  EXPECT_EQ(1u, externalfile->pythonPluginInstances().size());
+  EXPECT_EQ(externalfile->handle(), plugin.externalFile().handle());
+  EXPECT_EQ("ZN_1_wall_south_Window_1_Control", plugin.pluginClassName());
+
+  EXPECT_FALSE(plugin.runDuringWarmupDays());
+  EXPECT_TRUE(plugin.isRunDuringWarmupDaysDefaulted());
+  plugin.setRunDuringWarmupDays(true);
+  EXPECT_TRUE(plugin.runDuringWarmupDays());
+  EXPECT_FALSE(plugin.isRunDuringWarmupDaysDefaulted());
+  plugin.resetRunDuringWarmupDays();
+  EXPECT_FALSE(plugin.runDuringWarmupDays());
+  EXPECT_TRUE(plugin.isRunDuringWarmupDaysDefaulted());
+
+  PythonPluginInstance plugin2(*externalfile, "ZN_1_wall_south_Window_1_Control");
+  EXPECT_EQ(2u, model.getConcreteModelObjects<PythonPluginInstance>().size());
+  EXPECT_EQ(2u, externalfile->pythonPluginInstances().size());
+  EXPECT_EQ(externalfile->handle(), plugin2.externalFile().handle());
+
+  plugin.remove();
+  EXPECT_EQ(1u, model.getConcreteModelObjects<ExternalFile>().size());
+  EXPECT_EQ(1u, model.getConcreteModelObjects<PythonPluginInstance>().size());
+  EXPECT_EQ(1u, externalfile->pythonPluginInstances().size());
+
+  path filePath = externalfile->filePath();
+  EXPECT_TRUE(exists(p));
+  EXPECT_TRUE(exists(filePath));
+
+  externalfile->remove();
+  EXPECT_EQ(0u, model.getConcreteModelObjects<ExternalFile>().size());
+  EXPECT_EQ(0u, model.getConcreteModelObjects<PythonPluginInstance>().size());
+  EXPECT_EQ(0u, externalfile->pythonPluginInstances().size());
+
+  EXPECT_TRUE(exists(p));
+  EXPECT_FALSE(exists(filePath));
+}
+
+TEST_F(ModelFixture, PythonPluginInstance_NotPYFile) {
+  Model model;
+
+  path p = resourcesPath() / toPath("model/7-7_Windows_Complete.osm");
+  EXPECT_TRUE(exists(p));
+
+  boost::optional<ExternalFile> externalfile = ExternalFile::getExternalFile(model, openstudio::toString(p));
+  ASSERT_TRUE(externalfile);
+
+  ASSERT_THROW(PythonPluginInstance plugin(*externalfile, "ZN_1_wall_south_Window_1_Control"), openstudio::Exception);
+}
+
+TEST_F(ModelFixture, PythonPluginInstance_ClassNameValidation) {
+  Model model;
+
+  path p = resourcesPath() / toPath("model/PythonPluginThermochromicWindow.py");
+  EXPECT_TRUE(exists(p));
+
+  boost::optional<ExternalFile> externalfile = ExternalFile::getExternalFile(model, openstudio::toString(p));
+  ASSERT_TRUE(externalfile);
+
+  // ctor
+  EXPECT_ANY_THROW(PythonPluginInstance(*externalfile, "ZN_1_wall_east_Window_1_Control"));
+
+  // Start with a valid one
+  PythonPluginInstance plugin(*externalfile, "ZN_1_wall_south_Window_1_Control");
+  // Then try to set an invalid one
+  EXPECT_FALSE(plugin.setPluginClassName("ZN_1_wall_west_Window_1_Control"));
+  EXPECT_EQ("ZN_1_wall_south_Window_1_Control", plugin.pluginClassName());
+
+  // Try another valid one
+  EXPECT_TRUE(plugin.setPluginClassName("AnotherDummyEnergyPlusPluginClass"));
+  EXPECT_EQ("AnotherDummyEnergyPlusPluginClass", plugin.pluginClassName());
+
+  // Try with a support class that doesn't derive EnergyPlusPlugin
+  EXPECT_FALSE(plugin.setPluginClassName("MySupportClassThatDoesntDeriveEnergyPlusPlugin"));
+  EXPECT_EQ("AnotherDummyEnergyPlusPluginClass", plugin.pluginClassName());
+
+  auto validClassNames = plugin.validPluginClassNamesInFile();
+  ASSERT_EQ(2, validClassNames.size());
+  EXPECT_EQ("ZN_1_wall_south_Window_1_Control", validClassNames[0]);
+  EXPECT_EQ("AnotherDummyEnergyPlusPluginClass", validClassNames[1]);
+}
diff --git a/src/model/test/PythonPluginOutputVariable_GTest.cpp b/src/model/test/PythonPluginOutputVariable_GTest.cpp
new file mode 100644
index 00000000000..e72828a13ae
--- /dev/null
+++ b/src/model/test/PythonPluginOutputVariable_GTest.cpp
@@ -0,0 +1,103 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include "ModelFixture.hpp"
+
+#include "../PythonPluginOutputVariable.hpp"
+#include "../PythonPluginVariable.hpp"
+
+using namespace openstudio;
+using namespace openstudio::model;
+
+TEST_F(ModelFixture, PythonPluginOutputVariable_GettersSetters) {
+  Model m;
+
+  PythonPluginVariable pyVar(m);
+  PythonPluginOutputVariable pythonPluginOutputVariable(pyVar);
+  EXPECT_EQ(pyVar, pythonPluginOutputVariable.pythonPluginVariable());
+  EXPECT_EQ(1, pyVar.pythonPluginOutputVariables().size());
+
+  pythonPluginOutputVariable.setName("My PythonPluginOutputVariable");
+
+  // Python Plugin Variable Name: Required Object
+  PythonPluginVariable pyVar2(m);
+  EXPECT_TRUE(pythonPluginOutputVariable.setPythonPluginVariable(pyVar2));
+  EXPECT_EQ(pyVar2, pythonPluginOutputVariable.pythonPluginVariable());
+
+  // Type of Data in Variable: Required String
+  EXPECT_TRUE(pythonPluginOutputVariable.setTypeofDatainVariable("Averaged"));
+  EXPECT_EQ("Averaged", pythonPluginOutputVariable.typeofDatainVariable());
+  // Bad Value
+  EXPECT_FALSE(pythonPluginOutputVariable.setTypeofDatainVariable("BADENUM"));
+  EXPECT_EQ("Averaged", pythonPluginOutputVariable.typeofDatainVariable());
+
+  // Update Frequency: Required String
+  EXPECT_TRUE(pythonPluginOutputVariable.setUpdateFrequency("ZoneTimestep"));
+  EXPECT_EQ("ZoneTimestep", pythonPluginOutputVariable.updateFrequency());
+  // Bad Value
+  EXPECT_FALSE(pythonPluginOutputVariable.setUpdateFrequency("BADENUM"));
+  EXPECT_EQ("ZoneTimestep", pythonPluginOutputVariable.updateFrequency());
+
+  // Units: Optional String
+  EXPECT_TRUE(pythonPluginOutputVariable.setUnits("C"));
+  ASSERT_TRUE(pythonPluginOutputVariable.units());
+  EXPECT_EQ("C", pythonPluginOutputVariable.units().get());
+
+  // Resource Type: Optional String
+  EXPECT_TRUE(pythonPluginOutputVariable.setResourceType("Electricity"));
+  ASSERT_TRUE(pythonPluginOutputVariable.resourceType());
+  EXPECT_EQ("Electricity", pythonPluginOutputVariable.resourceType().get());
+  // Bad Value
+  EXPECT_FALSE(pythonPluginOutputVariable.setResourceType("BADENUM"));
+  ASSERT_TRUE(pythonPluginOutputVariable.resourceType());
+  EXPECT_EQ("Electricity", pythonPluginOutputVariable.resourceType().get());
+
+  // Group Type: Optional String
+  EXPECT_TRUE(pythonPluginOutputVariable.setGroupType("Building"));
+  ASSERT_TRUE(pythonPluginOutputVariable.groupType());
+  EXPECT_EQ("Building", pythonPluginOutputVariable.groupType().get());
+  // Bad Value
+  EXPECT_FALSE(pythonPluginOutputVariable.setGroupType("BADENUM"));
+  ASSERT_TRUE(pythonPluginOutputVariable.groupType());
+  EXPECT_EQ("Building", pythonPluginOutputVariable.groupType().get());
+
+  // End-Use Category: Optional String
+  EXPECT_TRUE(pythonPluginOutputVariable.setEndUseCategory("Heating"));
+  ASSERT_TRUE(pythonPluginOutputVariable.endUseCategory());
+  EXPECT_EQ("Heating", pythonPluginOutputVariable.endUseCategory().get());
+  // Bad Value
+  EXPECT_FALSE(pythonPluginOutputVariable.setEndUseCategory("BADENUM"));
+  ASSERT_TRUE(pythonPluginOutputVariable.endUseCategory());
+  EXPECT_EQ("Heating", pythonPluginOutputVariable.endUseCategory().get());
+
+  // End-Use Subcategory: Optional String
+  EXPECT_TRUE(pythonPluginOutputVariable.setEndUseSubcategory("Space Heating"));
+  ASSERT_TRUE(pythonPluginOutputVariable.endUseSubcategory());
+  EXPECT_EQ("Space Heating", pythonPluginOutputVariable.endUseSubcategory().get());
+}
diff --git a/src/model/test/PythonPluginTrendVariable_GTest.cpp b/src/model/test/PythonPluginTrendVariable_GTest.cpp
new file mode 100644
index 00000000000..dab0362e0e7
--- /dev/null
+++ b/src/model/test/PythonPluginTrendVariable_GTest.cpp
@@ -0,0 +1,59 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include "ModelFixture.hpp"
+
+#include "../PythonPluginTrendVariable.hpp"
+#include "../PythonPluginVariable.hpp"
+
+using namespace openstudio;
+using namespace openstudio::model;
+
+TEST_F(ModelFixture, PythonPluginTrendVariable_GettersSetters) {
+  Model m;
+
+  PythonPluginVariable pyVar(m);
+
+  PythonPluginTrendVariable pythonPluginTrendVariable(pyVar);
+  EXPECT_EQ(pyVar, pythonPluginTrendVariable.pythonPluginVariable());
+
+  pythonPluginTrendVariable.setName("My PythonPluginTrendVariable");
+
+  // Name of a Python Plugin Variable: Required Object
+  PythonPluginVariable pyVar2(m);
+  EXPECT_TRUE(pythonPluginTrendVariable.setPythonPluginVariable(pyVar2));
+  EXPECT_EQ(pyVar2, pythonPluginTrendVariable.pythonPluginVariable());
+
+  // Number of Timesteps to be Logged: Required Integer
+  EXPECT_TRUE(pythonPluginTrendVariable.setNumberofTimestepstobeLogged(5));
+  EXPECT_EQ(5, pythonPluginTrendVariable.numberofTimestepstobeLogged());
+  // Bad Value
+  EXPECT_FALSE(pythonPluginTrendVariable.setNumberofTimestepstobeLogged(-9));
+  EXPECT_EQ(5, pythonPluginTrendVariable.numberofTimestepstobeLogged());
+}
diff --git a/src/model/test/PythonPluginVariable_GTest.cpp b/src/model/test/PythonPluginVariable_GTest.cpp
new file mode 100644
index 00000000000..ff3938659f3
--- /dev/null
+++ b/src/model/test/PythonPluginVariable_GTest.cpp
@@ -0,0 +1,129 @@
+/***********************************************************************************************************************
+*  OpenStudio(R), Copyright (c) 2008-2022, Alliance for Sustainable Energy, LLC, and other contributors. All rights reserved.
+*
+*  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the
+*  following conditions are met:
+*
+*  (1) Redistributions of source code must retain the above copyright notice, this list of conditions and the following
+*  disclaimer.
+*
+*  (2) Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following
+*  disclaimer in the documentation and/or other materials provided with the distribution.
+*
+*  (3) Neither the name of the copyright holder nor the names of any contributors may be used to endorse or promote products
+*  derived from this software without specific prior written permission from the respective party.
+*
+*  (4) Other than as required in clauses (1) and (2), distributions in any form of modifications or other derivative works
+*  may not use the "OpenStudio" trademark, "OS", "os", or any other confusingly similar designation without specific prior
+*  written permission from Alliance for Sustainable Energy, LLC.
+*
+*  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
+*  INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+*  DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE UNITED STATES GOVERNMENT, OR THE UNITED
+*  STATES DEPARTMENT OF ENERGY, NOR ANY OF THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
+*  EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF
+*  USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
+*  STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
+*  ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+***********************************************************************************************************************/
+
+#include "ModelFixture.hpp"
+
+#include "../PythonPluginVariable.hpp"
+#include "../PythonPluginVariable_Impl.hpp"
+
+#include "../PythonPluginTrendVariable.hpp"
+#include "../PythonPluginTrendVariable_Impl.hpp"
+#include "../PythonPluginOutputVariable.hpp"
+#include "../PythonPluginOutputVariable_Impl.hpp"
+
+using namespace openstudio;
+using namespace openstudio::model;
+
+TEST_F(ModelFixture, PythonPluginVariable) {
+  Model m;
+  PythonPluginVariable var(m);
+  var.setName("My PythonPluginVariable");
+
+  EXPECT_EQ(0, var.pythonPluginTrendVariables().size());
+  EXPECT_EQ(0, var.pythonPluginOutputVariables().size());
+
+  PythonPluginTrendVariable pyTrendVar(var);
+  EXPECT_EQ(var, pyTrendVar.pythonPluginVariable());
+  EXPECT_EQ(1, var.pythonPluginTrendVariables().size());
+  EXPECT_EQ(0, var.pythonPluginOutputVariables().size());
+
+  PythonPluginOutputVariable pyOutVar(var);
+  EXPECT_EQ(var, pyOutVar.pythonPluginVariable());
+  EXPECT_EQ(1, var.pythonPluginTrendVariables().size());
+  EXPECT_EQ(1, var.pythonPluginOutputVariables().size());
+
+  PythonPluginOutputVariable pyOutVar2(var);
+  EXPECT_EQ(var, pyOutVar2.pythonPluginVariable());
+  EXPECT_EQ(1, var.pythonPluginTrendVariables().size());
+  EXPECT_EQ(2, var.pythonPluginOutputVariables().size());
+
+  EXPECT_EQ(1, m.getConcreteModelObjects<PythonPluginVariable>().size());
+  EXPECT_EQ(1, m.getConcreteModelObjects<PythonPluginTrendVariable>().size());
+  EXPECT_EQ(2, m.getConcreteModelObjects<PythonPluginOutputVariable>().size());
+
+  // Clone
+  auto varClone = var.clone(m).cast<PythonPluginVariable>();
+
+  EXPECT_EQ(2, m.getConcreteModelObjects<PythonPluginVariable>().size());
+  EXPECT_EQ(2, m.getConcreteModelObjects<PythonPluginTrendVariable>().size());
+  EXPECT_EQ(4, m.getConcreteModelObjects<PythonPluginOutputVariable>().size());
+
+  EXPECT_EQ(var, pyTrendVar.pythonPluginVariable());
+  EXPECT_EQ(var, pyOutVar.pythonPluginVariable());
+  EXPECT_EQ(var, pyOutVar2.pythonPluginVariable());
+  EXPECT_EQ(1, var.pythonPluginTrendVariables().size());
+  EXPECT_EQ(2, var.pythonPluginOutputVariables().size());
+
+  ASSERT_EQ(1, varClone.pythonPluginTrendVariables().size());
+  EXPECT_NE(pyTrendVar, varClone.pythonPluginTrendVariables().front());
+  ASSERT_EQ(2, varClone.pythonPluginOutputVariables().size());
+  EXPECT_NE(pyOutVar, varClone.pythonPluginOutputVariables().front());
+  EXPECT_NE(pyOutVar2, varClone.pythonPluginOutputVariables().front());
+  EXPECT_NE(pyOutVar, varClone.pythonPluginOutputVariables().back());
+  EXPECT_NE(pyOutVar2, varClone.pythonPluginOutputVariables().back());
+
+  pyOutVar.remove();
+  EXPECT_EQ(2, m.getConcreteModelObjects<PythonPluginTrendVariable>().size());
+  EXPECT_EQ(3, m.getConcreteModelObjects<PythonPluginOutputVariable>().size());
+  EXPECT_EQ(var, pyTrendVar.pythonPluginVariable());
+  EXPECT_EQ(var, pyOutVar2.pythonPluginVariable());
+  ASSERT_EQ(1, var.pythonPluginTrendVariables().size());
+  EXPECT_EQ(pyTrendVar, var.pythonPluginTrendVariables().front());
+  ASSERT_EQ(1, var.pythonPluginOutputVariables().size());
+  EXPECT_EQ(pyOutVar2, var.pythonPluginOutputVariables().front());
+
+  // remove
+  varClone.remove();
+
+  EXPECT_EQ(1, m.getConcreteModelObjects<PythonPluginVariable>().size());
+  EXPECT_EQ(1, m.getConcreteModelObjects<PythonPluginTrendVariable>().size());
+  EXPECT_EQ(1, m.getConcreteModelObjects<PythonPluginOutputVariable>().size());
+  ASSERT_EQ(1, var.pythonPluginTrendVariables().size());
+  EXPECT_EQ(pyTrendVar, var.pythonPluginTrendVariables().front());
+  ASSERT_EQ(1, var.pythonPluginOutputVariables().size());
+  EXPECT_EQ(pyOutVar2, var.pythonPluginOutputVariables().front());
+
+  // Clone other model
+  Model m2;
+  varClone = var.clone(m2).cast<PythonPluginVariable>();
+  EXPECT_EQ(1, m.getConcreteModelObjects<PythonPluginTrendVariable>().size());
+  EXPECT_EQ(1, m.getConcreteModelObjects<PythonPluginOutputVariable>().size());
+  ASSERT_EQ(1, var.pythonPluginTrendVariables().size());
+  EXPECT_EQ(pyTrendVar, var.pythonPluginTrendVariables().front());
+  ASSERT_EQ(1, var.pythonPluginOutputVariables().size());
+  EXPECT_EQ(pyOutVar2, var.pythonPluginOutputVariables().front());
+
+  EXPECT_EQ(1, m2.getConcreteModelObjects<PythonPluginVariable>().size());
+  EXPECT_EQ(1, m2.getConcreteModelObjects<PythonPluginTrendVariable>().size());
+  EXPECT_EQ(1, m2.getConcreteModelObjects<PythonPluginOutputVariable>().size());
+  ASSERT_EQ(1, varClone.pythonPluginTrendVariables().size());
+  EXPECT_NE(pyTrendVar, varClone.pythonPluginTrendVariables().front());
+  ASSERT_EQ(1, varClone.pythonPluginOutputVariables().size());
+  EXPECT_NE(pyOutVar2, varClone.pythonPluginOutputVariables().front());
+}