openstudio-load-flexibility-measures 0.3.1 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +36 -36
  3. data/.rubocop.yml +6 -9
  4. data/CHANGELOG.html +75 -75
  5. data/CHANGELOG.md +63 -48
  6. data/Gemfile +31 -7
  7. data/Jenkinsfile +11 -10
  8. data/LICENSE.md +27 -0
  9. data/README.md +57 -42
  10. data/Rakefile +15 -15
  11. data/doc_templates/LICENSE.md +26 -26
  12. data/doc_templates/README.md.erb +41 -41
  13. data/doc_templates/copyright_erb.txt +35 -35
  14. data/doc_templates/copyright_js.txt +3 -3
  15. data/doc_templates/copyright_ruby.txt +33 -33
  16. data/lib/measures/add_central_ice_storage/LICENSE.md +26 -26
  17. data/lib/measures/add_central_ice_storage/README.md +264 -264
  18. data/lib/measures/add_central_ice_storage/README.md.erb +41 -41
  19. data/lib/measures/add_central_ice_storage/measure.rb +1325 -1324
  20. data/lib/measures/add_central_ice_storage/measure.xml +503 -503
  21. data/lib/measures/add_central_ice_storage/resources/OsLib_Schedules.rb +171 -173
  22. data/lib/measures/add_central_ice_storage/tests/add_central_ice_storage_test.rb +203 -203
  23. data/lib/measures/add_central_ice_storage/tests/ice_test_model.osm +21523 -21523
  24. data/lib/measures/add_hpwh/LICENSE.md +26 -26
  25. data/lib/measures/add_hpwh/README.md +186 -186
  26. data/lib/measures/add_hpwh/README.md.erb +41 -41
  27. data/lib/measures/add_hpwh/docs/Flexible Domestic Hot Water Implementation Guide.pdf +0 -0
  28. data/lib/measures/add_hpwh/measure.rb +674 -647
  29. data/lib/measures/add_hpwh/measure.xml +402 -397
  30. data/lib/measures/add_hpwh/tests/SmallHotel-2A.osm +42893 -42893
  31. data/lib/measures/add_hpwh/tests/add_hpwh_test.rb +237 -0
  32. data/lib/measures/add_packaged_ice_storage/LICENSE.md +26 -26
  33. data/lib/measures/add_packaged_ice_storage/README.html +185 -185
  34. data/lib/measures/add_packaged_ice_storage/README.md +189 -189
  35. data/lib/measures/add_packaged_ice_storage/measure.rb +694 -691
  36. data/lib/measures/add_packaged_ice_storage/measure.xml +245 -245
  37. data/lib/measures/add_packaged_ice_storage/resources/TESCurves.idf +1059 -1059
  38. data/lib/measures/add_packaged_ice_storage/tests/MeasureTest.osm +9507 -9507
  39. data/lib/measures/add_packaged_ice_storage/tests/add_packaged_ice_storage_test.rb +96 -96
  40. data/lib/openstudio/load_flexibility_measures/version.rb +40 -40
  41. data/lib/openstudio/load_flexibility_measures.rb +50 -50
  42. data/openstudio-load-flexibility-measures.gemspec +33 -34
  43. metadata +25 -24
  44. data/lib/measures/add_hpwh/tests/add_hphw_test.rb +0 -98
@@ -1,647 +1,674 @@
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 AddHphw < 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
- 'Add HPWH for 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.setDefaultValue(2.8)
180
- args << cop
181
-
182
- # create argument for electric backup capacity
183
- bu_cap = OpenStudio::Measure::OSArgument.makeDoubleArgument('bu_cap', true)
184
- bu_cap.setDisplayName('Set electric backup heating capacity')
185
- bu_cap.setDescription('[kW]')
186
- bu_cap.setDefaultValue((23.446 * (default_vol / 80.0)).round(1))
187
- args << bu_cap
188
-
189
- # create argument for maximum tank temperature
190
- max_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('max_temp', true)
191
- max_temp.setDisplayName('Set maximum tank temperature')
192
- max_temp.setDescription('[F]')
193
- max_temp.setDefaultValue(160)
194
- args << max_temp
195
-
196
- # create argument for minimum float temperature
197
- min_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('min_temp', true)
198
- min_temp.setDisplayName('Set minimum tank temperature during float')
199
- min_temp.setDescription('[F]')
200
- min_temp.setDefaultValue(120)
201
- args << min_temp
202
-
203
- # create argument for deadband temperature difference between heat pump setpoint and electric backup
204
- db_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('db_temp', true)
205
- db_temp.setDisplayName('Set deadband temperature difference between heat pump and electric backup')
206
- db_temp.setDescription('[F]')
207
- db_temp.setDefaultValue(5)
208
- args << db_temp
209
-
210
- # find existing temperature setpoint schedules for water heater
211
- all_scheds = model.getSchedules
212
- temp_sched_names = []
213
- default_sched = '--Create New @ 140F--'
214
- default_ambient = ''
215
- all_scheds.each do |sch|
216
- next if sch.scheduleTypeLimits.empty?
217
- next unless sch.scheduleTypeLimits.get.unitType.to_s == 'Temperature'
218
- temp_sched_names << sch.name.to_s
219
- if !wheaters.empty? && (sch.name.to_s == wheaters[0].setpointTemperatureSchedule.get.name.to_s)
220
- default_sched = sch.name.to_s
221
- end
222
- end
223
- temp_sched_names = [default_sched] + temp_sched_names.sort
224
-
225
- # create argument for predefined schedule
226
- sched = OpenStudio::Measure::OSArgument.makeChoiceArgument('sched', temp_sched_names, true)
227
- sched.setDisplayName('Select reference tank setpoint temperature schedule')
228
- sched.setDescription('')
229
- sched.setDefaultValue(temp_sched_names[0])
230
- args << sched
231
-
232
- # define possible flex options
233
- flex_options = ['None', 'Charge - Heat Pump', 'Charge - Electric', 'Float']
234
-
235
- # create choice and string arguments for flex periods
236
- 4.times do |n|
237
- flex = OpenStudio::Measure::OSArgument.makeChoiceArgument('flex' + n.to_s, flex_options, true)
238
- flex.setDisplayName("Daily Flex Period #{n + 1}:")
239
- flex.setDescription('Applies every day in the full run period.')
240
- flex.setDefaultValue('None')
241
- args << flex
242
-
243
- flex_hrs = OpenStudio::Measure::OSArgument.makeStringArgument('flex_hrs' + n.to_s, false)
244
- flex_hrs.setDisplayName('Use 24-Hour Format')
245
- flex_hrs.setDefaultValue('HH:MM - HH:MM')
246
- args << flex_hrs
247
- end
248
-
249
- args
250
- end
251
- ## END USER ARGS -----------------------------------------------------------------------------------------------------
252
-
253
- ## MEASURE RUN -------------------------------------------------------------------------------------------------------
254
- # Index:
255
- # => Argument Validation
256
- # => Controls: Heat Pump Heating Shedule
257
- # => Controls: Tank Electric Backup Heating Schedule
258
- # => Hardware
259
- # => Controls Modifications for Tank
260
- # => Report Output Variables
261
-
262
- # define what happens when the measure is run
263
- def run(model, runner, user_arguments)
264
- super(model, runner, user_arguments)
265
-
266
- ## ARGUMENT VALIDATION ---------------------------------------------------------------------------------------------
267
- # Measure does not immedately return false upon error detection. Errors are accumulated throughout this selection
268
- # before exiting gracefully prior to measure execution.
269
-
270
- # use the built-in error checking
271
- unless runner.validateUserArguments(arguments(model), user_arguments)
272
- return false
273
- end
274
-
275
- # report initial condition of model
276
- tanks_ic = model.getWaterHeaterMixeds.size + model.getWaterHeaterStratifieds.size
277
- hpwh_ic = model.getWaterHeaterHeatPumps.size + model.getWaterHeaterHeatPumpWrappedCondensers.size
278
- runner.registerInitialCondition("The building started with #{tanks_ic} water heater tank(s) and " \
279
- "#{hpwh_ic} heat pump water heater(s).")
280
-
281
- # create empty arrays and initialize variables for future use
282
- flex = []
283
- flex_type = []
284
- flex_hrs = []
285
- time_check = []
286
- hours = []
287
- minutes = []
288
- flex_times = []
289
-
290
- # assign the user inputs to variables
291
- remove_wh = runner.getBoolArgumentValue('remove_wh', user_arguments)
292
- loop = runner.getStringArgumentValue('loop', user_arguments)
293
- zone = runner.getStringArgumentValue('zone', user_arguments)
294
- type = runner.getStringArgumentValue('type', user_arguments)
295
- cap = runner.getDoubleArgumentValue('cap', user_arguments)
296
- cop = runner.getDoubleArgumentValue('cop', user_arguments)
297
- bu_cap = runner.getDoubleArgumentValue('bu_cap', user_arguments)
298
- vol = runner.getDoubleArgumentValue('vol', user_arguments)
299
- max_temp = runner.getDoubleArgumentValue('max_temp', user_arguments)
300
- min_temp = runner.getDoubleArgumentValue('min_temp', user_arguments)
301
- db_temp = runner.getDoubleArgumentValue('db_temp', user_arguments)
302
- sched = runner.getStringArgumentValue('sched', user_arguments)
303
-
304
- 4.times do |n|
305
- flex << runner.getStringArgumentValue('flex' + n.to_s, user_arguments)
306
- flex_hrs << runner.getStringArgumentValue('flex_hrs' + n.to_s, user_arguments)
307
- end
308
-
309
- # check for error inputs
310
- if loop.include?('Error')
311
- runner.registerError('No service hot water loop was found. Measure did not run.')
312
- end
313
-
314
- if zone.include?('Error')
315
- runner.registerError('No thermal zone was found. Measure did not run.')
316
- end
317
-
318
- # check capacity, volume, and temps for reasonableness
319
- if cap < 5
320
- runner.registerWarning('HPWH heating capacity is less than 5kW ( 17kBtu/hr)')
321
- end
322
-
323
- if bu_cap < 5
324
- runner.registerWarning('Backup heating capaicty is less than 5kW ( 17kBtu/hr).')
325
- end
326
-
327
- if vol < 40
328
- runner.registerWarning('Tank has less than 40 gallon capacity; check heat pump sizing if model fails.')
329
- end
330
-
331
- if min_temp < 120
332
- runner.registerWarning('Minimum tank temperature is very low; consider increasing to at least 120F.')
333
- runner.registerWarning('Do not store water for long periods at temperatures below 135-140F as those ' \
334
- 'conditions facilitate the growth of Legionella.')
335
- end
336
-
337
- if max_temp > 180
338
- runner.registerWarning('Maximum charging temperature exceeded practical limits; reset to 180F.')
339
- max_temp = 180.0
340
- end
341
-
342
- if max_temp > 160
343
- runner.registerWarning("#{max_temp}F is above or near the limit of the HP performance curves. If the " \
344
- 'simulation fails with cooling capacity less than 0, you have exceeded performance ' \
345
- 'limits. Consider setting max temp to less than 160F.')
346
- end
347
-
348
- # check selected schedule and set flag for later use
349
- sched_flag = false # flag for either creating new (false) or modifying existing (true) schedule
350
- if sched == '--Create New @ 140F--'
351
- runner.registerInfo('No reference water heater temperature setpoint schedule was selected; a new one ' \
352
- 'will be created.')
353
- else
354
- sched_flag = true
355
- runner.registerInfo("#{sched} will be used as the water heater temperature setpoint schedule.")
356
- end
357
-
358
- # parse flex_hrs into hours and minuts arrays
359
- idx = 0
360
- flex_hrs.each do |fh|
361
- if flex[idx] != 'None'
362
- data = fh.split(/[-:]/)
363
- data.each { |e| e.delete!(' ') }
364
- if data[2] > data[0]
365
- flex_type << flex[idx]
366
- hours << data[0]
367
- hours << data[2]
368
- minutes << data[1]
369
- minutes << data[3]
370
- else
371
- flex_type << flex[idx]
372
- flex_type << flex[idx]
373
- hours << 0
374
- hours << data[2]
375
- hours << data[0]
376
- hours << 24
377
- minutes << 0
378
- minutes << data[3]
379
- minutes << data[1]
380
- minutes << 0
381
- end
382
- end
383
- idx += 1
384
- end
385
-
386
- # convert hours and minutes into OS:Time objects
387
- idx = 0
388
- hours.each do |h|
389
- flex_times << OpenStudio::Time.new(0, h.to_i, minutes[idx].to_i, 0)
390
- idx += 1
391
- end
392
-
393
- # flex.delete('None')
394
-
395
- runner.registerInfo("A total of #{idx / 2} flex periods will be added to the selected water heater setpoint schedule.")
396
-
397
- # exit gracefully if errors registered above
398
- return false unless runner.result.errors.empty?
399
- ## END ARGUMENT VALIDATION -----------------------------------------------------------------------------------------
400
-
401
- ## CONTROLS: HEAT PUMP HEATING TEMPERATURE SETPOINT SCHEDULE -------------------------------------------------------
402
- # This section creates the heat pump heating temperature setpoint schedule with flex periods
403
- # The tank schedule is created here
404
-
405
- # find or create new reference temperature schedule based on sched_flag value
406
- if sched_flag # schedule already exists and must be modified
407
- # converts the STRING into a MODEL OBJECT, same variable name
408
- sched = model.getScheduleRulesetByName(sched).get.clone.to_ScheduleRuleset.get
409
- else
410
- # must create new water heater setpoint temperature schedule at 140F
411
- sched = OpenStudio::Model::ScheduleRuleset.new(model, 60)
412
- end
413
-
414
- # rename and duplicate for later modification
415
- sched.setName('Heat Pump Heating Temperature Setpoint')
416
- sched.defaultDaySchedule.setName('Heat Pump Heating Temperature Setpoint Default')
417
-
418
- # tank_sched = sched.clone.to_ScheduleRuleset.get
419
- tank_sched = OpenStudio::Model::ScheduleRuleset.new(model, 60 - (db_temp / 1.8 + 2))
420
- tank_sched.setName('Tank Electric Heater Setpoint')
421
- tank_sched.defaultDaySchedule.setName('Tank Electric Heater Setpoint Default')
422
-
423
- # grab default day and time-value pairs for modification
424
- d_day = sched.defaultDaySchedule
425
- old_times = d_day.times
426
- old_values = d_day.values
427
- new_values = Array.new(flex_times.size, 2)
428
-
429
- # find existing values in reference schedule and grab for use in new-rule creation
430
- flex_times.size.times do |i|
431
- if i.even?
432
- n = 0
433
- old_times.each do |ot|
434
- new_values[i] = old_values[n] if flex_times[i] <= ot
435
- n += 1
436
- end
437
- elsif flex_type[(i / 2).floor] == 'Charge - Heat Pump'
438
- new_values[i] = OpenStudio.convert(max_temp, 'F', 'C').get
439
- elsif flex_type[(i / 2).floor] == 'Float' || flex_type[(i / 2).floor] == 'Charge - Electric'
440
- new_values[i] = OpenStudio.convert(min_temp, 'F', 'C').get
441
- end
442
- end
443
-
444
- # create new rules and add to default day based on flex period options above
445
- idx = 0
446
- flex_times.each do |ft|
447
- d_day.addValue(ft, new_values[idx])
448
- idx += 1
449
- end
450
-
451
- ## END CONTROLS: HEAT PUMP HEATING TEMPERATURE SETPOINT SCHEDULE ---------------------------------------------------
452
-
453
- ## CONTROLS: TANK TEMPERATURE SETPOINT SCHEDULE (ELECTRIC BACKUP) --------------------------------------------------
454
- # This section creates the setpoint temperature schedule for the electric backup heating coils in the water tank
455
-
456
- # grab default day and time-value pairs for modification
457
- d_day = tank_sched.defaultDaySchedule
458
- old_times = d_day.times
459
- old_values = d_day.values
460
- new_values = Array.new(flex_times.size, 2)
461
-
462
- # find existing values in reference schedule and grab for use in new-rule creation
463
- flex_times.size.times do |i|
464
- if i.even?
465
- n = 0
466
- old_times.each do |ot|
467
- new_values[i] = old_values[n] if flex_times[i] <= ot
468
- n += 1
469
- end
470
- elsif flex_type[(i / 2).floor] == 'Charge - Electric'
471
- new_values[i] = OpenStudio.convert(max_temp, 'F', 'C').get
472
- elsif flex_type[(i / 2).floor] == 'Float' # || flex_type[(i/2).floor] == 'Charge - Heat Pump'
473
- new_values[i] = OpenStudio.convert(min_temp - db_temp, 'F', 'C').get
474
- elsif flex_type[(i / 2).floor] == 'Charge - Heat Pump'
475
- new_values[i] = 60 - (db_temp / 1.8)
476
- end
477
- end
478
-
479
- # create new rules and add to default day based on flex period options above
480
- idx = 0
481
- flex_times.each do |ft|
482
- d_day.addValue(ft, new_values[idx])
483
- idx += 1
484
- end
485
-
486
- ## CONTROLS: TANK TEMPERATURE SETPOINT SCHEDULE (ELECTRIC BACKUP) --------------------------------------------------
487
-
488
- ## HARDWARE --------------------------------------------------------------------------------------------------------
489
- # This section adds the selected type of heat pump water heater to the supply side of the selected loop. If
490
- # selected, measure will remove any existing water heaters on the supply side of the loop. If old heater(s) are left
491
- # in place, the new HPWH tank will be placed in front (to the left) of them.
492
-
493
- # use OS standards build - arbitrary selection, but NZE Ready seems appropriate
494
- std = Standard.build('NREL ZNE Ready 2017')
495
-
496
- # create empty arrays and initialize variables for later use
497
- old_heater = []
498
- count = 0
499
-
500
- # convert loop and zone names from STRINGS into OS model OBJECTS
501
- zone = model.getThermalZoneByName(zone).get
502
- loop = model.getPlantLoopByName(loop).get
503
-
504
- # find and locate old water heater on selected loop, if applicable
505
- loop_equip = loop.supplyComponents
506
- loop_equip.each do |le|
507
- if le.iddObject.name.include?('WaterHeater:Mixed')
508
- old_heater << model.getWaterHeaterMixedByName(le.name.to_s).get
509
- count += 1
510
- elsif le.iddObject.name.include?('WaterHeater:Stratified')
511
- old_heater << model.getWaterHeaterStratifiedByName(le.name.to_s).get
512
- count += 1
513
- end
514
- end
515
-
516
- unless old_heater.empty?
517
- inlet = old_heater[0].supplyInletModelObject.get.to_Node.get
518
- outlet = old_heater[0].supplyOutletModelObject.get.to_Node.get
519
- end
520
-
521
- # Add heat pump water heater and attach to selected loop
522
- # Reference: https://github.com/NREL/openstudio-standards/blob/master/lib/
523
- # => openstudio-standards/prototypes/common/objects/Prototype.ServiceWaterHeating.rb
524
- if type != 'Simplified'
525
- hpwh = std.model_add_heatpump_water_heater(model, # model
526
- type: type, # type
527
- water_heater_capacity: (cap * 1000 / cop), # water_heater_capacity
528
- electric_backup_capacity: (bu_cap * 1000), # electric_backup_capacity
529
- water_heater_volume: OpenStudio.convert(vol, 'gal', 'm^3').get, # water_heater_volume
530
- service_water_temperature: OpenStudio.convert(140.0, 'F', 'C').get, # service_water_temperature
531
- parasitic_fuel_consumption_rate: 3.0, # parasitic_fuel_consumption_rate
532
- swh_temp_sch: sched, # swh_temp_sch
533
- cop: cop, # cop
534
- shr: 0.88, # shr
535
- tank_ua: 3.9, # tank_ua
536
- set_peak_use_flowrate: false, # set_peak_use_flowrate
537
- peak_flowrate: 0.0, # peak_flowrate
538
- flowrate_schedule: nil, # flowrate_schedule
539
- water_heater_thermal_zone: zone) # water_heater_thermal_zone
540
- else
541
- hpwh = std.model_add_water_heater(model, # model
542
- (cap * 1000), # water_heater_capacity
543
- OpenStudio.convert(vol, 'gal', 'm^3').get, # water_heater_volume
544
- 'HeatPump', # water_heater_fuel
545
- OpenStudio.convert(140.0, 'F', 'C').get, # service_water_temperature
546
- 3.0, # parasitic_fuel_consumption_rate
547
- sched, # swh_temp_sch
548
- false, # set_peak_use_flowrate
549
- 0.0, # peak_flowrate
550
- nil, # flowrate_schedule
551
- zone, # water_heater_thermal_zone
552
- 1) # number_water_heaters
553
- end
554
-
555
- # add tank to appropriate branch and node (will be placed first in series if old tanks not removed)
556
- # modify objects as ncessary
557
- if old_heater.empty?
558
- loop.addSupplyBranchForComponent(hpwh.tank)
559
- elsif type != 'Simplified'
560
- hpwh.tank.addToNode(inlet)
561
- hpwh.setDeadBandTemperatureDifference(db_temp / 1.8)
562
- runner.registerInfo("#{hpwh.tank.name} was added to the model on #{loop.name}")
563
- else
564
- hpwh.addToNode(inlet)
565
- hpwh.setMaximumTemperatureLimit(OpenStudio.convert(max_temp, 'F', 'C').get)
566
- runner.registerInfo("#{hpwh.name} was added to the model on #{loop.name}")
567
- end
568
-
569
- # remove old tank objects if necessary
570
- if remove_wh
571
- old_heater.each do |oh|
572
- runner.registerInfo("#{oh.name} was removed from the model.")
573
- oh.remove
574
- end
575
- end
576
- ## END HARDWARE ----------------------------------------------------------------------------------------------------
577
-
578
- ## CONTROLS MODIFICATIONS FOR TANK ---------------------------------------------------------------------------------
579
- # apply schedule to tank
580
- if type == 'PumpedCondenser'
581
- hpwh.tank.to_WaterHeaterMixed.get.setSetpointTemperatureSchedule(tank_sched)
582
- elsif type == 'WrappedCondenser'
583
- hpwh.tank.to_WaterHeaterStratified.get.setHeater1SetpointTemperatureSchedule(tank_sched)
584
- hpwh.tank.to_WaterHeaterStratified.get.setHeater2SetpointTemperatureSchedule(tank_sched)
585
- elsif type == 'Simplified'
586
- runner.registerInfo('Line 492 was used. Nothing done here yet... Check tank temperature schedules...')
587
- end
588
- ## END CONTROLS MODIFICATIONS FOR TANK -----------------------------------------------------------------------------
589
-
590
- ## ADD REPORTED VARIABLES ------------------------------------------------------------------------------------------
591
-
592
- ovar_names = ['Cooling Coil Total Cooling Rate',
593
- 'Cooling Coil Total Water Heating Rate',
594
- 'Cooling Coil Water Heating Electric Power',
595
- 'Cooling Coil Crankcase Heater Electric Power',
596
- 'Water Heater Tank Temperature',
597
- 'Water Heater Heat Loss Rate',
598
- 'Water Heater Heating Rate',
599
- 'Water Heater Use Side Heat Transfer Rate',
600
- 'Water Heater Source Side Heat Transfer Rate',
601
- 'Water Heater Unmet Demand Heat Transfer Rate',
602
- 'Water Heater Electricity Rate',
603
- 'Water Heater Water Volume Flow Rate',
604
- 'Water Use Connections Hot Water Temperature']
605
-
606
- # Create new output variable objects
607
- ovars = []
608
- ovar_names.each do |nm|
609
- ovars << OpenStudio::Model::OutputVariable.new(nm, model)
610
- end
611
-
612
- # add temperate schedule outputs - clean up and put names into array, then loop over setting key values
613
- v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
614
- v.setKeyValue(sched.name.to_s)
615
- ovars << v
616
-
617
- v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
618
- v.setKeyValue(tank_sched.name.to_s)
619
- ovars << v
620
-
621
- if type != 'Simplified'
622
- v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
623
- v.setKeyValue(tank_sched.name.to_s)
624
- ovars << v
625
- end
626
-
627
- # Set variable reporting frequency for newly created output variables
628
- ovars.each do |var|
629
- var.setReportingFrequency('TimeStep')
630
- end
631
-
632
- # Register info re: output variables:
633
- runner.registerInfo("#{ovars.size} output variables were added to the model.")
634
- ## END ADD REPORTED VARIABLES --------------------------------------------------------------------------------------
635
-
636
- # Register final condition
637
- hpwh_fc = model.getWaterHeaterHeatPumps.size + model.getWaterHeaterHeatPumpWrappedCondensers.size
638
- tanks_fc = model.getWaterHeaterMixeds.size + model.getWaterHeaterStratifieds.size
639
- runner.registerFinalCondition("The building finshed with #{tanks_fc} water heater tank(s) and " \
640
- "#{hpwh_fc} heat pump water heater(s).")
641
-
642
- true
643
- end
644
- end
645
-
646
- # register the measure to be used by the application
647
- AddHphw.new.registerWithApplication
1
+ # *******************************************************************************
2
+ # OpenStudio(R), Copyright (c) 2008-2021, 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 AddHpwh < 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
+ 'Add HPWH for 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?')
91
+ remove_wh.setDescription('')
92
+ remove_wh.setDefaultValue(true)
93
+ args << remove_wh
94
+
95
+ # find available water heaters and get default volume
96
+ default_vol = 80.0 # gallons
97
+ wheaters = []
98
+ wh_names = ['All Water Heaters (Simplified Only)']
99
+ if !model.getWaterHeaterMixeds.empty?
100
+ wheaters = model.getWaterHeaterMixeds
101
+ wheaters.each do |w|
102
+ if w.tankVolume.to_f > OpenStudio.convert(39, 'gal', 'm^3').to_f
103
+ wh_names << w.name.to_s
104
+ default_vol = [default_vol, (w.tankVolume.to_f / 0.0037854118).round(1)].max
105
+ end
106
+ end
107
+ end
108
+
109
+ wh = OpenStudio::Measure::OSArgument.makeChoiceArgument('wh', wh_names, true)
110
+ wh.setDisplayName('Select 40+ gallon water heater to replace or augment')
111
+ wh.setDescription("All can only be used with the 'Simplified' model")
112
+ wh.setDefaultValue(wh_names[0])
113
+ args << wh
114
+
115
+ # create argument for hot water tank volume
116
+ vol = OpenStudio::Measure::OSArgument.makeDoubleArgument('vol', false)
117
+ vol.setDisplayName('Set hot water tank volume [gal]')
118
+ vol.setDescription('Enter 0 to use existing tank volume(s). Values less than 5 are treated as sizing multipliers.')
119
+ vol.setUnits('gal')
120
+ vol.setDefaultValue(0)
121
+ args << vol
122
+
123
+ # create argument for water heater type
124
+ type = OpenStudio::Measure::OSArgument.makeChoiceArgument('type',
125
+ ['Simplified', 'PumpedCondenser', 'WrappedCondenser'], true)
126
+ type.setDisplayName('Select heat pump water heater type')
127
+ type.setDescription('')
128
+ type.setDefaultValue('Simplified')
129
+ args << type
130
+
131
+ # find available spaces for heater location
132
+ zone_names = []
133
+ unless model.getThermalZones.empty?
134
+ zones = model.getThermalZones
135
+ zones.each do |zn|
136
+ zone_names << zn.name.to_s
137
+ end
138
+ zone_names.sort!
139
+ end
140
+
141
+ zone_names << 'Error: No Thermal Zones Found' if zone_names.empty?
142
+ zone_names = ['N/A - Simplified'] + zone_names
143
+
144
+ # create argument for thermal zone selection (location of water heater)
145
+ zone = OpenStudio::Measure::OSArgument.makeChoiceArgument('zone', zone_names, true)
146
+ zone.setDisplayName('Select thermal zone for HP evaporator')
147
+ zone.setDescription("Does not apply to 'Simplified' cases")
148
+ zone.setDefaultValue(zone_names[0])
149
+ args << zone
150
+
151
+ # create argument for heat pump capacity
152
+ cap = OpenStudio::Measure::OSArgument.makeDoubleArgument('cap', true)
153
+ cap.setDisplayName('Set heat pump heating capacity')
154
+ cap.setDescription('[kW]')
155
+ cap.setDefaultValue((23.446 * (default_vol / 80.0)).round(1))
156
+ args << cap
157
+
158
+ # create argument for heat pump rated cop
159
+ cop = OpenStudio::Measure::OSArgument.makeDoubleArgument('cop', true)
160
+ cop.setDisplayName('Set heat pump rated COP (heating)')
161
+ cop.setDefaultValue(3.2)
162
+ args << cop
163
+
164
+ # create argument for electric backup capacity
165
+ bu_cap = OpenStudio::Measure::OSArgument.makeDoubleArgument('bu_cap', true)
166
+ bu_cap.setDisplayName('Set electric backup heating capacity')
167
+ bu_cap.setDescription('[kW]')
168
+ bu_cap.setDefaultValue((23.446 * (default_vol / 80.0)).round(1))
169
+ args << bu_cap
170
+
171
+ # create argument for maximum tank temperature
172
+ max_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('max_temp', true)
173
+ max_temp.setDisplayName('Set maximum tank temperature')
174
+ max_temp.setDescription('[F]')
175
+ max_temp.setDefaultValue(160)
176
+ args << max_temp
177
+
178
+ # create argument for minimum float temperature
179
+ min_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('min_temp', true)
180
+ min_temp.setDisplayName('Set minimum tank temperature during float')
181
+ min_temp.setDescription('[F]')
182
+ min_temp.setDefaultValue(120)
183
+ args << min_temp
184
+
185
+ # create argument for deadband temperature difference between heat pump setpoint and electric backup
186
+ db_temp = OpenStudio::Measure::OSArgument.makeDoubleArgument('db_temp', true)
187
+ db_temp.setDisplayName('Set deadband temperature difference between heat pump and electric backup')
188
+ db_temp.setDescription('[F]')
189
+ db_temp.setDefaultValue(5)
190
+ args << db_temp
191
+
192
+ # find existing temperature setpoint schedules for water heater
193
+ all_scheds = model.getSchedules
194
+ temp_sched_names = []
195
+ default_sched = '--Create New @ 140F--'
196
+ default_ambient = ''
197
+ all_scheds.each do |sch|
198
+ next if sch.scheduleTypeLimits.empty?
199
+ next unless sch.scheduleTypeLimits.get.unitType.to_s == 'Temperature'
200
+
201
+ temp_sched_names << sch.name.to_s
202
+ if !wheaters.empty? && (sch.name.to_s == wheaters[0].setpointTemperatureSchedule.get.name.to_s)
203
+ default_sched = sch.name.to_s
204
+ end
205
+ end
206
+ temp_sched_names = [default_sched] + temp_sched_names.sort
207
+
208
+ # create argument for predefined schedule
209
+ sched = OpenStudio::Measure::OSArgument.makeChoiceArgument('sched', temp_sched_names, true)
210
+ sched.setDisplayName('Select reference tank setpoint temperature schedule')
211
+ sched.setDescription('')
212
+ sched.setDefaultValue(temp_sched_names[0])
213
+ args << sched
214
+
215
+ # define possible flex options
216
+ flex_options = ['None', 'Charge - Heat Pump', 'Charge - Electric', 'Float']
217
+
218
+ # create choice and string arguments for flex periods
219
+ 4.times do |n|
220
+ flex = OpenStudio::Measure::OSArgument.makeChoiceArgument("flex#{n}", flex_options, true)
221
+ flex.setDisplayName("Daily Flex Period #{n + 1}:")
222
+ flex.setDescription('Applies every day in the full run period.')
223
+ flex.setDefaultValue('None')
224
+ args << flex
225
+
226
+ flex_hrs = OpenStudio::Measure::OSArgument.makeStringArgument("flex_hrs#{n}", false)
227
+ flex_hrs.setDisplayName('Use 24-Hour Format')
228
+ flex_hrs.setDefaultValue('HH:MM - HH:MM')
229
+ args << flex_hrs
230
+ end
231
+
232
+ args
233
+ end
234
+ ## END USER ARGS -----------------------------------------------------------------------------------------------------
235
+
236
+ ## MEASURE RUN -------------------------------------------------------------------------------------------------------
237
+ # Index:
238
+ # => Argument Validation
239
+ # => Controls: Heat Pump Heating Shedule
240
+ # => Controls: Tank Electric Backup Heating Schedule
241
+ # => Hardware
242
+ # => Controls Modifications for Tank
243
+ # => Report Output Variables
244
+
245
+ # define what happens when the measure is run
246
+ def run(model, runner, user_arguments)
247
+ super(model, runner, user_arguments)
248
+
249
+ ## ARGUMENT VALIDATION ---------------------------------------------------------------------------------------------
250
+ # Measure does not immedately return false upon error detection. Errors are accumulated throughout this selection
251
+ # before exiting gracefully prior to measure execution.
252
+
253
+ # use the built-in error checking
254
+ unless runner.validateUserArguments(arguments(model), user_arguments)
255
+ return false
256
+ end
257
+
258
+ # report initial condition of model
259
+ tanks_ic = model.getWaterHeaterMixeds.size + model.getWaterHeaterStratifieds.size
260
+ hpwh_ic = model.getWaterHeaterHeatPumps.size + model.getWaterHeaterHeatPumpWrappedCondensers.size
261
+ runner.registerInitialCondition("The building started with #{tanks_ic} water heater tank(s) and " \
262
+ "#{hpwh_ic} heat pump water heater(s).")
263
+
264
+ # create empty arrays and initialize variables for future use
265
+ flex = []
266
+ flex_type = []
267
+ flex_hrs = []
268
+ time_check = []
269
+ hours = []
270
+ minutes = []
271
+ flex_times = []
272
+
273
+ # assign the user inputs to variables
274
+ remove_wh = runner.getBoolArgumentValue('remove_wh', user_arguments)
275
+ wh = runner.getStringArgumentValue('wh', user_arguments)
276
+ vol = runner.getDoubleArgumentValue('vol', user_arguments)
277
+ type = runner.getStringArgumentValue('type', user_arguments)
278
+ zone = runner.getStringArgumentValue('zone', user_arguments)
279
+ cap = runner.getDoubleArgumentValue('cap', user_arguments)
280
+ cop = runner.getDoubleArgumentValue('cop', user_arguments)
281
+ bu_cap = runner.getDoubleArgumentValue('bu_cap', user_arguments)
282
+ max_temp = runner.getDoubleArgumentValue('max_temp', user_arguments)
283
+ min_temp = runner.getDoubleArgumentValue('min_temp', user_arguments)
284
+ db_temp = runner.getDoubleArgumentValue('db_temp', user_arguments)
285
+ sched = runner.getStringArgumentValue('sched', user_arguments)
286
+
287
+ # get zone of one was selected
288
+ if zone.to_s != 'N/A - Simplified'
289
+ if model.getThermalZoneByName(zone).is_initialized
290
+ zone = model.getThermalZoneByName(zone).get
291
+ else
292
+ runner.registerError("Could not find zone named #{zone} in the moodel")
293
+ return false
294
+ end
295
+ else
296
+ zone = 'N/A - Simplified'
297
+ end
298
+
299
+ 4.times do |n|
300
+ flex << runner.getStringArgumentValue("flex#{n}", user_arguments)
301
+ flex_hrs << runner.getStringArgumentValue("flex_hrs#{n}", user_arguments)
302
+ end
303
+
304
+ # check for existence of water heaters (if "all" is selected)
305
+ if model.getWaterHeaterMixeds.empty?
306
+ runner.registerError('No water heaters found in the model')
307
+ return false
308
+ end
309
+
310
+ # Alert user to "simplified" selection
311
+ if type == 'Simplified'
312
+ runner.registerInfo('NOTE: The simplified model is used, so heat pump objects are not employed.')
313
+ end
314
+
315
+ # check capacity, volume, and temps for reasonableness
316
+ if cap < 5
317
+ runner.registerWarning('HPWH heating capacity is less than 5kW ( 17kBtu/hr)')
318
+ end
319
+
320
+ if bu_cap < 5
321
+ runner.registerWarning('Backup heating capaicty is less than 5kW ( 17kBtu/hr).')
322
+ end
323
+
324
+ if vol == 0
325
+ runner.registerInfo('Tank volume was not specified, using existing tank capacity.')
326
+ elsif vol < 40
327
+ runner.registerWarning('Tank has less than 40 gallon capacity; check heat pump sizing if model fails.')
328
+ end
329
+
330
+ if min_temp < 120
331
+ runner.registerWarning('Minimum tank temperature is very low; consider increasing to at least 120F.')
332
+ runner.registerWarning('Do not store water for long periods at temperatures below 135-140F as those ' \
333
+ 'conditions facilitate the growth of Legionella.')
334
+ end
335
+
336
+ if max_temp > 185
337
+ runner.registerWarning('Maximum charging temperature exceeded practical limits; reset to 185F.')
338
+ max_temp = 185.0
339
+ end
340
+
341
+ if max_temp > 170
342
+ runner.registerWarning("#{max_temp}F is above or near the limit of the HP performance curves. If the " \
343
+ 'simulation fails with cooling capacity less than 0, you have exceeded performance ' \
344
+ 'limits. Consider setting max temp to less than 170F.')
345
+ end
346
+
347
+ # check selected schedule and set flag for later use
348
+ sched_flag = false # flag for either creating new (false) or modifying existing (true) schedule
349
+ if sched == '--Create New @ 140F--'
350
+ runner.registerInfo('No reference water heater temperature setpoint schedule was selected; a new one ' \
351
+ 'will be created.')
352
+ else
353
+ sched_flag = true
354
+ runner.registerInfo("#{sched} will be used as the water heater temperature setpoint schedule.")
355
+ end
356
+
357
+ # parse flex_hrs into hours and minuts arrays
358
+ idx = 0
359
+ flex_hrs.each do |fh|
360
+ if flex[idx] != 'None'
361
+ data = fh.split(/[-:]/)
362
+ data.each { |e| e.delete!(' ') }
363
+ if data[2] > data[0]
364
+ flex_type << flex[idx]
365
+ hours << data[0]
366
+ hours << data[2]
367
+ minutes << data[1]
368
+ minutes << data[3]
369
+ else
370
+ flex_type << flex[idx]
371
+ flex_type << flex[idx]
372
+ hours << 0
373
+ hours << data[2]
374
+ hours << data[0]
375
+ hours << 24
376
+ minutes << 0
377
+ minutes << data[3]
378
+ minutes << data[1]
379
+ minutes << 0
380
+ end
381
+ end
382
+ idx += 1
383
+ end
384
+
385
+ # convert hours and minutes into OS:Time objects
386
+ idx = 0
387
+ hours.each do |h|
388
+ flex_times << OpenStudio::Time.new(0, h.to_i, minutes[idx].to_i, 0)
389
+ idx += 1
390
+ end
391
+
392
+ # flex.delete('None')
393
+
394
+ runner.registerInfo("A total of #{idx / 2} flex periods will be added to the selected water heater setpoint schedule.")
395
+
396
+ # exit gracefully if errors registered above
397
+ return false unless runner.result.errors.empty?
398
+
399
+ ## END ARGUMENT VALIDATION -----------------------------------------------------------------------------------------
400
+
401
+ ## CONTROLS: HEAT PUMP HEATING TEMPERATURE SETPOINT SCHEDULE -------------------------------------------------------
402
+ # This section creates the heat pump heating temperature setpoint schedule with flex periods
403
+ # The tank schedule is created here
404
+
405
+ # find or create new reference temperature schedule based on sched_flag value
406
+ if sched_flag # schedule already exists and must be modified
407
+ # converts the STRING into a MODEL OBJECT, same variable name
408
+ sched = model.getScheduleRulesetByName(sched).get.clone.to_ScheduleRuleset.get
409
+ else
410
+ # must create new water heater setpoint temperature schedule at 140F
411
+ sched = OpenStudio::Model::ScheduleRuleset.new(model, 60)
412
+ end
413
+
414
+ # rename and duplicate for later modification
415
+ sched.setName('Heat Pump Heating Temperature Setpoint')
416
+ sched.defaultDaySchedule.setName('Heat Pump Heating Temperature Setpoint Default')
417
+
418
+ # tank_sched = sched.clone.to_ScheduleRuleset.get
419
+ tank_sched = OpenStudio::Model::ScheduleRuleset.new(model, 60 - (db_temp / 1.8 + 2))
420
+ tank_sched.setName('Tank Electric Heater Setpoint')
421
+ tank_sched.defaultDaySchedule.setName('Tank Electric Heater Setpoint Default')
422
+
423
+ # grab default day and time-value pairs for modification
424
+ d_day = sched.defaultDaySchedule
425
+ old_times = d_day.times
426
+ old_values = d_day.values
427
+ new_values = Array.new(flex_times.size, 2)
428
+
429
+ # find existing values in reference schedule and grab for use in new-rule creation
430
+ flex_times.size.times do |i|
431
+ if i.even?
432
+ n = 0
433
+ old_times.each do |ot|
434
+ new_values[i] = old_values[n] if flex_times[i] <= ot
435
+ n += 1
436
+ end
437
+ elsif flex_type[(i / 2).floor] == 'Charge - Heat Pump'
438
+ new_values[i] = OpenStudio.convert(max_temp, 'F', 'C').get
439
+ elsif flex_type[(i / 2).floor] == 'Float' || flex_type[(i / 2).floor] == 'Charge - Electric'
440
+ new_values[i] = OpenStudio.convert(min_temp, 'F', 'C').get
441
+ end
442
+ end
443
+
444
+ # create new rules and add to default day based on flex period options above
445
+ idx = 0
446
+ flex_times.each do |ft|
447
+ d_day.addValue(ft, new_values[idx])
448
+ idx += 1
449
+ end
450
+
451
+ ## END CONTROLS: HEAT PUMP HEATING TEMPERATURE SETPOINT SCHEDULE ---------------------------------------------------
452
+
453
+ ## CONTROLS: TANK TEMPERATURE SETPOINT SCHEDULE (ELECTRIC BACKUP) --------------------------------------------------
454
+ # This section creates the setpoint temperature schedule for the electric backup heating coils in the water tank
455
+
456
+ # grab default day and time-value pairs for modification
457
+ d_day = tank_sched.defaultDaySchedule
458
+ old_times = d_day.times
459
+ old_values = d_day.values
460
+ new_values = Array.new(flex_times.size, 2)
461
+
462
+ # find existing values in reference schedule and grab for use in new-rule creation
463
+ flex_times.size.times do |i|
464
+ if i.even?
465
+ n = 0
466
+ old_times.each do |ot|
467
+ new_values[i] = old_values[n] if flex_times[i] <= ot
468
+ n += 1
469
+ end
470
+ elsif flex_type[(i / 2).floor] == 'Charge - Electric'
471
+ new_values[i] = OpenStudio.convert(max_temp, 'F', 'C').get
472
+ elsif flex_type[(i / 2).floor] == 'Float' # || flex_type[(i/2).floor] == 'Charge - Heat Pump'
473
+ new_values[i] = OpenStudio.convert(min_temp - db_temp, 'F', 'C').get
474
+ elsif flex_type[(i / 2).floor] == 'Charge - Heat Pump'
475
+ new_values[i] = 60 - (db_temp / 1.8)
476
+ end
477
+ end
478
+
479
+ # create new rules and add to default day based on flex period options above
480
+ idx = 0
481
+ flex_times.each do |ft|
482
+ d_day.addValue(ft, new_values[idx])
483
+ idx += 1
484
+ end
485
+
486
+ ## END CONTROLS: TANK TEMPERATURE SETPOINT SCHEDULE (ELECTRIC BACKUP) ----------------------------------------------
487
+
488
+ ## HARDWARE --------------------------------------------------------------------------------------------------------
489
+ # This section adds the selected type of heat pump water heater to the supply side of the selected loop. If
490
+ # selected, measure will remove any existing water heaters on the supply side of the loop. If old heater(s) are left
491
+ # in place, the new HPWH tank will be placed in front (to the left) of them.
492
+
493
+ # use OS standards build - arbitrary selection, but NZE Ready seems appropriate
494
+ std = Standard.build('NREL ZNE Ready 2017')
495
+
496
+ #####
497
+ # get the selected water heaters
498
+ whtrs = []
499
+ model.getWaterHeaterMixeds.each do |w|
500
+ case wh
501
+ when 'All Water Heaters (Simplified Only)'
502
+ # exclude booster tanks (<10gal):
503
+ if w.tankVolume.to_f < 0.037854
504
+ next
505
+ else
506
+ whtrs << w
507
+ end
508
+ when w.name.to_s
509
+ whtrs << w
510
+ end
511
+ end
512
+
513
+ whtrs.each do |whtr|
514
+ # create empty arrays and initialize variables for later use
515
+ old_heater = []
516
+ count = 0
517
+
518
+ # get the appropriate plant loop
519
+ loop = ''
520
+ loops = model.getPlantLoops
521
+ loops.each do |l|
522
+ l.supplyComponents.each do |c|
523
+ if c.name.to_s == whtr.name.to_s
524
+ loop = l
525
+ end
526
+ end
527
+ end
528
+
529
+ # use existing tank volume unless otherwise specified
530
+ # values between 0.0 and 5.0 are considered tank sizing multipliers
531
+ if vol == 0
532
+ v = whtr.tankVolume
533
+ elsif (vol > 0.0) && (vol < 5.0)
534
+ v = whtr.tankVolume.to_f * vol
535
+ else
536
+ v = OpenStudio.convert(vol, 'gal', 'm^3').get
537
+ end
538
+
539
+ inlet = whtr.supplyInletModelObject.get.to_Node.get
540
+ outlet = whtr.supplyOutletModelObject.get.to_Node.get
541
+
542
+ # Add heat pump water heater and attach to selected loop
543
+ # Reference: https://github.com/NREL/openstudio-standards/blob/master/lib/
544
+ # => openstudio-standards/prototypes/common/objects/Prototype.ServiceWaterHeating.rb
545
+ if type != 'Simplified'
546
+ # convert zone name from STRING into OS model OBJECT
547
+ hpwh = std.model_add_heatpump_water_heater(model, # model
548
+ type: type, # type
549
+ water_heater_capacity: (cap * 1000 / cop), # water_heater_capacity
550
+ electric_backup_capacity: (bu_cap * 1000), # electric_backup_capacity
551
+ water_heater_volume: v.to_f, # water_heater_volume
552
+ service_water_temperature: OpenStudio.convert(140.0, 'F', 'C').get, # service_water_temperature
553
+ parasitic_fuel_consumption_rate: 3.0, # parasitic_fuel_consumption_rate
554
+ swh_temp_sch: sched, # swh_temp_sch
555
+ cop: cop, # cop
556
+ shr: 0.88, # shr
557
+ tank_ua: 3.9, # tank_ua
558
+ set_peak_use_flowrate: false, # set_peak_use_flowrate
559
+ peak_flowrate: 0.0, # peak_flowrate
560
+ flowrate_schedule: nil, # flowrate_schedule
561
+ water_heater_thermal_zone: zone) # water_heater_thermal_zone
562
+ else
563
+ hpwh = std.model_add_water_heater(model, # model
564
+ (cap * 1000), # water_heater_capacity
565
+ v.to_f, # water_heater_volume
566
+ 'HeatPump', # water_heater_fuel
567
+ OpenStudio.convert(140.0, 'F', 'C').to_f, # service_water_temperature
568
+ 3.0, # parasitic_fuel_consumption_rate
569
+ sched, # swh_temp_sch
570
+ false, # set_peak_use_flowrate
571
+ 0.0, # peak_flowrate
572
+ nil, # flowrate_schedule
573
+ model.getThermalZones[0], # water_heater_thermal_zone
574
+ 1) # number_water_heaters
575
+ # set COP in PLF curve
576
+ cop_curve = hpwh.partLoadFactorCurve.get
577
+ cop_curve.setName(cop_curve.name.get.gsub('2.8', cop.to_s))
578
+ cop_curve.setCoefficient1Constant(cop)
579
+ end
580
+
581
+ # add tank to appropriate branch and node (will be placed first in series if old tanks not removed)
582
+ # modify objects as ncessary
583
+ if type != 'Simplified'
584
+ hpwh.tank.addToNode(inlet)
585
+ hpwh.setDeadBandTemperatureDifference(db_temp / 1.8)
586
+ runner.registerInfo("#{hpwh.tank.name} was added to the model on #{loop.name}")
587
+ else
588
+ hpwh.addToNode(inlet)
589
+ hpwh.setMaximumTemperatureLimit(OpenStudio.convert(max_temp, 'F', 'C').get)
590
+ runner.registerInfo("#{hpwh.name} was added to the model on #{loop.name}")
591
+ end
592
+
593
+ # remove old tank objects if necessary
594
+ if remove_wh
595
+ runner.registerInfo("#{whtr.name} was removed from the model.")
596
+ whtr.remove
597
+ end
598
+
599
+ # CONTROLS MODIFICATIONS FOR TANK ---------------------------------------------------------------------------------
600
+ # apply schedule to tank
601
+ case type
602
+ when 'PumpedCondenser'
603
+ hpwh.tank.to_WaterHeaterMixed.get.setSetpointTemperatureSchedule(tank_sched)
604
+ when 'WrappedCondenser'
605
+ hpwh.tank.to_WaterHeaterStratified.get.setHeater1SetpointTemperatureSchedule(tank_sched)
606
+ hpwh.tank.to_WaterHeaterStratified.get.setHeater2SetpointTemperatureSchedule(tank_sched)
607
+ end
608
+ # END CONTROLS MODIFICATIONS FOR TANK -----------------------------------------------------------------------------
609
+ end
610
+ ## END HARDWARE ----------------------------------------------------------------------------------------------------
611
+
612
+ ## ADD REPORTED VARIABLES ------------------------------------------------------------------------------------------
613
+
614
+ ovar_names = ['Cooling Coil Total Cooling Rate',
615
+ 'Cooling Coil Total Water Heating Rate',
616
+ 'Cooling Coil Water Heating Electric Power',
617
+ 'Cooling Coil Crankcase Heater Electric Power',
618
+ 'Water Heater Tank Temperature',
619
+ 'Water Heater Heat Loss Rate',
620
+ 'Water Heater Heating Rate',
621
+ 'Water Heater Use Side Heat Transfer Rate',
622
+ 'Water Heater Source Side Heat Transfer Rate',
623
+ 'Water Heater Unmet Demand Heat Transfer Rate',
624
+ 'Water Heater Electricity Rate',
625
+ 'Water Heater Water Volume Flow Rate',
626
+ 'Water Use Connections Hot Water Temperature']
627
+
628
+ # Create new output variable objects
629
+ ovars = []
630
+ ovar_names.each do |nm|
631
+ ovars << OpenStudio::Model::OutputVariable.new(nm, model)
632
+ end
633
+
634
+ # add temperate schedule outputs - clean up and put names into array, then loop over setting key values
635
+ v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
636
+ v.setKeyValue(sched.name.to_s)
637
+ ovars << v
638
+
639
+ v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
640
+ v.setKeyValue(tank_sched.name.to_s)
641
+ ovars << v
642
+
643
+ if type != 'Simplified'
644
+ v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
645
+ v.setKeyValue(tank_sched.name.to_s)
646
+ ovars << v
647
+ end
648
+
649
+ # Set variable reporting frequency for newly created output variables
650
+ ovars.each do |var|
651
+ var.setReportingFrequency('TimeStep')
652
+ end
653
+
654
+ # Register info re: output variables:
655
+ runner.registerInfo("#{ovars.size} output variables were added to the model.")
656
+ ## END ADD REPORTED VARIABLES --------------------------------------------------------------------------------------
657
+
658
+ # Register final condition
659
+ hpwh_fc = model.getWaterHeaterHeatPumps.size + model.getWaterHeaterHeatPumpWrappedCondensers.size
660
+ tanks_fc = model.getWaterHeaterMixeds.size + model.getWaterHeaterStratifieds.size
661
+ if type != 'Simplified'
662
+ runner.registerFinalCondition("The building finshed with #{tanks_fc} water heater tank(s) and " \
663
+ "#{hpwh_fc} heat pump water heater(s).")
664
+ else
665
+ runner.registerFinalCondition("The building finished with #{tanks_fc - whtrs.size} water heater tank(s) " \
666
+ "and #{whtrs.size} heat pump water heater(s).")
667
+ end
668
+
669
+ true
670
+ end
671
+ end
672
+
673
+ # register the measure to be used by the application
674
+ AddHpwh.new.registerWithApplication