class Standard # @!group ZoneHVACComponent # default fan efficiency for small zone hvac fans, in watts per cfm # # @return [Double] fan efficiency in watts per cfm def zone_hvac_component_prm_baseline_fan_efficacy fan_efficacy_w_per_cfm = 0.3 return fan_efficacy_w_per_cfm end # Sets the fan power of zone level HVAC equipment # (Fan coils, Unit Heaters, PTACs, PTHPs, VRF Terminals, WSHPs, ERVs) # based on the W/cfm specified in the standard. # # @param zone_hvac_component [OpenStudio::Model::ZoneHVACComponent] zone hvac component # @return [Bool] returns true if successful, false if not def zone_hvac_component_apply_prm_baseline_fan_power(zone_hvac_component) OpenStudio.logFree(OpenStudio::Debug, 'openstudio.standards.ZoneHVACComponent', "Setting fan power for #{zone_hvac_component.name}.") # Convert this to the actual class type zone_hvac = if zone_hvac_component.to_ZoneHVACFourPipeFanCoil.is_initialized zone_hvac_component.to_ZoneHVACFourPipeFanCoil.get elsif zone_hvac_component.to_ZoneHVACUnitHeater.is_initialized zone_hvac_component.to_ZoneHVACUnitHeater.get elsif zone_hvac_component.to_ZoneHVACPackagedTerminalAirConditioner.is_initialized zone_hvac_component.to_ZoneHVACPackagedTerminalAirConditioner.get elsif zone_hvac_component.to_ZoneHVACPackagedTerminalHeatPump.is_initialized zone_hvac_component.to_ZoneHVACPackagedTerminalHeatPump.get elsif zone_hvac_component.to_ZoneHVACTerminalUnitVariableRefrigerantFlow.is_initialized zone_hvac_component.to_ZoneHVACTerminalUnitVariableRefrigerantFlow.get elsif zone_hvac_component.to_ZoneHVACWaterToAirHeatPump.is_initialized zone_hvac_component.to_ZoneHVACWaterToAirHeatPump.get elsif zone_hvac_component.to_ZoneHVACEnergyRecoveryVentilator.is_initialized zone_hvac_component.to_ZoneHVACEnergyRecoveryVentilator.get end # Do nothing for other types of zone HVAC equipment if zone_hvac.nil? return false end # Determine the W/cfm fan_efficacy_w_per_cfm = zone_hvac_component_prm_baseline_fan_efficacy # Convert efficacy to metric # 1 cfm = 0.0004719 m^3/s fan_efficacy_w_per_m3_per_s = fan_efficacy_w_per_cfm / 0.0004719 # Get the fan fan = if zone_hvac.supplyAirFan.to_FanConstantVolume.is_initialized zone_hvac.supplyAirFan.to_FanConstantVolume.get elsif zone_hvac.supplyAirFan.to_FanVariableVolume.is_initialized zone_hvac.supplyAirFan.to_FanVariableVolume.get elsif zone_hvac.supplyAirFan.to_FanOnOff.is_initialized zone_hvac.supplyAirFan.to_FanOnOff.get end # Get the maximum flow rate through the fan max_air_flow_rate = nil if fan.autosizedMaximumFlowRate.is_initialized max_air_flow_rate = fan.autosizedMaximumFlowRate.get elsif fan.maximumFlowRate.is_initialized max_air_flow_rate = fan.maximumFlowRate.get end max_air_flow_rate_cfm = OpenStudio.convert(max_air_flow_rate, 'm^3/s', 'ft^3/min').get # Set the impeller efficiency fan_change_impeller_efficiency(fan, fan_baseline_impeller_efficiency(fan)) # Set the motor efficiency, preserving the impeller efficency. # For zone HVAC fans, a bhp lookup of 0.5bhp is always used because # they are assumed to represent a series of small fans in reality. fan_apply_standard_minimum_motor_efficiency(fan, fan_brake_horsepower(fan)) # Calculate a new pressure rise to hit the target W/cfm fan_tot_eff = fan.fanEfficiency fan_rise_new_pa = fan_efficacy_w_per_m3_per_s * fan_tot_eff fan.setPressureRise(fan_rise_new_pa) # Calculate the newly set efficacy fan_power_new_w = fan_rise_new_pa * max_air_flow_rate / fan_tot_eff fan_efficacy_new_w_per_cfm = fan_power_new_w / max_air_flow_rate_cfm OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ZoneHVACComponent', "For #{zone_hvac_component.name}: fan efficacy set to #{fan_efficacy_new_w_per_cfm.round(2)} W/cfm.") return true end # Get the supply fan object for a zone equipment component # @author Doug Maddox, PNNL # @param zone_hvac_component [object] # @return [object] supply fan of zone equipment component def zone_hvac_get_fan_object(zone_hvac_component) zone_hvac = nil # Check for any zone equipment type that has a supply fan # except EnergyRecoveryVentilator, which is not a primary conditioning system zone_hvac = if zone_hvac_component.to_ZoneHVACFourPipeFanCoil.is_initialized zone_hvac_component.to_ZoneHVACFourPipeFanCoil.get elsif zone_hvac_component.to_ZoneHVACPackagedTerminalAirConditioner.is_initialized zone_hvac_component.to_ZoneHVACPackagedTerminalAirConditioner.get elsif zone_hvac_component.to_ZoneHVACPackagedTerminalHeatPump.is_initialized zone_hvac_component.to_ZoneHVACPackagedTerminalHeatPump.get elsif zone_hvac_component.to_ZoneHVACTerminalUnitVariableRefrigerantFlow.is_initialized zone_hvac_component.to_ZoneHVACTerminalUnitVariableRefrigerantFlow.get elsif zone_hvac_component.to_ZoneHVACUnitHeater.is_initialized zone_hvac_component.to_ZoneHVACUnitHeater.get elsif zone_hvac_component.to_ZoneHVACUnitVentilator.is_initialized zone_hvac_component.to_ZoneHVACUnitVentilator.get elsif zone_hvac_component.to_ZoneHVACWaterToAirHeatPump.is_initialized zone_hvac_component.to_ZoneHVACWaterToAirHeatPump.get end # Get the fan if !zone_hvac.nil? fan_obj = if zone_hvac.supplyAirFan.to_FanConstantVolume.is_initialized zone_hvac.supplyAirFan.to_FanConstantVolume.get elsif zone_hvac.supplyAirFan.to_FanVariableVolume.is_initialized zone_hvac.supplyAirFan.to_FanVariableVolume.get elsif zone_hvac.supplyAirFan.to_FanOnOff.is_initialized zone_hvac.supplyAirFan.to_FanOnOff.get elsif zone_hvac.supplyAirFan.to_FanSystemModel.is_initialized zone_hvac.supplyAirFan.to_FanSystemModel.get end return fan_obj else return nil end end # Default occupancy fraction threshold for determining if the spaces served by the zone hvac are occupied # # @return [Double] unoccupied threshold def zone_hvac_unoccupied_threshold return 0.15 end # If the supply air fan operating mode schedule is always off (to follow load), # and the zone requires ventilation, override it to follow the zone occupancy schedule # # @param zone_hvac_component [OpenStudio::Model::ZoneHVACComponent] zone hvac component # @return [Bool] returns true if successful, false if not def zone_hvac_component_occupancy_ventilation_control(zone_hvac_component) ventilation = false # Zone HVAC operating schedule if providing ventilation # Zone HVAC components return an OptionalSchedule object for supplyAirFanOperatingModeSchedule # except for ZoneHVACTerminalUnitVariableRefrigerantFlow which returns a Schedule # and starting at 3.5.0, PTAC / PTHP also return a Schedule, optional before that existing_sch = nil if zone_hvac_component.to_ZoneHVACFourPipeFanCoil.is_initialized zone_hvac_component = zone_hvac_component.to_ZoneHVACFourPipeFanCoil.get if zone_hvac_component.maximumOutdoorAirFlowRate.is_initialized oa_rate = zone_hvac_component.maximumOutdoorAirFlowRate.get ventilation = true if oa_rate > 0.0 end ventilation = true if zone_hvac_component.isMaximumOutdoorAirFlowRateAutosized fan_op_sch = zone_hvac_component.supplyAirFanOperatingModeSchedule existing_sch = fan_op_sch.get if fan_op_sch.is_initialized elsif zone_hvac_component.to_ZoneHVACPackagedTerminalAirConditioner.is_initialized zone_hvac_component = zone_hvac_component.to_ZoneHVACPackagedTerminalAirConditioner.get if zone_hvac_component.outdoorAirFlowRateWhenNoCoolingorHeatingisNeeded.is_initialized oa_rate = zone_hvac_component.outdoorAirFlowRateWhenNoCoolingorHeatingisNeeded.get ventilation = true if oa_rate > 0.0 end ventilation = true if zone_hvac_component.isOutdoorAirFlowRateWhenNoCoolingorHeatingisNeededAutosized fan_op_sch = OpenStudio::Model::OptionalSchedule.new(zone_hvac_component.supplyAirFanOperatingModeSchedule) existing_sch = fan_op_sch.get if fan_op_sch.is_initialized elsif zone_hvac_component.to_ZoneHVACPackagedTerminalHeatPump.is_initialized zone_hvac_component = zone_hvac_component.to_ZoneHVACPackagedTerminalHeatPump.get if zone_hvac_component.outdoorAirFlowRateWhenNoCoolingorHeatingisNeeded.is_initialized oa_rate = zone_hvac_component.outdoorAirFlowRateWhenNoCoolingorHeatingisNeeded.get ventilation = true if oa_rate > 0.0 end ventilation = true if zone_hvac_component.isOutdoorAirFlowRateWhenNoCoolingorHeatingisNeededAutosized fan_op_sch = OpenStudio::Model::OptionalSchedule.new(zone_hvac_component.supplyAirFanOperatingModeSchedule) existing_sch = fan_op_sch.get if fan_op_sch.is_initialized elsif zone_hvac_component.to_ZoneHVACTerminalUnitVariableRefrigerantFlow.is_initialized zone_hvac_component = zone_hvac_component.to_ZoneHVACTerminalUnitVariableRefrigerantFlow.get if zone_hvac_component.outdoorAirFlowRateWhenNoCoolingorHeatingisNeeded.is_initialized oa_rate = zone_hvac_component.outdoorAirFlowRateWhenNoCoolingorHeatingisNeeded.get ventilation = true if oa_rate > 0.0 end ventilation = true if zone_hvac_component.isOutdoorAirFlowRateWhenNoCoolingorHeatingisNeededAutosized existing_sch = zone_hvac_component.supplyAirFanOperatingModeSchedule elsif zone_hvac_component.to_ZoneHVACWaterToAirHeatPump.is_initialized zone_hvac_component = zone_hvac_component.to_ZoneHVACWaterToAirHeatPump.get if zone_hvac_component.outdoorAirFlowRateWhenNoCoolingorHeatingisNeeded.is_initialized oa_rate = zone_hvac_component.outdoorAirFlowRateWhenNoCoolingorHeatingisNeeded.get ventilation = true if oa_rate > 0.0 end ventilation = true if zone_hvac_component.isOutdoorAirFlowRateWhenNoCoolingorHeatingisNeededAutosized fan_op_sch = zone_hvac_component.supplyAirFanOperatingModeSchedule existing_sch = fan_op_sch.get if fan_op_sch.is_initialized end return false unless ventilation # if supply air fan operating schedule is always off, # override to provide ventilation during occupied hours unless existing_sch.nil? if existing_sch.name.is_initialized OpenStudio.logFree(OpenStudio::Info, 'openstudio.Standards.ZoneHVACComponent', "#{zone_hvac_component.name} has ventilation, and schedule is set to always on; keeping always on schedule.") return false if existing_sch.name.get.to_s.downcase.include?('always on discrete') || existing_sch.name.get.to_s.downcase.include?('guestroom_vent_ctrl_sch') end end thermal_zone = zone_hvac_component.thermalZone.get occ_threshold = zone_hvac_unoccupied_threshold occ_sch = thermal_zones_get_occupancy_schedule([thermal_zone], sch_name: "#{zone_hvac_component.name} Occ Sch", occupied_percentage_threshold: occ_threshold) zone_hvac_component.setSupplyAirFanOperatingModeSchedule(occ_sch) OpenStudio.logFree(OpenStudio::Info, 'openstudio.Standards.ZoneHVACComponent', "#{zone_hvac_component.name} has ventilation. Setting fan operating mode schedule to align with zone occupancy schedule.") return true end # Apply all standard required controls to the zone equipment # # @param zone_hvac_component [OpenStudio::Model::ZoneHVACComponent] zone hvac component # @return [Bool] returns true if successful, false if not def zone_hvac_component_apply_standard_controls(zone_hvac_component) # Vestibule heating control if zone_hvac_component_vestibule_heating_control_required?(zone_hvac_component) zone_hvac_component_apply_vestibule_heating_control(zone_hvac_component) end # Convert to objects zone_hvac_component = if zone_hvac_component.to_ZoneHVACFourPipeFanCoil.is_initialized zone_hvac_component.to_ZoneHVACFourPipeFanCoil.get elsif zone_hvac_component.to_ZoneHVACPackagedTerminalAirConditioner.is_initialized zone_hvac_component.to_ZoneHVACPackagedTerminalAirConditioner.get elsif zone_hvac_component.to_ZoneHVACPackagedTerminalHeatPump.is_initialized zone_hvac_component.to_ZoneHVACPackagedTerminalHeatPump.get end # Do nothing for other types of zone HVAC equipment if zone_hvac_component.nil? return true end # Standby mode occupancy control return true unless zone_hvac_component.thermalZone.empty? thermal_zone = zone_hvac_component.thermalZone.get standby_mode_spaces = [] thermal_zone.spaces.sort.each do |space| if space_occupancy_standby_mode_required?(space) standby_mode_spaces << space end end if !standby_mode_spaces.empty? zone_hvac_model_standby_mode_occupancy_control(zone_hvac_component) end # zone ventilation occupancy control for systems with ventilation zone_hvac_component_occupancy_ventilation_control(zone_hvac_component) return true end # Determine if vestibule heating control is required. # Defaults to 90.1-2004 through 2010, not required. # # @param zone_hvac_component [OpenStudio::Model::ZoneHVACComponent] zone hvac component # @return [Bool] returns true if successful, false if not def zone_hvac_component_vestibule_heating_control_required?(zone_hvac_component) vest_htg_control_required = false return vest_htg_control_required end # Add occupant standby controls to zone equipment # Currently, the controls consists of cycling the # fan during the occupant standby mode hours # # @param zone_hvac_component OpenStudio zonal equipment object # @retrun [Boolean] true if sucessful, false otherwise def zone_hvac_model_standby_mode_occupancy_control(zone_hvac_component) return true end # Turns off vestibule heating below 45F # # @param zone_hvac_component [OpenStudio::Model::ZoneHVACComponent] zone hvac component # @return [Bool] returns true if successful, false if not def zone_hvac_component_apply_vestibule_heating_control(zone_hvac_component) # Ensure that the equipment is assigned to a thermal zone if zone_hvac_component.thermalZone.empty? OpenStudio.logFree(OpenStudio::Warn, 'openstudio.model.ZoneHVACComponent', "For #{zone_hvac_component.name}: equipment is not assigned to a thermal zone, cannot apply vestibule heating control.") return true end # Convert this to the actual class type zone_hvac = if zone_hvac_component.to_ZoneHVACFourPipeFanCoil.is_initialized zone_hvac_component.to_ZoneHVACFourPipeFanCoil.get elsif zone_hvac_component.to_ZoneHVACUnitHeater.is_initialized zone_hvac_component.to_ZoneHVACUnitHeater.get elsif zone_hvac_component.to_ZoneHVACPackagedTerminalAirConditioner.is_initialized zone_hvac_component.to_ZoneHVACPackagedTerminalAirConditioner.get elsif zone_hvac_component.to_ZoneHVACPackagedTerminalHeatPump.is_initialized zone_hvac_component.to_ZoneHVACPackagedTerminalHeatPump.get end # Do nothing for other types of zone HVAC equipment if zone_hvac.nil? return true end # Get the heating coil and fan htg_coil = zone_hvac.heatingCoil htg_coil = if htg_coil.to_CoilHeatingGas.is_initialized htg_coil.to_CoilHeatingGas.get elsif htg_coil.to_CoilHeatingElectric.is_initialized htg_coil.to_CoilHeatingElectric.get elsif htg_coil.to_CoilHeatingWater.is_initialized htg_coil.to_CoilHeatingWater.get elsif htg_coil.to_CoilHeatingDXSingleSpeed.is_initialized htg_coil.to_CoilHeatingDXSingleSpeed.get end fan = zone_hvac.supplyAirFan fan = if fan.to_FanOnOff.is_initialized fan.to_FanOnOff.get elsif fan.to_FanConstantVolume.is_initialized fan.to_FanConstantVolume.get elsif fan.to_FanVariableVolume.is_initialized fan.to_FanVariableVolume.get end # Get existing heater availability schedule if present # or create a new one avail_sch = nil avail_sch_name = 'VestibuleHeaterAvailSch' if zone_hvac_component.model.getScheduleConstantByName(avail_sch_name).is_initialized avail_sch = zone_hvac_component.model.getScheduleConstantByName(avail_sch_name).get else avail_sch = OpenStudio::Model::ScheduleConstant.new(zone_hvac_component.model) avail_sch.setName(avail_sch_name) avail_sch.setValue(1) end # Replace the existing availabilty schedule with the one # that will be controlled via EMS htg_coil.setAvailabilitySchedule(avail_sch) fan.setAvailabilitySchedule(avail_sch) # Clean name of zone HVAC equip_name_clean = zone_hvac.name.get.to_s.gsub(/\W/, '').delete('_') # If the name starts with a number, prepend with a letter if equip_name_clean[0] =~ /[0-9]/ equip_name_clean = "EQUIP#{equip_name_clean}" end # Sensors # Get existing OAT sensor if present oat_db_c_sen = nil if zone_hvac_component.model.getEnergyManagementSystemSensorByName('OATVestibule').is_initialized oat_db_c_sen = zone_hvac_component.model.getEnergyManagementSystemSensorByName('OATVestibule').get else oat_db_c_sen = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Site Outdoor Air Drybulb Temperature') oat_db_c_sen.setName('OATVestibule') oat_db_c_sen.setKeyName('Environment') end # Actuators avail_sch_act = OpenStudio::Model::EnergyManagementSystemActuator.new(avail_sch, 'Schedule:Constant', 'Schedule Value') avail_sch_act.setName("#{equip_name_clean}VestHtgAvailSch") # Programs htg_lim_f = 45 vestibule_htg_prg = OpenStudio::Model::EnergyManagementSystemProgram.new(model) vestibule_htg_prg.setName("#{equip_name_clean}VestHtgPrg") vestibule_htg_prg_body = <<-EMS IF #{oat_db_c_sen.handle} > #{OpenStudio.convert(htg_lim_f, 'F', 'C').get} SET #{avail_sch_act.handle} = 0 ENDIF EMS vestibule_htg_prg.setBody(vestibule_htg_prg_body) # Program Calling Managers vestibule_htg_mgr = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) vestibule_htg_mgr.setName("#{equip_name_clean}VestHtgMgr") vestibule_htg_mgr.setCallingPoint('BeginTimestepBeforePredictor') vestibule_htg_mgr.addProgram(vestibule_htg_prg) OpenStudio.logFree(OpenStudio::Info, 'openstudio.standards.ZoneHVACComponent', "For #{zone_hvac_component.name}: Vestibule heating control applied, heating disabled below #{htg_lim_f} F.") return true end end