lib/measures/add_hpwh/measure.rb in openstudio-load-flexibility-measures-0.3.2 vs lib/measures/add_hpwh/measure.rb in openstudio-load-flexibility-measures-0.4.0
- old
+ new
@@ -1,645 +1,663 @@
-# *******************************************************************************
-# OpenStudio(R), Copyright (c) 2008-2020, Alliance for Sustainable Energy, LLC.
-# 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.
-# *******************************************************************************
-
-# Measure distributed under NREL Copyright terms, see LICENSE.md file.
-
-# Author: Karl Heine
-# Date: December 2019 - March 2020
-
-# References:
-# EnergyPlus InputOutput Reference, Sections:
-# EnergyPlus Engineering Reference, Sections:
-
-# start the measure
-class AddHphw < OpenStudio::Measure::ModelMeasure
- require 'openstudio-standards'
-
- # human readable name
- def name
- # Measure name should be the title case of the class name.
- 'Add HPWH for Domestic Hot Water'
- end
-
- # human readable description
- def description
- 'This measure adds or replaces existing domestic hot water heater with air source heat pump system and ' \
- 'allows for the addition of multiple daily flexible control time windows. The heater/tank system may ' \
- 'charge at maximum capacity up to an elevated temperature, or float without any heat addition for a ' \
- 'specified timeframe down to a minimum tank temperature.'
- end
-
- # human readable description of modeling approach
- def modeler_description
- return 'This measure allows selection between three heat pump water heater modeling approaches in EnergyPlus.' \
- 'The user may select between the pumped-condenser or wrapped-condenser objects. They may also elect to ' \
- 'use a simplified calculation which does not use the heat pump objects, but instead used an electric ' \
- 'resistance heater and approximates the equivalent electrical input that would be required from a heat ' \
- "pump. This expedites simulation at the expense of accuracy. \n" \
- 'The flexibility of the system is based on user-defined temperatures and times, which are converted into ' \
- 'schedule objects. There are four flexibility options. (1) None: normal operation of the DHW system at ' \
- 'a fixed tank temperature setpoint. (2) Charge - Heat Pump: the tank is charged to a maximum temperature ' \
- 'using only the heat pump. (3) Charge - Electric: the tank is charged using internal electric resistance ' \
- 'heaters to a maximum temperature. (4) Float: all heating elements are turned-off for a user-defined time ' \
- 'period unless the tank temperature falls below a minimum value. The heat pump will be prioritized in a ' \
- "low tank temperature event, with the electric resistance heaters serving as back-up. \n"
- 'Due to the heat pump interaction with zone conditioning as well as tank heating, users may experience ' \
- 'simulation errors if the heat pump is too large and placed in an already conditioned zoned. Try using ' \
- 'multiple smaller units, modifying the heat pump location within the model, or adjusting the zone thermo' \
- 'stat constraints. Use mulitiple instances of the measure to add multiple heat pump water heaters. '
- end
-
- ## USER ARGS ---------------------------------------------------------------------------------------------------------
- # define the arguments that the user will input
- def arguments(model)
- args = OpenStudio::Measure::OSArgumentVector.new
-
- # create argument for removal of existing water heater tanks on selected loop
- remove_wh = OpenStudio::Measure::OSArgument.makeBoolArgument('remove_wh', true)
- remove_wh.setDisplayName('Remove existing water heater?')
- remove_wh.setDescription('')
- remove_wh.setDefaultValue(true)
- args << remove_wh
-
- # find available water heaters and get default volume
- if !model.getWaterHeaterMixeds.empty?
- wheaters = model.getWaterHeaterMixeds
- end
-
- default_vol = 80.0 # gallons
- wh_names = ['All Water Heaters (Simplified Only)']
- wheaters.each do |w|
- if w.tankVolume.to_f > OpenStudio.convert(39, 'gal', 'm^3').to_f
- wh_names << w.name.to_s
- default_vol = [default_vol, (w.tankVolume.to_f / 0.0037854118).round(1)].max
- end
- end
-
- wh = OpenStudio::Measure::OSArgument.makeChoiceArgument('wh', wh_names, true)
- wh.setDisplayName('Select 40+ gallon water heater to replace or augment')
- wh.setDescription("All can only be used with the 'Simplified' model")
- wh.setDefaultValue(wh_names[0])
- args << wh
-
- # create argument for hot water tank volume
- vol = OpenStudio::Measure::OSArgument.makeDoubleArgument('vol', false)
- vol.setDisplayName('Set hot water tank volume [gal]')
- vol.setDescription('Enter 0 to use existing tank volume(s). Values less than 5 are treated as sizing multipliers.')
- vol.setUnits('gal')
- vol.setDefaultValue(0)
- args << vol
-
- # create argument for water heater type
- type = OpenStudio::Measure::OSArgument.makeChoiceArgument('type',
- ['Simplified', 'PumpedCondenser', 'WrappedCondenser'], true)
- type.setDisplayName('Select heat pump water heater type')
- type.setDescription('')
- type.setDefaultValue('Simplified')
- args << type
-
- # find available spaces for heater location
- zone_names = []
- unless model.getThermalZones.empty?
- zones = model.getThermalZones
- zones.each do |zn|
- zone_names << zn.name.to_s
- end
- zone_names.sort!
- end
-
- zone_names << 'Error: No Thermal Zones Found' if zone_names.empty?
- zone_names = ['N/A - Simplified'] + zone_names
-
- # create argument for thermal zone selection (location of water heater)
- zone = OpenStudio::Measure::OSArgument.makeChoiceArgument('zone', zone_names, true)
- zone.setDisplayName('Select thermal zone for HP evaporator')
- zone.setDescription("Does not apply to 'Simplified' cases")
- zone.setDefaultValue(zone_names[0])
- args << zone
-
- # create argument for heat pump capacity
- cap = OpenStudio::Measure::OSArgument.makeDoubleArgument('cap', true)
- cap.setDisplayName('Set heat pump heating capacity')
- cap.setDescription('[kW]')
- cap.setDefaultValue((23.446 * (default_vol / 80.0)).round(1))
- args << cap
-
- # create argument for heat pump rated cop
- cop = OpenStudio::Measure::OSArgument.makeDoubleArgument('cop', true)
- cop.setDisplayName('Set heat pump rated COP (heating)')
- cop.setDefaultValue(3.2)
- args << cop
-
- # create argument for electric backup capacity
- bu_cap = OpenStudio::Measure::OSArgument.makeDoubleArgument('bu_cap', true)
- bu_cap.setDisplayName('Set electric backup heating capacity')
- bu_cap.setDescription('[kW]')
- bu_cap.setDefaultValue((23.446 * (default_vol / 80.0)).round(1))
- args << bu_cap
-
- # create argument for maximum tank temperature
- max_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('max_temp', true)
- max_temp.setDisplayName('Set maximum tank temperature')
- max_temp.setDescription('[F]')
- max_temp.setDefaultValue(160)
- args << max_temp
-
- # create argument for minimum float temperature
- min_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('min_temp', true)
- min_temp.setDisplayName('Set minimum tank temperature during float')
- min_temp.setDescription('[F]')
- min_temp.setDefaultValue(120)
- args << min_temp
-
- # create argument for deadband temperature difference between heat pump setpoint and electric backup
- db_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('db_temp', true)
- db_temp.setDisplayName('Set deadband temperature difference between heat pump and electric backup')
- db_temp.setDescription('[F]')
- db_temp.setDefaultValue(5)
- args << db_temp
-
- # find existing temperature setpoint schedules for water heater
- all_scheds = model.getSchedules
- temp_sched_names = []
- default_sched = '--Create New @ 140F--'
- default_ambient = ''
- all_scheds.each do |sch|
- next if sch.scheduleTypeLimits.empty?
- next unless sch.scheduleTypeLimits.get.unitType.to_s == 'Temperature'
- temp_sched_names << sch.name.to_s
- if !wheaters.empty? && (sch.name.to_s == wheaters[0].setpointTemperatureSchedule.get.name.to_s)
- default_sched = sch.name.to_s
- end
- end
- temp_sched_names = [default_sched] + temp_sched_names.sort
-
- # create argument for predefined schedule
- sched = OpenStudio::Measure::OSArgument.makeChoiceArgument('sched', temp_sched_names, true)
- sched.setDisplayName('Select reference tank setpoint temperature schedule')
- sched.setDescription('')
- sched.setDefaultValue(temp_sched_names[0])
- args << sched
-
- # define possible flex options
- flex_options = ['None', 'Charge - Heat Pump', 'Charge - Electric', 'Float']
-
- # create choice and string arguments for flex periods
- 4.times do |n|
- flex = OpenStudio::Measure::OSArgument.makeChoiceArgument('flex' + n.to_s, flex_options, true)
- flex.setDisplayName("Daily Flex Period #{n + 1}:")
- flex.setDescription('Applies every day in the full run period.')
- flex.setDefaultValue('None')
- args << flex
-
- flex_hrs = OpenStudio::Measure::OSArgument.makeStringArgument('flex_hrs' + n.to_s, false)
- flex_hrs.setDisplayName('Use 24-Hour Format')
- flex_hrs.setDefaultValue('HH:MM - HH:MM')
- args << flex_hrs
- end
-
- args
- end
- ## END USER ARGS -----------------------------------------------------------------------------------------------------
-
- ## MEASURE RUN -------------------------------------------------------------------------------------------------------
- # Index:
- # => Argument Validation
- # => Controls: Heat Pump Heating Shedule
- # => Controls: Tank Electric Backup Heating Schedule
- # => Hardware
- # => Controls Modifications for Tank
- # => Report Output Variables
-
- # define what happens when the measure is run
- def run(model, runner, user_arguments)
- super(model, runner, user_arguments)
-
- ## ARGUMENT VALIDATION ---------------------------------------------------------------------------------------------
- # Measure does not immedately return false upon error detection. Errors are accumulated throughout this selection
- # before exiting gracefully prior to measure execution.
-
- # use the built-in error checking
- unless runner.validateUserArguments(arguments(model), user_arguments)
- return false
- end
-
- # report initial condition of model
- tanks_ic = model.getWaterHeaterMixeds.size + model.getWaterHeaterStratifieds.size
- hpwh_ic = model.getWaterHeaterHeatPumps.size + model.getWaterHeaterHeatPumpWrappedCondensers.size
- runner.registerInitialCondition("The building started with #{tanks_ic} water heater tank(s) and " \
- "#{hpwh_ic} heat pump water heater(s).")
-
- # create empty arrays and initialize variables for future use
- flex = []
- flex_type = []
- flex_hrs = []
- time_check = []
- hours = []
- minutes = []
- flex_times = []
-
- # assign the user inputs to variables
- remove_wh = runner.getBoolArgumentValue('remove_wh', user_arguments)
- wh = runner.getStringArgumentValue('wh', user_arguments)
- vol = runner.getDoubleArgumentValue('vol', user_arguments)
- type = runner.getStringArgumentValue('type', user_arguments)
- zone = runner.getStringArgumentValue('zone', user_arguments)
- cap = runner.getDoubleArgumentValue('cap', user_arguments)
- cop = runner.getDoubleArgumentValue('cop', user_arguments)
- bu_cap = runner.getDoubleArgumentValue('bu_cap', user_arguments)
- max_temp = runner.getDoubleArgumentValue('max_temp', user_arguments)
- min_temp = runner.getDoubleArgumentValue('min_temp', user_arguments)
- db_temp = runner.getDoubleArgumentValue('db_temp', user_arguments)
- sched = runner.getStringArgumentValue('sched', user_arguments)
-
- 4.times do |n|
- flex << runner.getStringArgumentValue('flex' + n.to_s, user_arguments)
- flex_hrs << runner.getStringArgumentValue('flex_hrs' + n.to_s, user_arguments)
- end
-
- # check capacity, volume, and temps for reasonableness
- if cap < 5
- runner.registerWarning('HPWH heating capacity is less than 5kW ( 17kBtu/hr)')
- end
-
- if bu_cap < 5
- runner.registerWarning('Backup heating capaicty is less than 5kW ( 17kBtu/hr).')
- end
-
- if vol == 0
- runner.registerInfo('Tank volume was not specified, using existing tank capacity.')
- elsif vol < 40
- runner.registerWarning('Tank has less than 40 gallon capacity; check heat pump sizing if model fails.')
- end
-
- if min_temp < 120
- runner.registerWarning('Minimum tank temperature is very low; consider increasing to at least 120F.')
- runner.registerWarning('Do not store water for long periods at temperatures below 135-140F as those ' \
- 'conditions facilitate the growth of Legionella.')
- end
-
- if max_temp > 185
- runner.registerWarning('Maximum charging temperature exceeded practical limits; reset to 185F.')
- max_temp = 185.0
- end
-
- if max_temp > 170
- runner.registerWarning("#{max_temp}F is above or near the limit of the HP performance curves. If the " \
- 'simulation fails with cooling capacity less than 0, you have exceeded performance ' \
- 'limits. Consider setting max temp to less than 170F.')
- end
-
- # check selected schedule and set flag for later use
- sched_flag = false # flag for either creating new (false) or modifying existing (true) schedule
- if sched == '--Create New @ 140F--'
- runner.registerInfo('No reference water heater temperature setpoint schedule was selected; a new one ' \
- 'will be created.')
- else
- sched_flag = true
- runner.registerInfo("#{sched} will be used as the water heater temperature setpoint schedule.")
- end
-
- # parse flex_hrs into hours and minuts arrays
- idx = 0
- flex_hrs.each do |fh|
- if flex[idx] != 'None'
- data = fh.split(/[-:]/)
- data.each { |e| e.delete!(' ') }
- if data[2] > data[0]
- flex_type << flex[idx]
- hours << data[0]
- hours << data[2]
- minutes << data[1]
- minutes << data[3]
- else
- flex_type << flex[idx]
- flex_type << flex[idx]
- hours << 0
- hours << data[2]
- hours << data[0]
- hours << 24
- minutes << 0
- minutes << data[3]
- minutes << data[1]
- minutes << 0
- end
- end
- idx += 1
- end
-
- # convert hours and minutes into OS:Time objects
- idx = 0
- hours.each do |h|
- flex_times << OpenStudio::Time.new(0, h.to_i, minutes[idx].to_i, 0)
- idx += 1
- end
-
- # flex.delete('None')
-
- runner.registerInfo("A total of #{idx / 2} flex periods will be added to the selected water heater setpoint schedule.")
-
- # exit gracefully if errors registered above
- return false unless runner.result.errors.empty?
- ## END ARGUMENT VALIDATION -----------------------------------------------------------------------------------------
-
- ## CONTROLS: HEAT PUMP HEATING TEMPERATURE SETPOINT SCHEDULE -------------------------------------------------------
- # This section creates the heat pump heating temperature setpoint schedule with flex periods
- # The tank schedule is created here
-
- # find or create new reference temperature schedule based on sched_flag value
- if sched_flag # schedule already exists and must be modified
- # converts the STRING into a MODEL OBJECT, same variable name
- sched = model.getScheduleRulesetByName(sched).get.clone.to_ScheduleRuleset.get
- else
- # must create new water heater setpoint temperature schedule at 140F
- sched = OpenStudio::Model::ScheduleRuleset.new(model, 60)
- end
-
- # rename and duplicate for later modification
- sched.setName('Heat Pump Heating Temperature Setpoint')
- sched.defaultDaySchedule.setName('Heat Pump Heating Temperature Setpoint Default')
-
- # tank_sched = sched.clone.to_ScheduleRuleset.get
- tank_sched = OpenStudio::Model::ScheduleRuleset.new(model, 60 - (db_temp / 1.8 + 2))
- tank_sched.setName('Tank Electric Heater Setpoint')
- tank_sched.defaultDaySchedule.setName('Tank Electric Heater Setpoint Default')
-
- # grab default day and time-value pairs for modification
- d_day = sched.defaultDaySchedule
- old_times = d_day.times
- old_values = d_day.values
- new_values = Array.new(flex_times.size, 2)
-
- # find existing values in reference schedule and grab for use in new-rule creation
- flex_times.size.times do |i|
- if i.even?
- n = 0
- old_times.each do |ot|
- new_values[i] = old_values[n] if flex_times[i] <= ot
- n += 1
- end
- elsif flex_type[(i / 2).floor] == 'Charge - Heat Pump'
- new_values[i] = OpenStudio.convert(max_temp, 'F', 'C').get
- elsif flex_type[(i / 2).floor] == 'Float' || flex_type[(i / 2).floor] == 'Charge - Electric'
- new_values[i] = OpenStudio.convert(min_temp, 'F', 'C').get
- end
- end
-
- # create new rules and add to default day based on flex period options above
- idx = 0
- flex_times.each do |ft|
- d_day.addValue(ft, new_values[idx])
- idx += 1
- end
-
- ## END CONTROLS: HEAT PUMP HEATING TEMPERATURE SETPOINT SCHEDULE ---------------------------------------------------
-
- ## CONTROLS: TANK TEMPERATURE SETPOINT SCHEDULE (ELECTRIC BACKUP) --------------------------------------------------
- # This section creates the setpoint temperature schedule for the electric backup heating coils in the water tank
-
- # grab default day and time-value pairs for modification
- d_day = tank_sched.defaultDaySchedule
- old_times = d_day.times
- old_values = d_day.values
- new_values = Array.new(flex_times.size, 2)
-
- # find existing values in reference schedule and grab for use in new-rule creation
- flex_times.size.times do |i|
- if i.even?
- n = 0
- old_times.each do |ot|
- new_values[i] = old_values[n] if flex_times[i] <= ot
- n += 1
- end
- elsif flex_type[(i / 2).floor] == 'Charge - Electric'
- new_values[i] = OpenStudio.convert(max_temp, 'F', 'C').get
- elsif flex_type[(i / 2).floor] == 'Float' # || flex_type[(i/2).floor] == 'Charge - Heat Pump'
- new_values[i] = OpenStudio.convert(min_temp - db_temp, 'F', 'C').get
- elsif flex_type[(i / 2).floor] == 'Charge - Heat Pump'
- new_values[i] = 60 - (db_temp / 1.8)
- end
- end
-
- # create new rules and add to default day based on flex period options above
- idx = 0
- flex_times.each do |ft|
- d_day.addValue(ft, new_values[idx])
- idx += 1
- end
-
- ## END CONTROLS: TANK TEMPERATURE SETPOINT SCHEDULE (ELECTRIC BACKUP) ----------------------------------------------
-
- ## HARDWARE --------------------------------------------------------------------------------------------------------
- # This section adds the selected type of heat pump water heater to the supply side of the selected loop. If
- # selected, measure will remove any existing water heaters on the supply side of the loop. If old heater(s) are left
- # in place, the new HPWH tank will be placed in front (to the left) of them.
-
- # use OS standards build - arbitrary selection, but NZE Ready seems appropriate
- std = Standard.build('NREL ZNE Ready 2017')
-
- #####
- # get the selected water heaters
- whtrs = []
- model.getWaterHeaterMixeds.each do |w|
- if wh == 'All Water Heaters (Simplified Only)'
- # exclude booster tanks (<10gal):
- if w.tankVolume.to_f < 0.037854
- next
- else
- whtrs << w
- end
- elsif w.name.to_s == wh
- whtrs << w
- end
- end
-
- whtrs.each do |wh|
- # create empty arrays and initialize variables for later use
- old_heater = []
- count = 0
-
- # get the appropriate plant loop
- loop = ''
- loops = model.getPlantLoops
- loops.each do |l|
- l.supplyComponents.each do |c|
- if c.name.to_s == wh.name.to_s
- loop = l
- end
- end
- end
-
- # use existing tank volume unless otherwise specified
- # values between 0.0 and 5.0 are considered tank sizing multipliers
- if vol == 0
- v = wh.tankVolume
- elsif (vol > 0.0) && (vol < 5.0)
- v = wh.tankVolume.to_f * vol
- else
- v = OpenStudio.convert(vol, 'gal', 'm^3').get
- end
-
- inlet = wh.supplyInletModelObject.get.to_Node.get
- outlet = wh.supplyOutletModelObject.get.to_Node.get
-
- # Add heat pump water heater and attach to selected loop
- # Reference: https://github.com/NREL/openstudio-standards/blob/master/lib/
- # => openstudio-standards/prototypes/common/objects/Prototype.ServiceWaterHeating.rb
- if type != 'Simplified'
- # convert zone name from STRING into OS model OBJECT
- zone = model.getThermalZoneByName(zone).get
- hpwh = std.model_add_heatpump_water_heater(model, # model
- type: type, # type
- water_heater_capacity: (cap * 1000 / cop), # water_heater_capacity
- electric_backup_capacity: (bu_cap * 1000), # electric_backup_capacity
- water_heater_volume: v, # water_heater_volume
- service_water_temperature: OpenStudio.convert(140.0, 'F', 'C').get, # service_water_temperature
- parasitic_fuel_consumption_rate: 3.0, # parasitic_fuel_consumption_rate
- swh_temp_sch: sched, # swh_temp_sch
- cop: cop, # cop
- shr: 0.88, # shr
- tank_ua: 3.9, # tank_ua
- set_peak_use_flowrate: false, # set_peak_use_flowrate
- peak_flowrate: 0.0, # peak_flowrate
- flowrate_schedule: nil, # flowrate_schedule
- water_heater_thermal_zone: zone) # water_heater_thermal_zone
- else
- # zone = wh.ambientTemperatureThermalZone.get
- runner.registerWarning(wh.to_s)
- hpwh = std.model_add_water_heater(model, # model
- (cap * 1000), # water_heater_capacity
- v.to_f, # water_heater_volume
- 'HeatPump', # water_heater_fuel
- OpenStudio.convert(140.0, 'F', 'C').to_f, # service_water_temperature
- 3.0, # parasitic_fuel_consumption_rate
- sched, # swh_temp_sch
- false, # set_peak_use_flowrate
- 0.0, # peak_flowrate
- nil, # flowrate_schedule
- model.getThermalZones[0], # water_heater_thermal_zone
- 1) # number_water_heaters
- # set COP in PLF curve
- cop_curve = hpwh.partLoadFactorCurve.get
- cop_curve.setName(cop_curve.name.get.gsub('2.8', cop.to_s))
- cop_curve.setCoefficient1Constant(cop)
- end
-
- # add tank to appropriate branch and node (will be placed first in series if old tanks not removed)
- # modify objects as ncessary
- if type != 'Simplified'
- hpwh.tank.addToNode(inlet)
- hpwh.setDeadBandTemperatureDifference(db_temp / 1.8)
- runner.registerInfo("#{hpwh.tank.name} was added to the model on #{loop.name}")
- else
- hpwh.addToNode(inlet)
- hpwh.setMaximumTemperatureLimit(OpenStudio.convert(max_temp, 'F', 'C').get)
- runner.registerInfo("#{hpwh.name} was added to the model on #{loop.name}")
- end
-
- # remove old tank objects if necessary
- if remove_wh
- runner.registerInfo("#{wh.name} was removed from the model.")
- wh.remove
- end
-
- # CONTROLS MODIFICATIONS FOR TANK ---------------------------------------------------------------------------------
- # apply schedule to tank
- if type == 'PumpedCondenser'
- hpwh.tank.to_WaterHeaterMixed.get.setSetpointTemperatureSchedule(tank_sched)
- elsif type == 'WrappedCondenser'
- hpwh.tank.to_WaterHeaterStratified.get.setHeater1SetpointTemperatureSchedule(tank_sched)
- hpwh.tank.to_WaterHeaterStratified.get.setHeater2SetpointTemperatureSchedule(tank_sched)
- end
- # END CONTROLS MODIFICATIONS FOR TANK -----------------------------------------------------------------------------
- end
- ## END HARDWARE ----------------------------------------------------------------------------------------------------
-
- ## ADD REPORTED VARIABLES ------------------------------------------------------------------------------------------
-
- ovar_names = ['Cooling Coil Total Cooling Rate',
- 'Cooling Coil Total Water Heating Rate',
- 'Cooling Coil Water Heating Electric Power',
- 'Cooling Coil Crankcase Heater Electric Power',
- 'Water Heater Tank Temperature',
- 'Water Heater Heat Loss Rate',
- 'Water Heater Heating Rate',
- 'Water Heater Use Side Heat Transfer Rate',
- 'Water Heater Source Side Heat Transfer Rate',
- 'Water Heater Unmet Demand Heat Transfer Rate',
- 'Water Heater Electricity Rate',
- 'Water Heater Water Volume Flow Rate',
- 'Water Use Connections Hot Water Temperature']
-
- # Create new output variable objects
- ovars = []
- ovar_names.each do |nm|
- ovars << OpenStudio::Model::OutputVariable.new(nm, model)
- end
-
- # add temperate schedule outputs - clean up and put names into array, then loop over setting key values
- v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
- v.setKeyValue(sched.name.to_s)
- ovars << v
-
- v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
- v.setKeyValue(tank_sched.name.to_s)
- ovars << v
-
- if type != 'Simplified'
- v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
- v.setKeyValue(tank_sched.name.to_s)
- ovars << v
- end
-
- # Set variable reporting frequency for newly created output variables
- ovars.each do |var|
- var.setReportingFrequency('TimeStep')
- end
-
- # Register info re: output variables:
- runner.registerInfo("#{ovars.size} output variables were added to the model.")
- ## END ADD REPORTED VARIABLES --------------------------------------------------------------------------------------
-
- # Register final condition
- hpwh_fc = model.getWaterHeaterHeatPumps.size + model.getWaterHeaterHeatPumpWrappedCondensers.size
- tanks_fc = model.getWaterHeaterMixeds.size + model.getWaterHeaterStratifieds.size
- runner.registerFinalCondition("The building finshed with #{tanks_fc} water heater tank(s) and " \
- "#{hpwh_fc} heat pump water heater(s).")
-
- true
- end
-end
-
-# register the measure to be used by the application
-AddHphw.new.registerWithApplication
+# *******************************************************************************
+# OpenStudio(R), Copyright (c) 2008-2021, Alliance for Sustainable Energy, LLC.
+# 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.
+# *******************************************************************************
+
+# Measure distributed under NREL Copyright terms, see LICENSE.md file.
+
+# Author: Karl Heine
+# Date: December 2019 - March 2020
+
+# References:
+# EnergyPlus InputOutput Reference, Sections:
+# EnergyPlus Engineering Reference, Sections:
+
+# start the measure
+class AddHpwh < OpenStudio::Measure::ModelMeasure
+ require 'openstudio-standards'
+
+ # human readable name
+ def name
+ # Measure name should be the title case of the class name.
+ 'Add HPWH for Domestic Hot Water'
+ end
+
+ # human readable description
+ def description
+ 'This measure adds or replaces existing domestic hot water heater with air source heat pump system and ' \
+ 'allows for the addition of multiple daily flexible control time windows. The heater/tank system may ' \
+ 'charge at maximum capacity up to an elevated temperature, or float without any heat addition for a ' \
+ 'specified timeframe down to a minimum tank temperature.'
+ end
+
+ # human readable description of modeling approach
+ def modeler_description
+ return 'This measure allows selection between three heat pump water heater modeling approaches in EnergyPlus.' \
+ 'The user may select between the pumped-condenser or wrapped-condenser objects. They may also elect to ' \
+ 'use a simplified calculation which does not use the heat pump objects, but instead used an electric ' \
+ 'resistance heater and approximates the equivalent electrical input that would be required from a heat ' \
+ "pump. This expedites simulation at the expense of accuracy. \n" \
+ 'The flexibility of the system is based on user-defined temperatures and times, which are converted into ' \
+ 'schedule objects. There are four flexibility options. (1) None: normal operation of the DHW system at ' \
+ 'a fixed tank temperature setpoint. (2) Charge - Heat Pump: the tank is charged to a maximum temperature ' \
+ 'using only the heat pump. (3) Charge - Electric: the tank is charged using internal electric resistance ' \
+ 'heaters to a maximum temperature. (4) Float: all heating elements are turned-off for a user-defined time ' \
+ 'period unless the tank temperature falls below a minimum value. The heat pump will be prioritized in a ' \
+ "low tank temperature event, with the electric resistance heaters serving as back-up. \n"
+ 'Due to the heat pump interaction with zone conditioning as well as tank heating, users may experience ' \
+ 'simulation errors if the heat pump is too large and placed in an already conditioned zoned. Try using ' \
+ 'multiple smaller units, modifying the heat pump location within the model, or adjusting the zone thermo' \
+ 'stat constraints. Use mulitiple instances of the measure to add multiple heat pump water heaters. '
+ end
+
+ ## USER ARGS ---------------------------------------------------------------------------------------------------------
+ # define the arguments that the user will input
+ def arguments(model)
+ args = OpenStudio::Measure::OSArgumentVector.new
+
+ # create argument for removal of existing water heater tanks on selected loop
+ remove_wh = OpenStudio::Measure::OSArgument.makeBoolArgument('remove_wh', true)
+ remove_wh.setDisplayName('Remove existing water heater?')
+ remove_wh.setDescription('')
+ remove_wh.setDefaultValue(true)
+ args << remove_wh
+
+ # find available water heaters and get default volume
+ default_vol = 80.0 # gallons
+ wh_names = ['All Water Heaters (Simplified Only)']
+ if !model.getWaterHeaterMixeds.empty?
+ wheaters = model.getWaterHeaterMixeds
+ wheaters.each do |w|
+ if w.tankVolume.to_f > OpenStudio.convert(39, 'gal', 'm^3').to_f
+ wh_names << w.name.to_s
+ default_vol = [default_vol, (w.tankVolume.to_f / 0.0037854118).round(1)].max
+ end
+ end
+ end
+
+ wh = OpenStudio::Measure::OSArgument.makeChoiceArgument('wh', wh_names, true)
+ wh.setDisplayName('Select 40+ gallon water heater to replace or augment')
+ wh.setDescription("All can only be used with the 'Simplified' model")
+ wh.setDefaultValue(wh_names[0])
+ args << wh
+
+ # create argument for hot water tank volume
+ vol = OpenStudio::Measure::OSArgument.makeDoubleArgument('vol', false)
+ vol.setDisplayName('Set hot water tank volume [gal]')
+ vol.setDescription('Enter 0 to use existing tank volume(s). Values less than 5 are treated as sizing multipliers.')
+ vol.setUnits('gal')
+ vol.setDefaultValue(0)
+ args << vol
+
+ # create argument for water heater type
+ type = OpenStudio::Measure::OSArgument.makeChoiceArgument('type',
+ ['Simplified', 'PumpedCondenser', 'WrappedCondenser'], true)
+ type.setDisplayName('Select heat pump water heater type')
+ type.setDescription('')
+ type.setDefaultValue('Simplified')
+ args << type
+
+ # find available spaces for heater location
+ zone_names = []
+ unless model.getThermalZones.empty?
+ zones = model.getThermalZones
+ zones.each do |zn|
+ zone_names << zn.name.to_s
+ end
+ zone_names.sort!
+ end
+
+ zone_names << 'Error: No Thermal Zones Found' if zone_names.empty?
+ zone_names = ['N/A - Simplified'] + zone_names
+
+ # create argument for thermal zone selection (location of water heater)
+ zone = OpenStudio::Measure::OSArgument.makeChoiceArgument('zone', zone_names, true)
+ zone.setDisplayName('Select thermal zone for HP evaporator')
+ zone.setDescription("Does not apply to 'Simplified' cases")
+ zone.setDefaultValue(zone_names[0])
+ args << zone
+
+ # create argument for heat pump capacity
+ cap = OpenStudio::Measure::OSArgument.makeDoubleArgument('cap', true)
+ cap.setDisplayName('Set heat pump heating capacity')
+ cap.setDescription('[kW]')
+ cap.setDefaultValue((23.446 * (default_vol / 80.0)).round(1))
+ args << cap
+
+ # create argument for heat pump rated cop
+ cop = OpenStudio::Measure::OSArgument.makeDoubleArgument('cop', true)
+ cop.setDisplayName('Set heat pump rated COP (heating)')
+ cop.setDefaultValue(3.2)
+ args << cop
+
+ # create argument for electric backup capacity
+ bu_cap = OpenStudio::Measure::OSArgument.makeDoubleArgument('bu_cap', true)
+ bu_cap.setDisplayName('Set electric backup heating capacity')
+ bu_cap.setDescription('[kW]')
+ bu_cap.setDefaultValue((23.446 * (default_vol / 80.0)).round(1))
+ args << bu_cap
+
+ # create argument for maximum tank temperature
+ max_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('max_temp', true)
+ max_temp.setDisplayName('Set maximum tank temperature')
+ max_temp.setDescription('[F]')
+ max_temp.setDefaultValue(160)
+ args << max_temp
+
+ # create argument for minimum float temperature
+ min_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('min_temp', true)
+ min_temp.setDisplayName('Set minimum tank temperature during float')
+ min_temp.setDescription('[F]')
+ min_temp.setDefaultValue(120)
+ args << min_temp
+
+ # create argument for deadband temperature difference between heat pump setpoint and electric backup
+ db_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('db_temp', true)
+ db_temp.setDisplayName('Set deadband temperature difference between heat pump and electric backup')
+ db_temp.setDescription('[F]')
+ db_temp.setDefaultValue(5)
+ args << db_temp
+
+ # find existing temperature setpoint schedules for water heater
+ all_scheds = model.getSchedules
+ temp_sched_names = []
+ default_sched = '--Create New @ 140F--'
+ default_ambient = ''
+ all_scheds.each do |sch|
+ next if sch.scheduleTypeLimits.empty?
+ next unless sch.scheduleTypeLimits.get.unitType.to_s == 'Temperature'
+
+ temp_sched_names << sch.name.to_s
+ if !wheaters.empty? && (sch.name.to_s == wheaters[0].setpointTemperatureSchedule.get.name.to_s)
+ default_sched = sch.name.to_s
+ end
+ end
+ temp_sched_names = [default_sched] + temp_sched_names.sort
+
+ # create argument for predefined schedule
+ sched = OpenStudio::Measure::OSArgument.makeChoiceArgument('sched', temp_sched_names, true)
+ sched.setDisplayName('Select reference tank setpoint temperature schedule')
+ sched.setDescription('')
+ sched.setDefaultValue(temp_sched_names[0])
+ args << sched
+
+ # define possible flex options
+ flex_options = ['None', 'Charge - Heat Pump', 'Charge - Electric', 'Float']
+
+ # create choice and string arguments for flex periods
+ 4.times do |n|
+ flex = OpenStudio::Measure::OSArgument.makeChoiceArgument("flex#{n}", flex_options, true)
+ flex.setDisplayName("Daily Flex Period #{n + 1}:")
+ flex.setDescription('Applies every day in the full run period.')
+ flex.setDefaultValue('None')
+ args << flex
+
+ flex_hrs = OpenStudio::Measure::OSArgument.makeStringArgument("flex_hrs#{n}", false)
+ flex_hrs.setDisplayName('Use 24-Hour Format')
+ flex_hrs.setDefaultValue('HH:MM - HH:MM')
+ args << flex_hrs
+ end
+
+ args
+ end
+ ## END USER ARGS -----------------------------------------------------------------------------------------------------
+
+ ## MEASURE RUN -------------------------------------------------------------------------------------------------------
+ # Index:
+ # => Argument Validation
+ # => Controls: Heat Pump Heating Shedule
+ # => Controls: Tank Electric Backup Heating Schedule
+ # => Hardware
+ # => Controls Modifications for Tank
+ # => Report Output Variables
+
+ # define what happens when the measure is run
+ def run(model, runner, user_arguments)
+ super(model, runner, user_arguments)
+
+ ## ARGUMENT VALIDATION ---------------------------------------------------------------------------------------------
+ # Measure does not immedately return false upon error detection. Errors are accumulated throughout this selection
+ # before exiting gracefully prior to measure execution.
+
+ # use the built-in error checking
+ unless runner.validateUserArguments(arguments(model), user_arguments)
+ return false
+ end
+
+ # report initial condition of model
+ tanks_ic = model.getWaterHeaterMixeds.size + model.getWaterHeaterStratifieds.size
+ hpwh_ic = model.getWaterHeaterHeatPumps.size + model.getWaterHeaterHeatPumpWrappedCondensers.size
+ runner.registerInitialCondition("The building started with #{tanks_ic} water heater tank(s) and " \
+ "#{hpwh_ic} heat pump water heater(s).")
+
+ # create empty arrays and initialize variables for future use
+ flex = []
+ flex_type = []
+ flex_hrs = []
+ time_check = []
+ hours = []
+ minutes = []
+ flex_times = []
+
+ # assign the user inputs to variables
+ remove_wh = runner.getBoolArgumentValue('remove_wh', user_arguments)
+ wh = runner.getStringArgumentValue('wh', user_arguments)
+ vol = runner.getDoubleArgumentValue('vol', user_arguments)
+ type = runner.getStringArgumentValue('type', user_arguments)
+ zone = runner.getStringArgumentValue('zone', user_arguments)
+ cap = runner.getDoubleArgumentValue('cap', user_arguments)
+ cop = runner.getDoubleArgumentValue('cop', user_arguments)
+ bu_cap = runner.getDoubleArgumentValue('bu_cap', user_arguments)
+ max_temp = runner.getDoubleArgumentValue('max_temp', user_arguments)
+ min_temp = runner.getDoubleArgumentValue('min_temp', user_arguments)
+ db_temp = runner.getDoubleArgumentValue('db_temp', user_arguments)
+ sched = runner.getStringArgumentValue('sched', user_arguments)
+
+ 4.times do |n|
+ flex << runner.getStringArgumentValue("flex#{n}", user_arguments)
+ flex_hrs << runner.getStringArgumentValue("flex_hrs#{n}", user_arguments)
+ end
+
+ # check for existence of water heaters (if "all" is selected)
+ if model.getWaterHeaterMixeds.empty?
+ runner.registerError('No water heaters found in the model')
+ return false
+ end
+
+ # Alert user to "simplified" selection
+ if type == 'Simplified'
+ runner.registerInfo('NOTE: The simplified model is used, so heat pump objects are not employed.')
+ end
+
+ # check capacity, volume, and temps for reasonableness
+ if cap < 5
+ runner.registerWarning('HPWH heating capacity is less than 5kW ( 17kBtu/hr)')
+ end
+
+ if bu_cap < 5
+ runner.registerWarning('Backup heating capaicty is less than 5kW ( 17kBtu/hr).')
+ end
+
+ if vol == 0
+ runner.registerInfo('Tank volume was not specified, using existing tank capacity.')
+ elsif vol < 40
+ runner.registerWarning('Tank has less than 40 gallon capacity; check heat pump sizing if model fails.')
+ end
+
+ if min_temp < 120
+ runner.registerWarning('Minimum tank temperature is very low; consider increasing to at least 120F.')
+ runner.registerWarning('Do not store water for long periods at temperatures below 135-140F as those ' \
+ 'conditions facilitate the growth of Legionella.')
+ end
+
+ if max_temp > 185
+ runner.registerWarning('Maximum charging temperature exceeded practical limits; reset to 185F.')
+ max_temp = 185.0
+ end
+
+ if max_temp > 170
+ runner.registerWarning("#{max_temp}F is above or near the limit of the HP performance curves. If the " \
+ 'simulation fails with cooling capacity less than 0, you have exceeded performance ' \
+ 'limits. Consider setting max temp to less than 170F.')
+ end
+
+ # check selected schedule and set flag for later use
+ sched_flag = false # flag for either creating new (false) or modifying existing (true) schedule
+ if sched == '--Create New @ 140F--'
+ runner.registerInfo('No reference water heater temperature setpoint schedule was selected; a new one ' \
+ 'will be created.')
+ else
+ sched_flag = true
+ runner.registerInfo("#{sched} will be used as the water heater temperature setpoint schedule.")
+ end
+
+ # parse flex_hrs into hours and minuts arrays
+ idx = 0
+ flex_hrs.each do |fh|
+ if flex[idx] != 'None'
+ data = fh.split(/[-:]/)
+ data.each { |e| e.delete!(' ') }
+ if data[2] > data[0]
+ flex_type << flex[idx]
+ hours << data[0]
+ hours << data[2]
+ minutes << data[1]
+ minutes << data[3]
+ else
+ flex_type << flex[idx]
+ flex_type << flex[idx]
+ hours << 0
+ hours << data[2]
+ hours << data[0]
+ hours << 24
+ minutes << 0
+ minutes << data[3]
+ minutes << data[1]
+ minutes << 0
+ end
+ end
+ idx += 1
+ end
+
+ # convert hours and minutes into OS:Time objects
+ idx = 0
+ hours.each do |h|
+ flex_times << OpenStudio::Time.new(0, h.to_i, minutes[idx].to_i, 0)
+ idx += 1
+ end
+
+ # flex.delete('None')
+
+ runner.registerInfo("A total of #{idx / 2} flex periods will be added to the selected water heater setpoint schedule.")
+
+ # exit gracefully if errors registered above
+ return false unless runner.result.errors.empty?
+
+ ## END ARGUMENT VALIDATION -----------------------------------------------------------------------------------------
+
+ ## CONTROLS: HEAT PUMP HEATING TEMPERATURE SETPOINT SCHEDULE -------------------------------------------------------
+ # This section creates the heat pump heating temperature setpoint schedule with flex periods
+ # The tank schedule is created here
+
+ # find or create new reference temperature schedule based on sched_flag value
+ if sched_flag # schedule already exists and must be modified
+ # converts the STRING into a MODEL OBJECT, same variable name
+ sched = model.getScheduleRulesetByName(sched).get.clone.to_ScheduleRuleset.get
+ else
+ # must create new water heater setpoint temperature schedule at 140F
+ sched = OpenStudio::Model::ScheduleRuleset.new(model, 60)
+ end
+
+ # rename and duplicate for later modification
+ sched.setName('Heat Pump Heating Temperature Setpoint')
+ sched.defaultDaySchedule.setName('Heat Pump Heating Temperature Setpoint Default')
+
+ # tank_sched = sched.clone.to_ScheduleRuleset.get
+ tank_sched = OpenStudio::Model::ScheduleRuleset.new(model, 60 - (db_temp / 1.8 + 2))
+ tank_sched.setName('Tank Electric Heater Setpoint')
+ tank_sched.defaultDaySchedule.setName('Tank Electric Heater Setpoint Default')
+
+ # grab default day and time-value pairs for modification
+ d_day = sched.defaultDaySchedule
+ old_times = d_day.times
+ old_values = d_day.values
+ new_values = Array.new(flex_times.size, 2)
+
+ # find existing values in reference schedule and grab for use in new-rule creation
+ flex_times.size.times do |i|
+ if i.even?
+ n = 0
+ old_times.each do |ot|
+ new_values[i] = old_values[n] if flex_times[i] <= ot
+ n += 1
+ end
+ elsif flex_type[(i / 2).floor] == 'Charge - Heat Pump'
+ new_values[i] = OpenStudio.convert(max_temp, 'F', 'C').get
+ elsif flex_type[(i / 2).floor] == 'Float' || flex_type[(i / 2).floor] == 'Charge - Electric'
+ new_values[i] = OpenStudio.convert(min_temp, 'F', 'C').get
+ end
+ end
+
+ # create new rules and add to default day based on flex period options above
+ idx = 0
+ flex_times.each do |ft|
+ d_day.addValue(ft, new_values[idx])
+ idx += 1
+ end
+
+ ## END CONTROLS: HEAT PUMP HEATING TEMPERATURE SETPOINT SCHEDULE ---------------------------------------------------
+
+ ## CONTROLS: TANK TEMPERATURE SETPOINT SCHEDULE (ELECTRIC BACKUP) --------------------------------------------------
+ # This section creates the setpoint temperature schedule for the electric backup heating coils in the water tank
+
+ # grab default day and time-value pairs for modification
+ d_day = tank_sched.defaultDaySchedule
+ old_times = d_day.times
+ old_values = d_day.values
+ new_values = Array.new(flex_times.size, 2)
+
+ # find existing values in reference schedule and grab for use in new-rule creation
+ flex_times.size.times do |i|
+ if i.even?
+ n = 0
+ old_times.each do |ot|
+ new_values[i] = old_values[n] if flex_times[i] <= ot
+ n += 1
+ end
+ elsif flex_type[(i / 2).floor] == 'Charge - Electric'
+ new_values[i] = OpenStudio.convert(max_temp, 'F', 'C').get
+ elsif flex_type[(i / 2).floor] == 'Float' # || flex_type[(i/2).floor] == 'Charge - Heat Pump'
+ new_values[i] = OpenStudio.convert(min_temp - db_temp, 'F', 'C').get
+ elsif flex_type[(i / 2).floor] == 'Charge - Heat Pump'
+ new_values[i] = 60 - (db_temp / 1.8)
+ end
+ end
+
+ # create new rules and add to default day based on flex period options above
+ idx = 0
+ flex_times.each do |ft|
+ d_day.addValue(ft, new_values[idx])
+ idx += 1
+ end
+
+ ## END CONTROLS: TANK TEMPERATURE SETPOINT SCHEDULE (ELECTRIC BACKUP) ----------------------------------------------
+
+ ## HARDWARE --------------------------------------------------------------------------------------------------------
+ # This section adds the selected type of heat pump water heater to the supply side of the selected loop. If
+ # selected, measure will remove any existing water heaters on the supply side of the loop. If old heater(s) are left
+ # in place, the new HPWH tank will be placed in front (to the left) of them.
+
+ # use OS standards build - arbitrary selection, but NZE Ready seems appropriate
+ std = Standard.build('NREL ZNE Ready 2017')
+
+ #####
+ # get the selected water heaters
+ whtrs = []
+ model.getWaterHeaterMixeds.each do |w|
+ case wh
+ when 'All Water Heaters (Simplified Only)'
+ # exclude booster tanks (<10gal):
+ if w.tankVolume.to_f < 0.037854
+ next
+ else
+ whtrs << w
+ end
+ when w.name.to_s
+ whtrs << w
+ end
+ end
+
+ whtrs.each do |whtr|
+ # create empty arrays and initialize variables for later use
+ old_heater = []
+ count = 0
+
+ # get the appropriate plant loop
+ loop = ''
+ loops = model.getPlantLoops
+ loops.each do |l|
+ l.supplyComponents.each do |c|
+ if c.name.to_s == whtr.name.to_s
+ loop = l
+ end
+ end
+ end
+
+ # use existing tank volume unless otherwise specified
+ # values between 0.0 and 5.0 are considered tank sizing multipliers
+ if vol == 0
+ v = whtr.tankVolume
+ elsif (vol > 0.0) && (vol < 5.0)
+ v = whtr.tankVolume.to_f * vol
+ else
+ v = OpenStudio.convert(vol, 'gal', 'm^3').get
+ end
+
+ inlet = whtr.supplyInletModelObject.get.to_Node.get
+ outlet = whtr.supplyOutletModelObject.get.to_Node.get
+
+ # Add heat pump water heater and attach to selected loop
+ # Reference: https://github.com/NREL/openstudio-standards/blob/master/lib/
+ # => openstudio-standards/prototypes/common/objects/Prototype.ServiceWaterHeating.rb
+ if type != 'Simplified'
+ # convert zone name from STRING into OS model OBJECT
+ zone = model.getThermalZoneByName(zone).get
+ hpwh = std.model_add_heatpump_water_heater(model, # model
+ type: type, # type
+ water_heater_capacity: (cap * 1000 / cop), # water_heater_capacity
+ electric_backup_capacity: (bu_cap * 1000), # electric_backup_capacity
+ water_heater_volume: v, # water_heater_volume
+ service_water_temperature: OpenStudio.convert(140.0, 'F', 'C').get, # service_water_temperature
+ parasitic_fuel_consumption_rate: 3.0, # parasitic_fuel_consumption_rate
+ swh_temp_sch: sched, # swh_temp_sch
+ cop: cop, # cop
+ shr: 0.88, # shr
+ tank_ua: 3.9, # tank_ua
+ set_peak_use_flowrate: false, # set_peak_use_flowrate
+ peak_flowrate: 0.0, # peak_flowrate
+ flowrate_schedule: nil, # flowrate_schedule
+ water_heater_thermal_zone: zone) # water_heater_thermal_zone
+ else
+ # zone = whtr.ambientTemperatureThermalZone.get
+ hpwh = std.model_add_water_heater(model, # model
+ (cap * 1000), # water_heater_capacity
+ v.to_f, # water_heater_volume
+ 'HeatPump', # water_heater_fuel
+ OpenStudio.convert(140.0, 'F', 'C').to_f, # service_water_temperature
+ 3.0, # parasitic_fuel_consumption_rate
+ sched, # swh_temp_sch
+ false, # set_peak_use_flowrate
+ 0.0, # peak_flowrate
+ nil, # flowrate_schedule
+ model.getThermalZones[0], # water_heater_thermal_zone
+ 1) # number_water_heaters
+ # set COP in PLF curve
+ cop_curve = hpwh.partLoadFactorCurve.get
+ cop_curve.setName(cop_curve.name.get.gsub('2.8', cop.to_s))
+ cop_curve.setCoefficient1Constant(cop)
+ end
+
+ # add tank to appropriate branch and node (will be placed first in series if old tanks not removed)
+ # modify objects as ncessary
+ if type != 'Simplified'
+ hpwh.tank.addToNode(inlet)
+ hpwh.setDeadBandTemperatureDifference(db_temp / 1.8)
+ runner.registerInfo("#{hpwh.tank.name} was added to the model on #{loop.name}")
+ else
+ hpwh.addToNode(inlet)
+ hpwh.setMaximumTemperatureLimit(OpenStudio.convert(max_temp, 'F', 'C').get)
+ runner.registerInfo("#{hpwh.name} was added to the model on #{loop.name}")
+ end
+
+ # remove old tank objects if necessary
+ if remove_wh
+ runner.registerInfo("#{whtr.name} was removed from the model.")
+ whtr.remove
+ end
+
+ # CONTROLS MODIFICATIONS FOR TANK ---------------------------------------------------------------------------------
+ # apply schedule to tank
+ case type
+ when 'PumpedCondenser'
+ hpwh.tank.to_WaterHeaterMixed.get.setSetpointTemperatureSchedule(tank_sched)
+ when 'WrappedCondenser'
+ hpwh.tank.to_WaterHeaterStratified.get.setHeater1SetpointTemperatureSchedule(tank_sched)
+ hpwh.tank.to_WaterHeaterStratified.get.setHeater2SetpointTemperatureSchedule(tank_sched)
+ end
+ # END CONTROLS MODIFICATIONS FOR TANK -----------------------------------------------------------------------------
+ end
+ ## END HARDWARE ----------------------------------------------------------------------------------------------------
+
+ ## ADD REPORTED VARIABLES ------------------------------------------------------------------------------------------
+
+ ovar_names = ['Cooling Coil Total Cooling Rate',
+ 'Cooling Coil Total Water Heating Rate',
+ 'Cooling Coil Water Heating Electric Power',
+ 'Cooling Coil Crankcase Heater Electric Power',
+ 'Water Heater Tank Temperature',
+ 'Water Heater Heat Loss Rate',
+ 'Water Heater Heating Rate',
+ 'Water Heater Use Side Heat Transfer Rate',
+ 'Water Heater Source Side Heat Transfer Rate',
+ 'Water Heater Unmet Demand Heat Transfer Rate',
+ 'Water Heater Electricity Rate',
+ 'Water Heater Water Volume Flow Rate',
+ 'Water Use Connections Hot Water Temperature']
+
+ # Create new output variable objects
+ ovars = []
+ ovar_names.each do |nm|
+ ovars << OpenStudio::Model::OutputVariable.new(nm, model)
+ end
+
+ # add temperate schedule outputs - clean up and put names into array, then loop over setting key values
+ v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
+ v.setKeyValue(sched.name.to_s)
+ ovars << v
+
+ v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
+ v.setKeyValue(tank_sched.name.to_s)
+ ovars << v
+
+ if type != 'Simplified'
+ v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
+ v.setKeyValue(tank_sched.name.to_s)
+ ovars << v
+ end
+
+ # Set variable reporting frequency for newly created output variables
+ ovars.each do |var|
+ var.setReportingFrequency('TimeStep')
+ end
+
+ # Register info re: output variables:
+ runner.registerInfo("#{ovars.size} output variables were added to the model.")
+ ## END ADD REPORTED VARIABLES --------------------------------------------------------------------------------------
+
+ # Register final condition
+ hpwh_fc = model.getWaterHeaterHeatPumps.size + model.getWaterHeaterHeatPumpWrappedCondensers.size
+ tanks_fc = model.getWaterHeaterMixeds.size + model.getWaterHeaterStratifieds.size
+ if type != 'Simplified'
+ runner.registerFinalCondition("The building finshed with #{tanks_fc} water heater tank(s) and " \
+ "#{hpwh_fc} heat pump water heater(s).")
+ else
+ runner.registerFinalCondition("The building finished with #{tanks_fc - whtrs.size} water heater tank(s) " \
+ "and #{whtrs.size} heat pump water heater(s).")
+ end
+
+ true
+ end
+end
+
+# register the measure to be used by the application
+AddHpwh.new.registerWithApplication