openstudio-load-flexibility-measures 0.2.1 → 0.4.0

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