openstudio-load-flexibility-measures 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +36 -0
- data/.rubocop.yml +9 -0
- data/CHANGELOG.md +5 -0
- data/Gemfile +3 -0
- data/README.md +42 -0
- data/Rakefile +15 -0
- data/doc_templates/LICENSE.md +27 -0
- data/doc_templates/README.md.erb +42 -0
- data/doc_templates/copyright_erb.txt +36 -0
- data/doc_templates/copyright_js.txt +4 -0
- data/doc_templates/copyright_ruby.txt +34 -0
- data/lib/measures/add_central_hpwh_for_load_flexibility/LICENSE.md +1 -0
- data/lib/measures/add_central_hpwh_for_load_flexibility/README.md +186 -0
- data/lib/measures/add_central_hpwh_for_load_flexibility/README.md.erb +42 -0
- data/lib/measures/add_central_hpwh_for_load_flexibility/docs/Flexible Domestic Hot Water Implementation Guide.pdf +0 -0
- data/lib/measures/add_central_hpwh_for_load_flexibility/measure.rb +648 -0
- data/lib/measures/add_central_hpwh_for_load_flexibility/measure.xml +398 -0
- data/lib/measures/add_central_hpwh_for_load_flexibility/tests/SmallHotel-2A.osm +42893 -0
- data/lib/measures/add_central_hpwh_for_load_flexibility/tests/add_central_hpwh_for_load_flexibility.rb +98 -0
- data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/LICENSE.md +13 -0
- data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/README.md +189 -0
- data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/measure.rb +689 -0
- data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/measure.xml +253 -0
- data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/resources/TESCurves.idf +1059 -0
- data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/tests/MeasureTest.osm +9507 -0
- data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/tests/add_distributed_ice_storage_to_air_loop_for_load_flexibility_test.rb +96 -0
- data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/LICENSE.md +13 -0
- data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/README.md +264 -0
- data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/README.md.erb +42 -0
- data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/docs/Ice Measure Implementation Guide.pdf +0 -0
- data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/measure.rb +1310 -0
- data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/measure.xml +506 -0
- data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/resources/OsLib_Schedules.rb +173 -0
- data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/tests/add_ice_storage_to_plant_loop_for_load_flexibility_test.rb +202 -0
- data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/tests/ice_test_model.osm +21523 -0
- data/lib/openstudio/load_flexibility_measures.rb +50 -0
- data/lib/openstudio/load_flexibility_measures/version.rb +40 -0
- data/openstudio-load-flexibility-measures.gemspec +32 -0
- metadata +172 -0
@@ -0,0 +1,42 @@
|
|
1
|
+
<%#= README.md.erb is used to auto-generate README.md. %>
|
2
|
+
<%#= To manually maintain README.md throw away README.md.erb and manually edit README.md %>
|
3
|
+
###### (Automatically generated documentation)
|
4
|
+
|
5
|
+
# <%= name %>
|
6
|
+
|
7
|
+
## Description
|
8
|
+
<%= description %>
|
9
|
+
|
10
|
+
## Modeler Description
|
11
|
+
<%= modelerDescription %>
|
12
|
+
|
13
|
+
## Measure Type
|
14
|
+
<%= measureType %>
|
15
|
+
|
16
|
+
## Taxonomy
|
17
|
+
<%= taxonomy %>
|
18
|
+
|
19
|
+
## Arguments
|
20
|
+
|
21
|
+
<% arguments.each do |argument| %>
|
22
|
+
### <%= argument[:display_name] %>
|
23
|
+
<%= argument[:description] %>
|
24
|
+
**Name:** <%= argument[:name] %>,
|
25
|
+
**Type:** <%= argument[:type] %>,
|
26
|
+
**Units:** <%= argument[:units] %>,
|
27
|
+
**Required:** <%= argument[:required] %>,
|
28
|
+
**Model Dependent:** <%= argument[:model_dependent] %>
|
29
|
+
<% end %>
|
30
|
+
|
31
|
+
<% if arguments.size == 0 %>
|
32
|
+
<%= "This measure does not have any user arguments" %>
|
33
|
+
<% end %>
|
34
|
+
|
35
|
+
<% if outputs.size > 0 %>
|
36
|
+
## Outputs
|
37
|
+
<% output_names = [] %>
|
38
|
+
<% outputs.each do |output| %>
|
39
|
+
<% output_names << output[:display_name] %>
|
40
|
+
<% end %>
|
41
|
+
<%= output_names.join(", ") %>
|
42
|
+
<% end %>
|
Binary file
|
@@ -0,0 +1,648 @@
|
|
1
|
+
# *******************************************************************************
|
2
|
+
# OpenStudio(R), Copyright (c) 2008-2020, Alliance for Sustainable Energy, LLC.
|
3
|
+
# All rights reserved.
|
4
|
+
# Redistribution and use in source and binary forms, with or without
|
5
|
+
# modification, are permitted provided that the following conditions are met:
|
6
|
+
#
|
7
|
+
# (1) Redistributions of source code must retain the above copyright notice,
|
8
|
+
# this list of conditions and the following disclaimer.
|
9
|
+
#
|
10
|
+
# (2) Redistributions in binary form must reproduce the above copyright notice,
|
11
|
+
# this list of conditions and the following disclaimer in the documentation
|
12
|
+
# and/or other materials provided with the distribution.
|
13
|
+
#
|
14
|
+
# (3) Neither the name of the copyright holder nor the names of any contributors
|
15
|
+
# may be used to endorse or promote products derived from this software without
|
16
|
+
# specific prior written permission from the respective party.
|
17
|
+
#
|
18
|
+
# (4) Other than as required in clauses (1) and (2), distributions in any form
|
19
|
+
# of modifications or other derivative works may not use the "OpenStudio"
|
20
|
+
# trademark, "OS", "os", or any other confusingly similar designation without
|
21
|
+
# specific prior written permission from Alliance for Sustainable Energy, LLC.
|
22
|
+
#
|
23
|
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER(S) AND ANY CONTRIBUTORS
|
24
|
+
# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
|
25
|
+
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
26
|
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER(S), ANY CONTRIBUTORS, THE
|
27
|
+
# UNITED STATES GOVERNMENT, OR THE UNITED STATES DEPARTMENT OF ENERGY, NOR ANY OF
|
28
|
+
# THEIR EMPLOYEES, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
|
29
|
+
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
|
30
|
+
# OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
31
|
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
32
|
+
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
33
|
+
# OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
34
|
+
# *******************************************************************************
|
35
|
+
|
36
|
+
# Measure distributed under NREL Copyright terms, see LICENSE.md file.
|
37
|
+
|
38
|
+
# Author: Karl Heine
|
39
|
+
# Date: December 2019 - March 2020
|
40
|
+
|
41
|
+
# References:
|
42
|
+
# EnergyPlus InputOutput Reference, Sections:
|
43
|
+
# EnergyPlus Engineering Reference, Sections:
|
44
|
+
|
45
|
+
# start the measure
|
46
|
+
class AddCentralHPWHForLoadFlexibility < OpenStudio::Measure::ModelMeasure
|
47
|
+
require 'openstudio-standards'
|
48
|
+
|
49
|
+
# human readable name
|
50
|
+
def name
|
51
|
+
# Measure name should be the title case of the class name.
|
52
|
+
'flexible_domestic_hot_water'
|
53
|
+
end
|
54
|
+
|
55
|
+
# human readable description
|
56
|
+
def description
|
57
|
+
'This measure adds or replaces existing domestic hot water heater with air source heat pump system and ' \
|
58
|
+
'allows for the addition of multiple daily flexible control time windows. The heater/tank system may ' \
|
59
|
+
'charge at maximum capacity up to an elevated temperature, or float without any heat addition for a ' \
|
60
|
+
'specified timeframe down to a minimum tank temperature.'
|
61
|
+
end
|
62
|
+
|
63
|
+
# human readable description of modeling approach
|
64
|
+
def modeler_description
|
65
|
+
return 'This measure allows selection between three heat pump water heater modeling approaches in EnergyPlus.' \
|
66
|
+
'The user may select between the pumped-condenser or wrapped-condenser objects. They may also elect to ' \
|
67
|
+
'use a simplified calculation which does not use the heat pump objects, but instead used an electric ' \
|
68
|
+
'resistance heater and approximates the equivalent electrical input that would be required from a heat ' \
|
69
|
+
"pump. This expedites simulation at the expense of accuracy. \n" \
|
70
|
+
'The flexibility of the system is based on user-defined temperatures and times, which are converted into ' \
|
71
|
+
'schedule objects. There are four flexibility options. (1) None: normal operation of the DHW system at ' \
|
72
|
+
'a fixed tank temperature setpoint. (2) Charge - Heat Pump: the tank is charged to a maximum temperature ' \
|
73
|
+
'using only the heat pump. (3) Charge - Electric: the tank is charged using internal electric resistance ' \
|
74
|
+
'heaters to a maximum temperature. (4) Float: all heating elements are turned-off for a user-defined time ' \
|
75
|
+
'period unless the tank temperature falls below a minimum value. The heat pump will be prioritized in a ' \
|
76
|
+
"low tank temperature event, with the electric resistance heaters serving as back-up. \n"
|
77
|
+
'Due to the heat pump interaction with zone conditioning as well as tank heating, users may experience ' \
|
78
|
+
'simulation errors if the heat pump is too large and placed in an already conditioned zoned. Try using ' \
|
79
|
+
'multiple smaller units, modifying the heat pump location within the model, or adjusting the zone thermo' \
|
80
|
+
'stat constraints. Use mulitiple instances of the measure to add multiple heat pump water heaters. '
|
81
|
+
end
|
82
|
+
|
83
|
+
## USER ARGS ---------------------------------------------------------------------------------------------------------
|
84
|
+
# define the arguments that the user will input
|
85
|
+
def arguments(model)
|
86
|
+
args = OpenStudio::Measure::OSArgumentVector.new
|
87
|
+
|
88
|
+
# create argument for removal of existing water heater tanks on selected loop
|
89
|
+
remove_wh = OpenStudio::Measure::OSArgument.makeBoolArgument('remove_wh', true)
|
90
|
+
remove_wh.setDisplayName('Remove existing water heater on selected loop?')
|
91
|
+
remove_wh.setDescription('')
|
92
|
+
remove_wh.setDefaultValue(true)
|
93
|
+
args << remove_wh
|
94
|
+
|
95
|
+
# find available plant loops (heating)
|
96
|
+
loop_names = []
|
97
|
+
|
98
|
+
unless model.getPlantLoops.empty?
|
99
|
+
loops = model.getPlantLoops
|
100
|
+
loops.each do |lp|
|
101
|
+
unless lp.sizingPlant.loopType.empty?
|
102
|
+
next unless lp.sizingPlant.loopType.to_s == 'Heating'
|
103
|
+
loop_names << lp.name.to_s
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
loop_names << 'Error: No Service Water Loop Found' if loop_names.empty?
|
109
|
+
|
110
|
+
# create argument for loop selection
|
111
|
+
loop = OpenStudio::Measure::OSArgument.makeChoiceArgument('loop', loop_names.sort, true)
|
112
|
+
loop.setDisplayName('Select hot water loop')
|
113
|
+
loop.setDescription('The water tank will be placed on the supply side of this loop.')
|
114
|
+
loop.setDefaultValue(loop_names.sort[0])
|
115
|
+
args << loop
|
116
|
+
|
117
|
+
# find available spaces for heater location
|
118
|
+
zone_names = []
|
119
|
+
|
120
|
+
unless model.getThermalZones.empty?
|
121
|
+
zones = model.getThermalZones
|
122
|
+
zones.each do |zn|
|
123
|
+
zone_names << zn.name.to_s
|
124
|
+
end
|
125
|
+
zone_names.sort!
|
126
|
+
end
|
127
|
+
|
128
|
+
zone_names << 'Error: No Thermal Zones Found' if zone_names.empty?
|
129
|
+
|
130
|
+
# create argument for thermal zone selection (location of water heater)
|
131
|
+
zone = OpenStudio::Measure::OSArgument.makeChoiceArgument('zone', zone_names, true)
|
132
|
+
zone.setDisplayName('Select thermal zone')
|
133
|
+
zone.setDescription('This is where the water heater tank will be placed')
|
134
|
+
zone.setDefaultValue(zone_names[0])
|
135
|
+
args << zone
|
136
|
+
|
137
|
+
# create argument for water heater type
|
138
|
+
type = OpenStudio::Measure::OSArgument.makeChoiceArgument('type',
|
139
|
+
['PumpedCondenser', 'WrappedCondenser', 'Simplified'], true)
|
140
|
+
type.setDisplayName('Select heat pump water heater type')
|
141
|
+
type.setDescription('')
|
142
|
+
type.setDefaultValue('PumpedCondenser')
|
143
|
+
args << type
|
144
|
+
|
145
|
+
# find largest current water heater volume - if any mixed tanks are already present. Default is 80 gal.
|
146
|
+
default_vol = 80.0 # gal
|
147
|
+
|
148
|
+
wheaters = if !model.getWaterHeaterMixeds.empty?
|
149
|
+
model.getWaterHeaterMixeds
|
150
|
+
else
|
151
|
+
[]
|
152
|
+
end
|
153
|
+
|
154
|
+
unless wheaters.empty?
|
155
|
+
wheaters.each do |wh|
|
156
|
+
unless wh.tankVolume.empty?
|
157
|
+
default_vol = [default_vol, (wh.tankVolume.to_f / 0.0037854118).round(1)].max # convert m^3 to gal
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# create argument for hot water tank volume
|
163
|
+
vol = OpenStudio::Measure::OSArgument.makeDoubleArgument('vol', true)
|
164
|
+
vol.setDisplayName('Set hot water tank volume')
|
165
|
+
vol.setDescription('[gal]')
|
166
|
+
vol.setDefaultValue(default_vol)
|
167
|
+
args << vol
|
168
|
+
|
169
|
+
# create argument for heat pump capacity
|
170
|
+
cap = OpenStudio::Measure::OSArgument.makeDoubleArgument('cap', true)
|
171
|
+
cap.setDisplayName('Set heat pump heating capacity')
|
172
|
+
cap.setDescription('[kW]')
|
173
|
+
cap.setDefaultValue((23.446 * (default_vol / 80.0)).round(1))
|
174
|
+
args << cap
|
175
|
+
|
176
|
+
# create argument for heat pump rated cop
|
177
|
+
cop = OpenStudio::Measure::OSArgument.makeDoubleArgument('cop', true)
|
178
|
+
cop.setDisplayName('Set heat pump rated COP (heating)')
|
179
|
+
cop.setDescription('[-]')
|
180
|
+
cop.setDefaultValue(2.8)
|
181
|
+
args << cop
|
182
|
+
|
183
|
+
# create argument for electric backup capacity
|
184
|
+
bu_cap = OpenStudio::Measure::OSArgument.makeDoubleArgument('bu_cap', true)
|
185
|
+
bu_cap.setDisplayName('Set electric backup heating capacity')
|
186
|
+
bu_cap.setDescription('[kW]')
|
187
|
+
bu_cap.setDefaultValue((23.446 * (default_vol / 80.0)).round(1))
|
188
|
+
args << bu_cap
|
189
|
+
|
190
|
+
# create argument for maximum tank temperature
|
191
|
+
max_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('max_temp', true)
|
192
|
+
max_temp.setDisplayName('Set maximum tank temperature')
|
193
|
+
max_temp.setDescription('[F]')
|
194
|
+
max_temp.setDefaultValue(160)
|
195
|
+
args << max_temp
|
196
|
+
|
197
|
+
# create argument for minimum float temperature
|
198
|
+
min_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('min_temp', true)
|
199
|
+
min_temp.setDisplayName('Set minimum tank temperature during float')
|
200
|
+
min_temp.setDescription('[F]')
|
201
|
+
min_temp.setDefaultValue(120)
|
202
|
+
args << min_temp
|
203
|
+
|
204
|
+
# create argument for deadband temperature difference between heat pump setpoint and electric backup
|
205
|
+
db_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('db_temp', true)
|
206
|
+
db_temp.setDisplayName('Set deadband temperature difference between heat pump and electric backup')
|
207
|
+
db_temp.setDescription('[F]')
|
208
|
+
db_temp.setDefaultValue(5)
|
209
|
+
args << db_temp
|
210
|
+
|
211
|
+
# find existing temperature setpoint schedules for water heater
|
212
|
+
all_scheds = model.getSchedules
|
213
|
+
temp_sched_names = []
|
214
|
+
default_sched = '--Create New @ 140F--'
|
215
|
+
default_ambient = ''
|
216
|
+
all_scheds.each do |sch|
|
217
|
+
next if sch.scheduleTypeLimits.empty?
|
218
|
+
next unless sch.scheduleTypeLimits.get.unitType.to_s == 'Temperature'
|
219
|
+
temp_sched_names << sch.name.to_s
|
220
|
+
if !wheaters.empty? && (sch.name.to_s == wheaters[0].setpointTemperatureSchedule.get.name.to_s)
|
221
|
+
default_sched = sch.name.to_s
|
222
|
+
end
|
223
|
+
end
|
224
|
+
temp_sched_names = [default_sched] + temp_sched_names.sort
|
225
|
+
|
226
|
+
# create argument for predefined schedule
|
227
|
+
sched = OpenStudio::Measure::OSArgument.makeChoiceArgument('sched', temp_sched_names, true)
|
228
|
+
sched.setDisplayName('Select reference tank setpoint temperature schedule')
|
229
|
+
sched.setDescription('')
|
230
|
+
sched.setDefaultValue(temp_sched_names[0])
|
231
|
+
args << sched
|
232
|
+
|
233
|
+
# define possible flex options
|
234
|
+
flex_options = ['None', 'Charge - Heat Pump', 'Charge - Electric', 'Float']
|
235
|
+
|
236
|
+
# create choice and string arguments for flex periods
|
237
|
+
4.times do |n|
|
238
|
+
flex = OpenStudio::Measure::OSArgument.makeChoiceArgument('flex' + n.to_s, flex_options, true)
|
239
|
+
flex.setDisplayName("Daily Flex Period #{n + 1}:")
|
240
|
+
flex.setDescription('Applies every day in the full run period.')
|
241
|
+
flex.setDefaultValue('None')
|
242
|
+
args << flex
|
243
|
+
|
244
|
+
flex_hrs = OpenStudio::Measure::OSArgument.makeStringArgument('flex_hrs' + n.to_s, false)
|
245
|
+
flex_hrs.setDisplayName('Use 24-Hour Format')
|
246
|
+
flex_hrs.setDefaultValue('HH:MM - HH:MM')
|
247
|
+
args << flex_hrs
|
248
|
+
end
|
249
|
+
|
250
|
+
args
|
251
|
+
end
|
252
|
+
## END USER ARGS -----------------------------------------------------------------------------------------------------
|
253
|
+
|
254
|
+
## MEASURE RUN -------------------------------------------------------------------------------------------------------
|
255
|
+
# Index:
|
256
|
+
# => Argument Validation
|
257
|
+
# => Controls: Heat Pump Heating Shedule
|
258
|
+
# => Controls: Tank Electric Backup Heating Schedule
|
259
|
+
# => Hardware
|
260
|
+
# => Controls Modifications for Tank
|
261
|
+
# => Report Output Variables
|
262
|
+
|
263
|
+
# define what happens when the measure is run
|
264
|
+
def run(model, runner, user_arguments)
|
265
|
+
super(model, runner, user_arguments)
|
266
|
+
|
267
|
+
## ARGUMENT VALIDATION ---------------------------------------------------------------------------------------------
|
268
|
+
# Measure does not immedately return false upon error detection. Errors are accumulated throughout this selection
|
269
|
+
# before exiting gracefully prior to measure execution.
|
270
|
+
|
271
|
+
# use the built-in error checking
|
272
|
+
unless runner.validateUserArguments(arguments(model), user_arguments)
|
273
|
+
return false
|
274
|
+
end
|
275
|
+
|
276
|
+
# report initial condition of model
|
277
|
+
tanks_ic = model.getWaterHeaterMixeds.size + model.getWaterHeaterStratifieds.size
|
278
|
+
hpwh_ic = model.getWaterHeaterHeatPumps.size + model.getWaterHeaterHeatPumpWrappedCondensers.size
|
279
|
+
runner.registerInitialCondition("The building started with #{tanks_ic} water heater tank(s) and " \
|
280
|
+
"#{hpwh_ic} heat pump water heater(s).")
|
281
|
+
|
282
|
+
# create empty arrays and initialize variables for future use
|
283
|
+
flex = []
|
284
|
+
flex_type = []
|
285
|
+
flex_hrs = []
|
286
|
+
time_check = []
|
287
|
+
hours = []
|
288
|
+
minutes = []
|
289
|
+
flex_times = []
|
290
|
+
|
291
|
+
# assign the user inputs to variables
|
292
|
+
remove_wh = runner.getBoolArgumentValue('remove_wh', user_arguments)
|
293
|
+
loop = runner.getStringArgumentValue('loop', user_arguments)
|
294
|
+
zone = runner.getStringArgumentValue('zone', user_arguments)
|
295
|
+
type = runner.getStringArgumentValue('type', user_arguments)
|
296
|
+
cap = runner.getDoubleArgumentValue('cap', user_arguments)
|
297
|
+
cop = runner.getDoubleArgumentValue('cop', user_arguments)
|
298
|
+
bu_cap = runner.getDoubleArgumentValue('bu_cap', user_arguments)
|
299
|
+
vol = runner.getDoubleArgumentValue('vol', user_arguments)
|
300
|
+
max_temp = runner.getDoubleArgumentValue('max_temp', user_arguments)
|
301
|
+
min_temp = runner.getDoubleArgumentValue('min_temp', user_arguments)
|
302
|
+
db_temp = runner.getDoubleArgumentValue('db_temp', user_arguments)
|
303
|
+
sched = runner.getStringArgumentValue('sched', user_arguments)
|
304
|
+
|
305
|
+
4.times do |n|
|
306
|
+
flex << runner.getStringArgumentValue('flex' + n.to_s, user_arguments)
|
307
|
+
flex_hrs << runner.getStringArgumentValue('flex_hrs' + n.to_s, user_arguments)
|
308
|
+
end
|
309
|
+
|
310
|
+
# check for error inputs
|
311
|
+
if loop.include?('Error')
|
312
|
+
runner.registerError('No service hot water loop was found. Measure did not run.')
|
313
|
+
end
|
314
|
+
|
315
|
+
if zone.include?('Error')
|
316
|
+
runner.registerError('No thermal zone was found. Measure did not run.')
|
317
|
+
end
|
318
|
+
|
319
|
+
# check capacity, volume, and temps for reasonableness
|
320
|
+
if cap < 5
|
321
|
+
runner.registerWarning('HPWH heating capacity is less than 5kW ( 17kBtu/hr)')
|
322
|
+
end
|
323
|
+
|
324
|
+
if bu_cap < 5
|
325
|
+
runner.registerWarning('Backup heating capaicty is less than 5kW ( 17kBtu/hr).')
|
326
|
+
end
|
327
|
+
|
328
|
+
if vol < 40
|
329
|
+
runner.registerWarning('Tank has less than 40 gallon capacity; check heat pump sizing if model fails.')
|
330
|
+
end
|
331
|
+
|
332
|
+
if min_temp < 120
|
333
|
+
runner.registerWarning('Minimum tank temperature is very low; consider increasing to at least 120F.')
|
334
|
+
runner.registerWarning('Do not store water for long periods at temperatures below 135-140F as those ' \
|
335
|
+
'conditions facilitate the growth of Legionella.')
|
336
|
+
end
|
337
|
+
|
338
|
+
if max_temp > 180
|
339
|
+
runner.registerWarning('Maximum charging temperature exceeded practical limits; reset to 180F.')
|
340
|
+
max_temp = 180.0
|
341
|
+
end
|
342
|
+
|
343
|
+
if max_temp > 160
|
344
|
+
runner.registerWarning("#{max_temp}F is above or near the limit of the HP performance curves. If the " \
|
345
|
+
'simulation fails with cooling capacity less than 0, you have exceeded performance ' \
|
346
|
+
'limits. Consider setting max temp to less than 160F.')
|
347
|
+
end
|
348
|
+
|
349
|
+
# check selected schedule and set flag for later use
|
350
|
+
sched_flag = false # flag for either creating new (false) or modifying existing (true) schedule
|
351
|
+
if sched == '--Create New @ 140F--'
|
352
|
+
runner.registerInfo('No reference water heater temperature setpoint schedule was selected; a new one ' \
|
353
|
+
'will be created.')
|
354
|
+
else
|
355
|
+
sched_flag = true
|
356
|
+
runner.registerInfo("#{sched} will be used as the water heater temperature setpoint schedule.")
|
357
|
+
end
|
358
|
+
|
359
|
+
# parse flex_hrs into hours and minuts arrays
|
360
|
+
idx = 0
|
361
|
+
flex_hrs.each do |fh|
|
362
|
+
if flex[idx] != 'None'
|
363
|
+
data = fh.split(/[-:]/)
|
364
|
+
data.each { |e| e.delete!(' ') }
|
365
|
+
if data[2] > data[0]
|
366
|
+
flex_type << flex[idx]
|
367
|
+
hours << data[0]
|
368
|
+
hours << data[2]
|
369
|
+
minutes << data[1]
|
370
|
+
minutes << data[3]
|
371
|
+
else
|
372
|
+
flex_type << flex[idx]
|
373
|
+
flex_type << flex[idx]
|
374
|
+
hours << 0
|
375
|
+
hours << data[2]
|
376
|
+
hours << data[0]
|
377
|
+
hours << 24
|
378
|
+
minutes << 0
|
379
|
+
minutes << data[3]
|
380
|
+
minutes << data[1]
|
381
|
+
minutes << 0
|
382
|
+
end
|
383
|
+
end
|
384
|
+
idx += 1
|
385
|
+
end
|
386
|
+
|
387
|
+
# convert hours and minutes into OS:Time objects
|
388
|
+
idx = 0
|
389
|
+
hours.each do |h|
|
390
|
+
flex_times << OpenStudio::Time.new(0, h.to_i, minutes[idx].to_i, 0)
|
391
|
+
idx += 1
|
392
|
+
end
|
393
|
+
|
394
|
+
# flex.delete('None')
|
395
|
+
|
396
|
+
runner.registerInfo("A total of #{idx / 2} flex periods will be added to the selected water heater setpoint schedule.")
|
397
|
+
|
398
|
+
# exit gracefully if errors registered above
|
399
|
+
return false unless runner.result.errors.empty?
|
400
|
+
## END ARGUMENT VALIDATION -----------------------------------------------------------------------------------------
|
401
|
+
|
402
|
+
## CONTROLS: HEAT PUMP HEATING TEMPERATURE SETPOINT SCHEDULE -------------------------------------------------------
|
403
|
+
# This section creates the heat pump heating temperature setpoint schedule with flex periods
|
404
|
+
# The tank schedule is created here
|
405
|
+
|
406
|
+
# find or create new reference temperature schedule based on sched_flag value
|
407
|
+
if sched_flag # schedule already exists and must be modified
|
408
|
+
# converts the STRING into a MODEL OBJECT, same variable name
|
409
|
+
sched = model.getScheduleRulesetByName(sched).get.clone.to_ScheduleRuleset.get
|
410
|
+
else
|
411
|
+
# must create new water heater setpoint temperature schedule at 140F
|
412
|
+
sched = OpenStudio::Model::ScheduleRuleset.new(model, 60)
|
413
|
+
end
|
414
|
+
|
415
|
+
# rename and duplicate for later modification
|
416
|
+
sched.setName('Heat Pump Heating Temperature Setpoint')
|
417
|
+
sched.defaultDaySchedule.setName('Heat Pump Heating Temperature Setpoint Default')
|
418
|
+
|
419
|
+
# tank_sched = sched.clone.to_ScheduleRuleset.get
|
420
|
+
tank_sched = OpenStudio::Model::ScheduleRuleset.new(model, 60 - (db_temp / 1.8 + 2))
|
421
|
+
tank_sched.setName('Tank Electric Heater Setpoint')
|
422
|
+
tank_sched.defaultDaySchedule.setName('Tank Electric Heater Setpoint Default')
|
423
|
+
|
424
|
+
# grab default day and time-value pairs for modification
|
425
|
+
d_day = sched.defaultDaySchedule
|
426
|
+
old_times = d_day.times
|
427
|
+
old_values = d_day.values
|
428
|
+
new_values = Array.new(flex_times.size, 2)
|
429
|
+
|
430
|
+
# find existing values in reference schedule and grab for use in new-rule creation
|
431
|
+
flex_times.size.times do |i|
|
432
|
+
if i.even?
|
433
|
+
n = 0
|
434
|
+
old_times.each do |ot|
|
435
|
+
new_values[i] = old_values[n] if flex_times[i] <= ot
|
436
|
+
n += 1
|
437
|
+
end
|
438
|
+
elsif flex_type[(i / 2).floor] == 'Charge - Heat Pump'
|
439
|
+
new_values[i] = OpenStudio.convert(max_temp, 'F', 'C').get
|
440
|
+
elsif flex_type[(i / 2).floor] == 'Float' || flex_type[(i / 2).floor] == 'Charge - Electric'
|
441
|
+
new_values[i] = OpenStudio.convert(min_temp, 'F', 'C').get
|
442
|
+
end
|
443
|
+
end
|
444
|
+
|
445
|
+
# create new rules and add to default day based on flex period options above
|
446
|
+
idx = 0
|
447
|
+
flex_times.each do |ft|
|
448
|
+
d_day.addValue(ft, new_values[idx])
|
449
|
+
idx += 1
|
450
|
+
end
|
451
|
+
|
452
|
+
## END CONTROLS: HEAT PUMP HEATING TEMPERATURE SETPOINT SCHEDULE ---------------------------------------------------
|
453
|
+
|
454
|
+
## CONTROLS: TANK TEMPERATURE SETPOINT SCHEDULE (ELECTRIC BACKUP) --------------------------------------------------
|
455
|
+
# This section creates the setpoint temperature schedule for the electric backup heating coils in the water tank
|
456
|
+
|
457
|
+
# grab default day and time-value pairs for modification
|
458
|
+
d_day = tank_sched.defaultDaySchedule
|
459
|
+
old_times = d_day.times
|
460
|
+
old_values = d_day.values
|
461
|
+
new_values = Array.new(flex_times.size, 2)
|
462
|
+
|
463
|
+
# find existing values in reference schedule and grab for use in new-rule creation
|
464
|
+
flex_times.size.times do |i|
|
465
|
+
if i.even?
|
466
|
+
n = 0
|
467
|
+
old_times.each do |ot|
|
468
|
+
new_values[i] = old_values[n] if flex_times[i] <= ot
|
469
|
+
n += 1
|
470
|
+
end
|
471
|
+
elsif flex_type[(i / 2).floor] == 'Charge - Electric'
|
472
|
+
new_values[i] = OpenStudio.convert(max_temp, 'F', 'C').get
|
473
|
+
elsif flex_type[(i / 2).floor] == 'Float' # || flex_type[(i/2).floor] == 'Charge - Heat Pump'
|
474
|
+
new_values[i] = OpenStudio.convert(min_temp - db_temp, 'F', 'C').get
|
475
|
+
elsif flex_type[(i / 2).floor] == 'Charge - Heat Pump'
|
476
|
+
new_values[i] = 60 - (db_temp / 1.8)
|
477
|
+
end
|
478
|
+
end
|
479
|
+
|
480
|
+
# create new rules and add to default day based on flex period options above
|
481
|
+
idx = 0
|
482
|
+
flex_times.each do |ft|
|
483
|
+
d_day.addValue(ft, new_values[idx])
|
484
|
+
idx += 1
|
485
|
+
end
|
486
|
+
|
487
|
+
## CONTROLS: TANK TEMPERATURE SETPOINT SCHEDULE (ELECTRIC BACKUP) --------------------------------------------------
|
488
|
+
|
489
|
+
## HARDWARE --------------------------------------------------------------------------------------------------------
|
490
|
+
# This section adds the selected type of heat pump water heater to the supply side of the selected loop. If
|
491
|
+
# selected, measure will remove any existing water heaters on the supply side of the loop. If old heater(s) are left
|
492
|
+
# in place, the new HPWH tank will be placed in front (to the left) of them.
|
493
|
+
|
494
|
+
# use OS standards build - arbitrary selection, but NZE Ready seems appropriate
|
495
|
+
std = Standard.build('NREL ZNE Ready 2017')
|
496
|
+
|
497
|
+
# create empty arrays and initialize variables for later use
|
498
|
+
old_heater = []
|
499
|
+
count = 0
|
500
|
+
|
501
|
+
# convert loop and zone names from STRINGS into OS model OBJECTS
|
502
|
+
zone = model.getThermalZoneByName(zone).get
|
503
|
+
loop = model.getPlantLoopByName(loop).get
|
504
|
+
|
505
|
+
# find and locate old water heater on selected loop, if applicable
|
506
|
+
loop_equip = loop.supplyComponents
|
507
|
+
loop_equip.each do |le|
|
508
|
+
if le.iddObject.name.include?('WaterHeater:Mixed')
|
509
|
+
old_heater << model.getWaterHeaterMixedByName(le.name.to_s).get
|
510
|
+
count += 1
|
511
|
+
elsif le.iddObject.name.include?('WaterHeater:Stratified')
|
512
|
+
old_heater << model.getWaterHeaterStratifiedByName(le.name.to_s).get
|
513
|
+
count += 1
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
unless old_heater.empty?
|
518
|
+
inlet = old_heater[0].supplyInletModelObject.get.to_Node.get
|
519
|
+
outlet = old_heater[0].supplyOutletModelObject.get.to_Node.get
|
520
|
+
end
|
521
|
+
|
522
|
+
# Add heat pump water heater and attach to selected loop
|
523
|
+
# Reference: https://github.com/NREL/openstudio-standards/blob/master/lib/
|
524
|
+
# => openstudio-standards/prototypes/common/objects/Prototype.ServiceWaterHeating.rb
|
525
|
+
if type != 'Simplified'
|
526
|
+
hpwh = std.model_add_heatpump_water_heater(model, # model
|
527
|
+
type: type, # type
|
528
|
+
water_heater_capacity: (cap * 1000 / cop), # water_heater_capacity
|
529
|
+
electric_backup_capacity: (bu_cap * 1000), # electric_backup_capacity
|
530
|
+
water_heater_volume: OpenStudio.convert(vol, 'gal', 'm^3').get, # water_heater_volume
|
531
|
+
service_water_temperature: OpenStudio.convert(140.0, 'F', 'C').get, # service_water_temperature
|
532
|
+
parasitic_fuel_consumption_rate: 3.0, # parasitic_fuel_consumption_rate
|
533
|
+
swh_temp_sch: sched, # swh_temp_sch
|
534
|
+
cop: cop, # cop
|
535
|
+
shr: 0.88, # shr
|
536
|
+
tank_ua: 3.9, # tank_ua
|
537
|
+
set_peak_use_flowrate: false, # set_peak_use_flowrate
|
538
|
+
peak_flowrate: 0.0, # peak_flowrate
|
539
|
+
flowrate_schedule: nil, # flowrate_schedule
|
540
|
+
water_heater_thermal_zone: zone) # water_heater_thermal_zone
|
541
|
+
else
|
542
|
+
hpwh = std.model_add_water_heater(model, # model
|
543
|
+
(cap * 1000), # water_heater_capacity
|
544
|
+
OpenStudio.convert(vol, 'gal', 'm^3').get, # water_heater_volume
|
545
|
+
'HeatPump', # water_heater_fuel
|
546
|
+
OpenStudio.convert(140.0, 'F', 'C').get, # service_water_temperature
|
547
|
+
3.0, # parasitic_fuel_consumption_rate
|
548
|
+
sched, # swh_temp_sch
|
549
|
+
false, # set_peak_use_flowrate
|
550
|
+
0.0, # peak_flowrate
|
551
|
+
nil, # flowrate_schedule
|
552
|
+
zone, # water_heater_thermal_zone
|
553
|
+
1) # number_water_heaters
|
554
|
+
end
|
555
|
+
|
556
|
+
# add tank to appropriate branch and node (will be placed first in series if old tanks not removed)
|
557
|
+
# modify objects as ncessary
|
558
|
+
if old_heater.empty?
|
559
|
+
loop.addSupplyBranchForComponent(hpwh.tank)
|
560
|
+
elsif type != 'Simplified'
|
561
|
+
hpwh.tank.addToNode(inlet)
|
562
|
+
hpwh.setDeadBandTemperatureDifference(db_temp / 1.8)
|
563
|
+
runner.registerInfo("#{hpwh.tank.name} was added to the model on #{loop.name}")
|
564
|
+
else
|
565
|
+
hpwh.addToNode(inlet)
|
566
|
+
hpwh.setMaximumTemperatureLimit(OpenStudio.convert(max_temp, 'F', 'C').get)
|
567
|
+
runner.registerInfo("#{hpwh.name} was added to the model on #{loop.name}")
|
568
|
+
end
|
569
|
+
|
570
|
+
# remove old tank objects if necessary
|
571
|
+
if remove_wh
|
572
|
+
old_heater.each do |oh|
|
573
|
+
runner.registerInfo("#{oh.name} was removed from the model.")
|
574
|
+
oh.remove
|
575
|
+
end
|
576
|
+
end
|
577
|
+
## END HARDWARE ----------------------------------------------------------------------------------------------------
|
578
|
+
|
579
|
+
## CONTROLS MODIFICATIONS FOR TANK ---------------------------------------------------------------------------------
|
580
|
+
# apply schedule to tank
|
581
|
+
if type == 'PumpedCondenser'
|
582
|
+
hpwh.tank.to_WaterHeaterMixed.get.setSetpointTemperatureSchedule(tank_sched)
|
583
|
+
elsif type == 'WrappedCondenser'
|
584
|
+
hpwh.tank.to_WaterHeaterStratified.get.setHeater1SetpointTemperatureSchedule(tank_sched)
|
585
|
+
hpwh.tank.to_WaterHeaterStratified.get.setHeater2SetpointTemperatureSchedule(tank_sched)
|
586
|
+
elsif type == 'Simplified'
|
587
|
+
runner.registerInfo('Line 492 was used. Nothing done here yet... Check tank temperature schedules...')
|
588
|
+
end
|
589
|
+
## END CONTROLS MODIFICATIONS FOR TANK -----------------------------------------------------------------------------
|
590
|
+
|
591
|
+
## ADD REPORTED VARIABLES ------------------------------------------------------------------------------------------
|
592
|
+
|
593
|
+
ovar_names = ['Cooling Coil Total Cooling Rate',
|
594
|
+
'Cooling Coil Total Water Heating Rate',
|
595
|
+
'Cooling Coil Water Heating Electric Power',
|
596
|
+
'Cooling Coil Crankcase Heater Electric Power',
|
597
|
+
'Water Heater Tank Temperature',
|
598
|
+
'Water Heater Heat Loss Rate',
|
599
|
+
'Water Heater Heating Rate',
|
600
|
+
'Water Heater Use Side Heat Transfer Rate',
|
601
|
+
'Water Heater Source Side Heat Transfer Rate',
|
602
|
+
'Water Heater Unmet Demand Heat Transfer Rate',
|
603
|
+
'Water Heater Electric Power',
|
604
|
+
'Water Heater Water Volume Flow Rate',
|
605
|
+
'Water Use Connections Hot Water Temperature']
|
606
|
+
|
607
|
+
# Create new output variable objects
|
608
|
+
ovars = []
|
609
|
+
ovar_names.each do |nm|
|
610
|
+
ovars << OpenStudio::Model::OutputVariable.new(nm, model)
|
611
|
+
end
|
612
|
+
|
613
|
+
# add temperate schedule outputs - clean up and put names into array, then loop over setting key values
|
614
|
+
v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
|
615
|
+
v.setKeyValue(sched.name.to_s)
|
616
|
+
ovars << v
|
617
|
+
|
618
|
+
v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
|
619
|
+
v.setKeyValue(tank_sched.name.to_s)
|
620
|
+
ovars << v
|
621
|
+
|
622
|
+
if type != 'Simplified'
|
623
|
+
v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
|
624
|
+
v.setKeyValue(tank_sched.name.to_s)
|
625
|
+
ovars << v
|
626
|
+
end
|
627
|
+
|
628
|
+
# Set variable reporting frequency for newly created output variables
|
629
|
+
ovars.each do |var|
|
630
|
+
var.setReportingFrequency('TimeStep')
|
631
|
+
end
|
632
|
+
|
633
|
+
# Register info re: output variables:
|
634
|
+
runner.registerInfo("#{ovars.size} output variables were added to the model.")
|
635
|
+
## END ADD REPORTED VARIABLES --------------------------------------------------------------------------------------
|
636
|
+
|
637
|
+
# Register final condition
|
638
|
+
hpwh_fc = model.getWaterHeaterHeatPumps.size + model.getWaterHeaterHeatPumpWrappedCondensers.size
|
639
|
+
tanks_fc = model.getWaterHeaterMixeds.size + model.getWaterHeaterStratifieds.size
|
640
|
+
runner.registerFinalCondition("The building finshed with #{tanks_fc} water heater tank(s) and " \
|
641
|
+
"#{hpwh_fc} heat pump water heater(s).")
|
642
|
+
|
643
|
+
true
|
644
|
+
end
|
645
|
+
end
|
646
|
+
|
647
|
+
# register the measure to be used by the application
|
648
|
+
AddCentralHPWHForLoadFlexibility.new.registerWithApplication
|