openstudio-load-flexibility-measures 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.gitignore +36 -0
  4. data/.rubocop.yml +9 -0
  5. data/CHANGELOG.md +5 -0
  6. data/Gemfile +3 -0
  7. data/README.md +42 -0
  8. data/Rakefile +15 -0
  9. data/doc_templates/LICENSE.md +27 -0
  10. data/doc_templates/README.md.erb +42 -0
  11. data/doc_templates/copyright_erb.txt +36 -0
  12. data/doc_templates/copyright_js.txt +4 -0
  13. data/doc_templates/copyright_ruby.txt +34 -0
  14. data/lib/measures/add_central_hpwh_for_load_flexibility/LICENSE.md +1 -0
  15. data/lib/measures/add_central_hpwh_for_load_flexibility/README.md +186 -0
  16. data/lib/measures/add_central_hpwh_for_load_flexibility/README.md.erb +42 -0
  17. data/lib/measures/add_central_hpwh_for_load_flexibility/docs/Flexible Domestic Hot Water Implementation Guide.pdf +0 -0
  18. data/lib/measures/add_central_hpwh_for_load_flexibility/measure.rb +648 -0
  19. data/lib/measures/add_central_hpwh_for_load_flexibility/measure.xml +398 -0
  20. data/lib/measures/add_central_hpwh_for_load_flexibility/tests/SmallHotel-2A.osm +42893 -0
  21. data/lib/measures/add_central_hpwh_for_load_flexibility/tests/add_central_hpwh_for_load_flexibility.rb +98 -0
  22. data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/LICENSE.md +13 -0
  23. data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/README.md +189 -0
  24. data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/measure.rb +689 -0
  25. data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/measure.xml +253 -0
  26. data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/resources/TESCurves.idf +1059 -0
  27. data/lib/measures/add_distributed_ice_storage_to_air_loop_for_load_flexibility/tests/MeasureTest.osm +9507 -0
  28. 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
  29. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/LICENSE.md +13 -0
  30. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/README.md +264 -0
  31. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/README.md.erb +42 -0
  32. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/docs/Ice Measure Implementation Guide.pdf +0 -0
  33. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/measure.rb +1310 -0
  34. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/measure.xml +506 -0
  35. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/resources/OsLib_Schedules.rb +173 -0
  36. 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
  37. data/lib/measures/add_ice_storage_to_plant_loop_for_load_flexibility/tests/ice_test_model.osm +21523 -0
  38. data/lib/openstudio/load_flexibility_measures.rb +50 -0
  39. data/lib/openstudio/load_flexibility_measures/version.rb +40 -0
  40. data/openstudio-load-flexibility-measures.gemspec +32 -0
  41. 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 %>
@@ -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