Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
## OpenStudio-HPXML v1.13.0

__Features__
- **Breaking change**: Conditioned crawlspaces are no longer allowed; use unvented crawlspace instead.
- **Breaking change**: Conditioned crawlspaces are no longer allowed; use unvented crawlspace instead.
- Adds unmet dehumidification hours output for hours where the dehumidifier RH setpoint is exceeded; issues a warning if more than 300 hours.

__Bugfixes__
- Fixes ERV supply outlet enthalpy calculation used to calculate latent effectiveness.
Expand Down
30 changes: 15 additions & 15 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>hpxml_to_openstudio</name>
<uid>b1543b30-9465-45ff-ba04-1d1f85e763bc</uid>
<version_id>033831a5-9516-48c0-9ed2-d9f07b0e1609</version_id>
<version_modified>2026-05-21T21:45:00Z</version_modified>
<version_id>71e7dd9f-069f-4c91-a575-e805acc6ecbe</version_id>
<version_modified>2026-06-12T20:16:55Z</version_modified>
<xml_checksum>D8922A73</xml_checksum>
<class_name>HPXMLToOpenStudio</class_name>
<display_name>HPXML to OpenStudio Translator</display_name>
Expand Down Expand Up @@ -217,7 +217,7 @@
<filename>airflow.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>FEB9F10E</checksum>
<checksum>50E3B7E9</checksum>
</file>
<file>
<filename>battery.rb</filename>
Expand All @@ -235,7 +235,7 @@
<filename>constants.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>64015603</checksum>
<checksum>08968840</checksum>
</file>
<file>
<filename>constructions.rb</filename>
Expand Down Expand Up @@ -367,7 +367,7 @@
<filename>defaults.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>4044148D</checksum>
<checksum>A3B194F3</checksum>
</file>
<file>
<filename>electric_panel.rb</filename>
Expand All @@ -391,7 +391,7 @@
<filename>geometry.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>7F402CF9</checksum>
<checksum>4666E48F</checksum>
</file>
<file>
<filename>hotwater_appliances.rb</filename>
Expand All @@ -403,7 +403,7 @@
<filename>hpxml.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>4741A9D8</checksum>
<checksum>7BB98B65</checksum>
</file>
<file>
<filename>hpxml_schema/HPXML.xsd</filename>
Expand All @@ -421,7 +421,7 @@
<filename>hpxml_schematron/EPvalidator.sch</filename>
<filetype>sch</filetype>
<usage_type>resource</usage_type>
<checksum>0D042BF0</checksum>
<checksum>54FA6347</checksum>
</file>
<file>
<filename>hpxml_schematron/iso-schematron.xsd</filename>
Expand All @@ -433,7 +433,7 @@
<filename>hvac.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>DF5A0B42</checksum>
<checksum>02072419</checksum>
</file>
<file>
<filename>hvac_sizing.rb</filename>
Expand Down Expand Up @@ -499,7 +499,7 @@
<filename>output.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>855DE50B</checksum>
<checksum>20631212</checksum>
</file>
<file>
<filename>psychrometrics.rb</filename>
Expand Down Expand Up @@ -715,7 +715,7 @@
<filename>waterheater.rb</filename>
<filetype>rb</filetype>
<usage_type>resource</usage_type>
<checksum>C9FBD6FB</checksum>
<checksum>C428F4AB</checksum>
</file>
<file>
<filename>weather.rb</filename>
Expand Down Expand Up @@ -751,7 +751,7 @@
<filename>test_defaults.rb</filename>
<filetype>rb</filetype>
<usage_type>test</usage_type>
<checksum>396FC7B6</checksum>
<checksum>BE1661EA</checksum>
</file>
<file>
<filename>test_electric_panel.rb</filename>
Expand All @@ -763,7 +763,7 @@
<filename>test_enclosure.rb</filename>
<filetype>rb</filetype>
<usage_type>test</usage_type>
<checksum>F3EDA5AC</checksum>
<checksum>0DA5536E</checksum>
</file>
<file>
<filename>test_generator.rb</filename>
Expand Down Expand Up @@ -829,7 +829,7 @@
<filename>test_validation.rb</filename>
<filetype>rb</filetype>
<usage_type>test</usage_type>
<checksum>08D1113A</checksum>
<checksum>EA35509A</checksum>
</file>
<file>
<filename>test_vehicle.rb</filename>
Expand All @@ -841,7 +841,7 @@
<filename>test_water_heater.rb</filename>
<filetype>rb</filetype>
<usage_type>test</usage_type>
<checksum>8DBA3BD3</checksum>
<checksum>F216022C</checksum>
</file>
<file>
<filename>test_weather.rb</filename>
Expand Down
7 changes: 5 additions & 2 deletions HPXMLtoOpenStudio/resources/constants.rb
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ module Constants
ObjectTypeSensorIndoorHeatingSetpointTemp = 'indoor heating setpoint temp sensor'
ObjectTypeSensorScheduleBAHSPCoolingSeason = 'bahsp cooling season sensor'
ObjectTypeSensorScheduleCoolingAvailability = 'cooling availability sensor'
ObjectTypeSensorScheduleDehumidAvailability = 'dehumid availability sensor'
ObjectTypeSensorScheduleHeatingAvailability = 'heating availability sensor'
ObjectTypeSensorScheduleVehicleDischarge = 'vehicle discharge schedule sensor'
ObjectTypeSensorSiteGroundTemp = 'site ground temp sensor'
Expand All @@ -104,9 +105,10 @@ module Constants
ObjectTypeTotalAirflowsProgram = 'total airflows program'
ObjectTypeTotalLoadsProgram = 'total loads program'
ObjectTypeUnitHeater = 'unit heater'
ObjectTypeUnmetHoursProgram = 'unmet hours program'
ObjectTypeUnmetDehumidHoursProgram = 'dehumid unmet hours program'
ObjectTypeUnmetHVACHoursProgram = 'hvac unmet hours program'
ObjectTypeUnmetVehicleHoursProgram = 'vehicle unmet hours program'
ObjectTypeVehicle = 'vehicle'
ObjectTypeVehicleUnmetHoursProgram = 'vehicle unmet hours program'
ObjectTypeWaterHeater = 'water heater'
ObjectTypeWaterHeaterSetpoint = 'water heater setpoint'
ObjectTypeWaterHeaterAdjustment = 'water heater energy adjustment'
Expand Down Expand Up @@ -278,6 +280,7 @@ module CLT
module UHT
Heating = 'Heating'
Cooling = 'Cooling'
Dehumid = 'Dehumidification'
Driving = 'EV Driving'
end

Expand Down
12 changes: 10 additions & 2 deletions HPXMLtoOpenStudio/resources/hvac.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1556,12 +1556,20 @@ def self.apply_dehumidifiers(runner, model, spaces, hpxml_bldg, hpxml_header)
# Availability Schedule
dehum_unavailable_periods = Schedule.get_unavailable_periods(runner, SchedulesFile::Columns[:Dehumidifier].name, hpxml_header.unavailable_periods)
avail_sch = ScheduleConstant.new(model, obj_name + ' schedule', 1.0, EPlus::ScheduleTypeLimitsFraction, unavailable_periods: dehum_unavailable_periods)
avail_sch = avail_sch.schedule

# Add sensor for unmet dehumidification hours EMS program
dehumid_avail_sensor = Model.add_ems_sensor(
model,
name: "#{avail_sch.schedule.name} s",
output_var_or_meter_name: 'Schedule Value',
key_name: avail_sch.schedule.name
)
dehumid_avail_sensor.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeSensorScheduleDehumidAvailability)

# Dehumidifier
dehumidifier = OpenStudio::Model::ZoneHVACDehumidifierDX.new(model, capacity_curve, energy_factor_curve, part_load_frac_curve)
dehumidifier.setName(obj_name)
dehumidifier.setAvailabilitySchedule(avail_sch)
dehumidifier.setAvailabilitySchedule(avail_sch.schedule)
dehumidifier.setRatedWaterRemoval(UnitConversions.convert(total_capacity, 'pint', 'L'))
dehumidifier.setRatedEnergyFactor(avg_energy_factor / total_fraction_served)
dehumidifier.setRatedAirFlowRate(UnitConversions.convert(air_flow_rate, 'cfm', 'm^3/s'))
Expand Down
65 changes: 60 additions & 5 deletions HPXMLtoOpenStudio/resources/output.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ module Outputs
# @param add_component_loads [Boolean] Whether to calculate component loads (since it incurs a runtime speed penalty)
# @return [nil]
def self.apply_ems_programs(model, hpxml_osm_map, hpxml_header, add_component_loads)
season_day_nums = apply_unmet_hours_ems_program(model, hpxml_osm_map, hpxml_header)
season_day_nums = apply_unmet_hvac_hours_ems_program(model, hpxml_osm_map, hpxml_header)
apply_unmet_dehumid_hours_ems_program(model, hpxml_osm_map)
apply_unmet_driving_hours_ems_program(model, hpxml_osm_map)
loads_data = apply_total_loads_ems_program(model, hpxml_osm_map, hpxml_header)
if add_component_loads
Expand All @@ -59,7 +60,7 @@ def self.apply_ems_programs(model, hpxml_osm_map, hpxml_header, add_component_lo
# @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit
# @param hpxml_header [HPXML::Header] HPXML Header object (one per HPXML file)
# @return [Hash] Mapping of unit index => heating/cooling season begin and end dates for use by subsequent programs
def self.apply_unmet_hours_ems_program(model, hpxml_osm_map, hpxml_header)
def self.apply_unmet_hvac_hours_ems_program(model, hpxml_osm_map, hpxml_header)
# Create sensors and gather data
htg_sensors, clg_sensors, htg_avail_sensors, clg_avail_sensors = {}, {}, {}, {}
zone_air_temp_sensors, htg_sp_sensors, clg_sp_sensors = {}, {}, {}
Expand Down Expand Up @@ -123,7 +124,7 @@ def self.apply_unmet_hours_ems_program(model, hpxml_osm_map, hpxml_header)
model,
name: 'unmet hours program'
)
program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeUnmetHoursProgram)
program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeUnmetHVACHoursProgram)
program.addLine("Set #{htg_hrs} = 0")
program.addLine("Set #{clg_hrs} = 0")
for unit in 0..hpxml_osm_map.size - 1
Expand Down Expand Up @@ -182,6 +183,60 @@ def self.apply_unmet_hours_ems_program(model, hpxml_osm_map, hpxml_header)
return season_day_nums
end

# Creates an EMS program that calculates dehumidification unmet hours (number
# of hours where the dehumidifiers' RH setpoint is not met).
#
# @param model [OpenStudio::Model::Model] OpenStudio Model object
# @param hpxml_osm_map [Hash] Map of HPXML::Building objects => OpenStudio Model objects for each dwelling unit
# @return [nil]
def self.apply_unmet_dehumid_hours_ems_program(model, hpxml_osm_map)
return if hpxml_osm_map.keys.map { |hpxml_bldg| hpxml_bldg.dehumidifiers.size }.sum == 0

rh_sensors, rh_setpoints, avail_sensors = {}, {}, {}
hpxml_osm_map.each_with_index do |(hpxml_bldg, unit_model), unit|
next if hpxml_bldg.dehumidifiers.empty?

# EMS Sensor
rh_sensors[unit] = unit_model.getEnergyManagementSystemSensors.find { |s| s.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeSensorIndoorAirRH }
rh_setpoints[unit] = hpxml_bldg.dehumidifiers[0].rh_setpoint * 100.0
avail_sensors[unit] = unit_model.getEnergyManagementSystemSensors.find { |s| s.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeSensorScheduleDehumidAvailability }
Comment thread
shorowit marked this conversation as resolved.
end

rh_tol = 1.0 # 1% RH

# EMS program
dehum_hrs = 'dehumid_unmet_hours'
unit_dehum_hrs = 'unit_dehumid_unmet_hours'
program = Model.add_ems_program(
model,
name: 'unmet dehumid hours program'
)
program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeUnmetDehumidHoursProgram)
program.addLine("Set #{dehum_hrs} = 0")
for unit in 0..hpxml_osm_map.size - 1
next if rh_sensors[unit].nil?

program.addLine("Set #{unit_dehum_hrs} = 0")
program.addLine("Set indoor_rh = #{rh_sensors[unit].name}")
line = "If (indoor_rh > #{rh_setpoints[unit]} + #{rh_tol})"
line += " && (#{avail_sensors[unit].name} == 1)" unless avail_sensors[unit].nil?
program.addLine(line)
program.addLine(" Set #{unit_dehum_hrs} = #{unit_dehum_hrs} + ZoneTimestep")
program.addLine('EndIf')
program.addLine("If (#{unit_dehum_hrs} > #{dehum_hrs})") # Use max hourly value across all units
program.addLine(" Set #{dehum_hrs} = #{unit_dehum_hrs}")
program.addLine('EndIf')
end

# EMS calling manager
Model.add_ems_program_calling_manager(
model,
name: "#{program.name} manager",
calling_point: 'EndOfZoneTimestepBeforeZoneReporting',
ems_programs: [program]
)
end

# Creates an EMS program that calculates driving unmet hours (number
# of hours where the EV driving demand is not met).
#
Expand All @@ -192,13 +247,13 @@ def self.apply_unmet_driving_hours_ems_program(model, hpxml_osm_map)
return if hpxml_osm_map.keys.map { |hpxml_bldg| hpxml_bldg.vehicles.map { |vehicle| vehicle.vehicle_type == HPXML::VehicleTypeBEV && !vehicle.ev_charger_idref.nil? }.size }.sum == 0

# EMS program
driving_hrs = 'unmet_driving_hours'
driving_hrs = 'driving_unmet_hours'
unit_driving_hrs = 'unit_driving_unmet_hours'
program = Model.add_ems_program(
model,
name: 'unmet driving hours program'
)
program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeVehicleUnmetHoursProgram)
program.additionalProperties.setFeature('ObjectType', Constants::ObjectTypeUnmetVehicleHoursProgram)
program.addLine("Set #{driving_hrs} = 0")

hpxml_osm_map.each do |hpxml_bldg, unit_model|
Expand Down
2 changes: 2 additions & 0 deletions ReportSimulationOutput/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -806,6 +806,8 @@ All possible measure outputs are listed below. Actual outputs depend on measure

- ``unmet_hours_cooling_hr``

- ``unmet_hours_dehumidification_hr``

- ``unmet_hours_ev_driving_hr``

- ``peak_electricity_winter_total_w``
Expand Down
20 changes: 14 additions & 6 deletions ReportSimulationOutput/measure.rb
Original file line number Diff line number Diff line change
Expand Up @@ -373,11 +373,12 @@ def modelOutputRequests(model, runner, user_arguments)
return false
end

unmet_hours_program = model.getEnergyManagementSystemPrograms.find { |p| p.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeUnmetHoursProgram }
unmet_hvac_hours_program = model.getEnergyManagementSystemPrograms.find { |p| p.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeUnmetHVACHoursProgram }
unmet_driving_hrs_program = model.getEnergyManagementSystemPrograms.find { |p| p.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeUnmetVehicleHoursProgram }
unmet_dehumid_hrs_program = model.getEnergyManagementSystemPrograms.find { |p| p.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeUnmetDehumidHoursProgram }
total_loads_program = model.getEnergyManagementSystemPrograms.find { |p| p.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeTotalLoadsProgram }
comp_loads_program = model.getEnergyManagementSystemPrograms.find { |p| p.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeComponentLoadsProgram }
total_airflows_program = model.getEnergyManagementSystemPrograms.find { |p| p.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeTotalAirflowsProgram }
unmet_driving_hrs_program = model.getEnergyManagementSystemPrograms.find { |p| p.additionalProperties.getFeatureAsString('ObjectType').to_s == Constants::ObjectTypeVehicleUnmetHoursProgram }
heated_zones = eval(model.getBuilding.additionalProperties.getFeatureAsString('heated_zones').get)
cooled_zones = eval(model.getBuilding.additionalProperties.getFeatureAsString('cooled_zones').get)

Expand Down Expand Up @@ -488,9 +489,13 @@ def modelOutputRequests(model, runner, user_arguments)

# Unmet Hours (annual only)
@unmet_hours.each do |key, unmet_hour|
next if key == UHT::Driving && unmet_driving_hrs_program.nil?

ems_program = key == UHT::Driving ? unmet_driving_hrs_program : unmet_hours_program
ems_program = {
UHT::Driving => unmet_driving_hrs_program,
UHT::Dehumid => unmet_dehumid_hrs_program,
UHT::Heating => unmet_hvac_hours_program,
UHT::Cooling => unmet_hvac_hours_program
}[key]
next if ems_program.nil?

ems_ov = Model.add_ems_output_variable(model, name: "#{unmet_hour.ems_variable}_annual_outvar", ems_variable_name: unmet_hour.ems_variable, type_of_data: 'Summed', update_frequency: 'ZoneTimestep', ems_program_or_subroutine: ems_program, units: 'hr')
Model.add_output_variable(model, key_value: '*', variable_name: ems_ov.name.to_s, reporting_frequency: 'runperiod')
Expand Down Expand Up @@ -901,6 +906,8 @@ def get_outputs(runner, args)
runner.registerWarning("There are a large number of unmet hours (#{unmet_hour.annual_output}) for heating; this may indicate the heating system is undersized or can be caused by recovery from thermostat setbacks.")
elsif key == UHT::Cooling && unmet_hour.annual_output > 300
runner.registerWarning("There are a large number of unmet hours (#{unmet_hour.annual_output}) for cooling; this may indicate the cooling system is undersized or can be caused by recovery from thermostat setbacks.")
elsif key == UHT::Dehumid && unmet_hour.annual_output > 300
runner.registerWarning("There are a large number of unmet hours (#{unmet_hour.annual_output}) for dehumidification; this may indicate the dehumidification system is undersized.")
end
end

Expand Down Expand Up @@ -2932,7 +2939,8 @@ def get_timeseries_units_from_fuel_type(fuel_type)
@unmet_hours = {}
@unmet_hours[UHT::Heating] = UnmetHours.new(ems_variable: 'htg_unmet_hours')
@unmet_hours[UHT::Cooling] = UnmetHours.new(ems_variable: 'clg_unmet_hours')
@unmet_hours[UHT::Driving] = UnmetHours.new(ems_variable: 'unmet_driving_hours')
@unmet_hours[UHT::Dehumid] = UnmetHours.new(ems_variable: 'dehumid_unmet_hours')
@unmet_hours[UHT::Driving] = UnmetHours.new(ems_variable: 'driving_unmet_hours')

@unmet_hours.each do |load_type, unmet_hour|
unmet_hour.name = "Unmet Hours: #{load_type}"
Expand Down
Loading