Skip to content
10 changes: 2 additions & 8 deletions BuildResidentialHPXML/measure.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<schema_version>3.1</schema_version>
<name>build_residential_hpxml</name>
<uid>a13a8983-2b01-4930-8af2-42030b6e4233</uid>
<version_id>74f8cfa4-d51d-49d3-a974-af3388ec0aff</version_id>
<version_modified>2025-05-03T15:58:46Z</version_modified>
<version_id>a35bc8f9-f4f8-4bfc-9387-701d162da262</version_id>
<version_modified>2025-05-03T17:47:18Z</version_modified>
<xml_checksum>2C38F48B</xml_checksum>
<class_name>BuildResidentialHPXML</class_name>
<display_name>HPXML Builder</display_name>
Expand Down Expand Up @@ -7713,12 +7713,6 @@
<usage_type>resource</usage_type>
<checksum>D8A38780</checksum>
</file>
<file>
<filename>options/ducts.tsv</filename>
<filetype>tsv</filetype>
<usage_type>resource</usage_type>
<checksum>5F1FC0B0</checksum>
</file>
<file>
<filename>version.txt</filename>
<filetype>txt</filetype>
Expand Down
8 changes: 4 additions & 4 deletions HPXMLtoOpenStudio/measure.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
<schema_version>3.1</schema_version>
<name>hpxm_lto_openstudio</name>
<uid>b1543b30-9465-45ff-ba04-1d1f85e763bc</uid>
<version_id>8af045cd-29e2-42a0-87ee-cd566e934120</version_id>
<version_modified>2025-05-03T15:58:56Z</version_modified>
<version_id>7425a4d6-eed7-4b64-8ccd-671060690b7b</version_id>
<version_modified>2025-05-03T20:01:42Z</version_modified>
<xml_checksum>D8922A73</xml_checksum>
<class_name>HPXMLtoOpenStudio</class_name>
<display_name>HPXML to OpenStudio Translator</display_name>
Expand Down Expand Up @@ -435,7 +435,7 @@
<filename>meta_measure.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>D7DF1BEA</checksum>
<checksum>0A27A2C8</checksum>
</file>
<file>
<filename>minitest_helper.rb</filename>
Expand All @@ -453,7 +453,7 @@
<filename>model.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>DA47D013</checksum>
<checksum>D85755A1</checksum>
</file>
<file>
<filename>output.rb</filename>
Expand Down
57 changes: 37 additions & 20 deletions HPXMLtoOpenStudio/resources/meta_measure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -51,32 +51,23 @@ def run_hpxml_workflow(rundir, measures, measures_dir, debug: false, run_measure
return { success: success, runner: runner }
end

# Remove unused objects automatically added by OpenStudio?
remove_objects = []
if model.alwaysOnContinuousSchedule.directUseCount == 0
remove_objects << ['Schedule:Constant', model.alwaysOnContinuousSchedule.name.to_s]
end
if model.alwaysOnDiscreteSchedule.directUseCount == 0
remove_objects << ['Schedule:Constant', model.alwaysOnDiscreteSchedule.name.to_s]
end
if model.alwaysOffDiscreteSchedule.directUseCount == 0
remove_objects << ['Schedule:Constant', model.alwaysOffDiscreteSchedule.name.to_s]
end
model.getScheduleConstants.each do |sch|
next unless sch.directUseCount == 0

remove_objects << ['Schedule:Constant', sch.name.to_s]
end
# Apply reporting measure output requests
apply_model_output_requests(measures_dir, measures, runner, model)

# Translate model to workspace
forward_translator = OpenStudio::EnergyPlus::ForwardTranslator.new
forward_translator.setExcludeLCCObjects(true)
workspace = forward_translator.translateModel(model)
success = report_ft_errors_warnings(forward_translator, rundir)

# Remove objects
remove_objects.uniq.each do |remove_object|
workspace.getObjectByTypeAndName(remove_object[0].to_IddObjectType, remove_object[1]).get.remove
# Remove unused objects automatically added by OpenStudio?
[model.alwaysOnContinuousSchedule,
model.alwaysOnDiscreteSchedule,
model.alwaysOffDiscreteSchedule].each do |sch|
# Don't know why we need to check for AdditionalProperties too, but it works
if sch.directUseCount == 0 || sch.sources.all? { |s| s.to_AdditionalProperties.is_initialized }
workspace.getObjectByTypeAndName('Schedule:Constant'.to_IddObjectType, sch.name.to_s).get.remove
end
end

if not success
Expand Down Expand Up @@ -250,7 +241,33 @@ def get_full_measure_path(measures_dir, measure_name, runner)
register_error("Cannot find measure #{measure_name} in any of the measures_dirs: #{measures_dirs.join(', ')}.", runner)
end

# Apply OpenStudio measures and arguments (i.e., "energyPlusOutputRequests" method) corresponding to a provided Hash.
# Apply reporting measure output requests (i.e., "modelOutputRequests" method).
#
# @param measures_dir [String or Array<String>] Parent directory path(s) of all OpenStudio-HPXML measures
# @param measures [Hash] Map of OpenStudio-HPXML measure directory name => List of measure argument hashes
# @param runner [OpenStudio::Measure::OSRunner] Object typically used to display warnings
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @return [Boolean] True if EnergyPlus output requests have been applied successfully
def apply_model_output_requests(measures_dir, measures, runner, model)
# Call each measure in the specified order
measures.keys.each do |measure_subdir|
# Gather measure arguments and call measure
full_measure_path = File.join(measures_dir, measure_subdir, 'measure.rb')
check_file_exists(full_measure_path, runner)
measure = get_measure_instance(full_measure_path)
measures[measure_subdir].each do |args|
next unless measure.class.superclass.name.to_s == 'OpenStudio::Measure::ReportingMeasure'

argument_map = get_argument_map(model, measure, args, measure_subdir, runner)
runner.setLastOpenStudioModel(model)
measure.modelOutputRequests(model, runner, argument_map)
end
end

return true
end

# Apply reporting measure output requests (i.e., "energyPlusOutputRequests" method).
#
# @param measures_dir [String or Array<String>] Parent directory path(s) of all OpenStudio-HPXML measures
# @param measures [Hash] Map of OpenStudio-HPXML measure directory name => List of measure argument hashes
Expand Down
109 changes: 101 additions & 8 deletions HPXMLtoOpenStudio/resources/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -722,7 +722,7 @@ def self.add_schedule_type_limits(model, schedule:, limits:)
return stl
end

# Adds an EnergyManagementSystemSensor to the OpenStudio model.
# Adds an EnergyManagementSystemSensor object to the OpenStudio model.
#
# The EnergyManagementSystemSensor object gets information during the simulation
# that can be used in custom calculations.
Expand All @@ -739,7 +739,7 @@ def self.add_ems_sensor(model, name:, output_var_or_meter_name:, key_name:)
return sensor
end

# Adds an EnergyManagementSystemGlobalVariable to the OpenStudio model.
# Adds an EnergyManagementSystemGlobalVariable object to the OpenStudio model.
#
# The EnergyManagementSystemGlobalVariable object allows an EMS variable to be
# global such that it can be used across EMS programs/subroutines.
Expand All @@ -751,7 +751,7 @@ def self.add_ems_global_var(model, var_name:)
return OpenStudio::Model::EnergyManagementSystemGlobalVariable.new(model, ems_friendly_name(var_name))
end

# Adds an EnergyManagementSystemTrendVariable to the OpenStudio model.
# Adds an EnergyManagementSystemTrendVariable object to the OpenStudio model.
#
# The EnergyManagementSystemTrendVariable object creates a global EMS variable
# that stores the recent history of an EMS variable for use in a calculation.
Expand All @@ -767,7 +767,7 @@ def self.add_ems_trend_var(model, ems_object:, num_timesteps_logged:)
return tvar
end

# Adds an EnergyManagementSystemInternalVariable to the OpenStudio model.
# Adds an EnergyManagementSystemInternalVariable object to the OpenStudio model.
#
# The EnergyManagementSystemInternalVariable object is used to obtain static data from
# elsewhere in the model.
Expand All @@ -784,7 +784,7 @@ def self.add_ems_internal_var(model, name:, model_object:, type:)
return ivar
end

# Adds an EnergyManagementSystemActuator to the OpenStudio model.
# Adds an EnergyManagementSystemActuator object to the OpenStudio model.
#
# The EnergyManagementSystemActuator object specifies the properties or controls
# of an EnergyPlus object that is to be overridden during the simulation.
Expand All @@ -803,7 +803,7 @@ def self.add_ems_actuator(name:, model_object:, comp_type_and_control:)
return act
end

# Adds an EnergyManagementSystemProgram to the OpenStudio model.
# Adds an EnergyManagementSystemProgram object to the OpenStudio model.
#
# The EnergyManagementSystemProgram object allows custom calculations to be
# performed within the EnergyPlus simulation in order to override the properties
Expand All @@ -820,7 +820,7 @@ def self.add_ems_program(model, name:, lines: nil)
return prg
end

# Adds an EnergyManagementSystemSubroutine to the OpenStudio model.
# Adds an EnergyManagementSystemSubroutine object to the OpenStudio model.
#
# The EnergyManagementSystemSubroutine object allows EMS code to be reused
# across multiple EMS programs.
Expand All @@ -836,7 +836,7 @@ def self.add_ems_subroutine(model, name:, lines: nil)
return sbrt
end

# Adds an EnergyManagementSystemProgramCallingManager to the OpenStudio model.
# Adds an EnergyManagementSystemProgramCallingManager object to the OpenStudio model.
#
# The EnergyManagementSystemProgramCallingManager object is used to specify when
# an EMS program is run during the simulation.
Expand All @@ -856,6 +856,99 @@ def self.add_ems_program_calling_manager(model, name:, calling_point:, ems_progr
return pcm
end

# Adds an EnergyManagementSystemOutputVariable object to the OpenStudio model.
#
# The EnergyManagementSystemOutputVariable object allows generating output for
# an EMS variable; the object can be referenced by an OutputVariable object.
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param name [String] User-defined name for the new output variable
# @param ems_variable_name [String] EMS variable name to be output
# @param type_of_data [String] The nature of the variable ('averaged' or 'summed')
# @param update_frequency [String] Timestep the variable is associated with ('ZoneTimestep' or 'SystemTimestep')
# @param ems_program_or_subroutine [OpenStudio::Model::EnergyManagementSystemProgram or EnergyManagementSystemSubroutine] The EMS program/subroutine with the EMS variable
# @param units [String] The units for the output variable in standard EnergyPlus units
# @return [OpenStudio::Model::EnergyManagementSystemOutputVariable] The model object
def self.add_ems_output_variable(model, name:, ems_variable_name:, type_of_data:, update_frequency:, ems_program_or_subroutine:, units:)
ov = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, ems_variable_name)
ov.setName(name)
ov.setTypeOfDataInVariable(type_of_data)
ov.setUpdateFrequency(update_frequency)
ov.setEMSProgramOrSubroutineName(ems_program_or_subroutine)
ov.setUnits(units)
return ov
end

# Adds an OutputVariable object to the OpenStudio model. If there is already an
# identical existing object on the model, returns that instead.
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param key_value [String] The specific object reference for reporting
# @param variable_name [String] The variable name as shown in the eplusout.rdd file
# @param reporting_frequency [String] Output reporting frequency ('detailed', 'timestep', 'hourly', 'daily', 'monthly', 'runperiod', or 'annual')
# @return [OpenStudio::Model::OutputVariable] The model object
def self.add_output_variable(model, key_value:, variable_name:, reporting_frequency:)
model.getOutputVariables.each do |ov|
next unless ov.variableName == variable_name
next unless ov.keyValue == key_value
next unless ov.reportingFrequency == reporting_frequency

return ov # Duplicate of existing object
end
ov = OpenStudio::Model::OutputVariable.new(variable_name, model)
ov.setKeyValue(key_value)
ov.setReportingFrequency(reporting_frequency)
return ov
end

# Adds an OutputMeter object to the OpenStudio model. If there is already an
# identical existing object on the model, returns that instead.
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param meter_name [String] The meter name as shown in the eplusout.mdd file
# @param reporting_frequency [String] Output reporting frequency ('detailed', 'timestep', 'hourly', 'daily', 'monthly', 'runperiod', or 'annual')
# @return [OpenStudio::Model::OutputMeter] The model object
def self.add_output_meter(model, meter_name:, reporting_frequency:)
model.getOutputMeters.each do |om|
next unless om.name == meter_name
next unless om.reportingFrequency == reporting_frequency

return om # Duplicate of existing object
end
om = OpenStudio::Model::OutputMeter.new(model)
om.setName(meter_name)
om.setReportingFrequency(reporting_frequency)
return om
end

# Adds an OutputTableMonthly to the OpenStudio model. If there is already an
# identical existing object on the model, returns that instead.
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param name [String] Name for the output table
# @param digits_after_decimal [Integer] Number of digits after the decimal point
# @param output_var_or_meter_name [String] EnergyPlus Output:Variable or Output:Meter name
# @param aggregation_type [String] Aggregation type (SumOrAverage, Maximum, Minimum, etc.)
# @return [OpenStudio::Model::OutputTableMonthly] The model object
def self.add_output_table_monthly(model, name:, digits_after_decimal: 2, output_var_or_meter_name:, aggregation_type:)
model.getOutputTableMonthlys.each do |otm|
next unless otm.name.to_s == name
next unless otm.digitsAfterDecimal == digits_after_decimal
next unless otm.numberofMonthlyVariableGroups == 1

first_group = otm.getMonthlyVariableGroup(0).get
next unless first_group.variableOrMeterName == output_var_or_meter_name
next unless first_group.aggregationType == aggregation_type

return otm # Duplicate of existing object
end
otm = OpenStudio::Model::OutputTableMonthly.new(model)
otm.setName(name)
otm.setDigitsAfterDecimal(digits_after_decimal)
otm.addMonthlyVariableGroup(output_var_or_meter_name, aggregation_type)
return otm
end

# Converts existing string to EMS friendly string.
#
# Source: openstudio-standards
Expand Down
Loading