openstudio-load-flexibility-measures 0.3.2 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (41) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +36 -36
  3. data/.rubocop.yml +6 -9
  4. data/CHANGELOG.html +75 -75
  5. data/CHANGELOG.md +59 -54
  6. data/Gemfile +31 -7
  7. data/Jenkinsfile +11 -10
  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 -26
  25. data/lib/measures/add_hpwh/README.md.erb +41 -41
  26. data/lib/measures/add_hpwh/measure.rb +663 -645
  27. data/lib/measures/add_hpwh/measure.xml +402 -463
  28. data/lib/measures/add_hpwh/tests/SmallHotel-2A.osm +42893 -42893
  29. data/lib/measures/add_hpwh/tests/{add_hphw_test.rb → add_hpwh_test.rb} +137 -98
  30. data/lib/measures/add_packaged_ice_storage/LICENSE.md +26 -26
  31. data/lib/measures/add_packaged_ice_storage/README.html +185 -185
  32. data/lib/measures/add_packaged_ice_storage/README.md +189 -189
  33. data/lib/measures/add_packaged_ice_storage/measure.rb +694 -691
  34. data/lib/measures/add_packaged_ice_storage/measure.xml +245 -245
  35. data/lib/measures/add_packaged_ice_storage/resources/TESCurves.idf +1059 -1059
  36. data/lib/measures/add_packaged_ice_storage/tests/MeasureTest.osm +9507 -9507
  37. data/lib/measures/add_packaged_ice_storage/tests/add_packaged_ice_storage_test.rb +96 -96
  38. data/lib/openstudio/load_flexibility_measures/version.rb +40 -40
  39. data/lib/openstudio/load_flexibility_measures.rb +50 -50
  40. data/openstudio-load-flexibility-measures.gemspec +33 -34
  41. metadata +26 -26
@@ -1,645 +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?')
91
- remove_wh.setDescription('')
92
- remove_wh.setDefaultValue(true)
93
- args << remove_wh
94
-
95
- # find available water heaters and get default volume
96
- if !model.getWaterHeaterMixeds.empty?
97
- wheaters = model.getWaterHeaterMixeds
98
- end
99
-
100
- default_vol = 80.0 # gallons
101
- wh_names = ['All Water Heaters (Simplified Only)']
102
- wheaters.each do |w|
103
- if w.tankVolume.to_f > OpenStudio.convert(39, 'gal', 'm^3').to_f
104
- wh_names << w.name.to_s
105
- default_vol = [default_vol, (w.tankVolume.to_f / 0.0037854118).round(1)].max
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
- 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.to_s, 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.to_s, 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.to_s, user_arguments)
288
- flex_hrs << runner.getStringArgumentValue('flex_hrs' + n.to_s, user_arguments)
289
- end
290
-
291
- # check capacity, volume, and temps for reasonableness
292
- if cap < 5
293
- runner.registerWarning('HPWH heating capacity is less than 5kW ( 17kBtu/hr)')
294
- end
295
-
296
- if bu_cap < 5
297
- runner.registerWarning('Backup heating capaicty is less than 5kW ( 17kBtu/hr).')
298
- end
299
-
300
- if vol == 0
301
- runner.registerInfo('Tank volume was not specified, using existing tank capacity.')
302
- elsif vol < 40
303
- runner.registerWarning('Tank has less than 40 gallon capacity; check heat pump sizing if model fails.')
304
- end
305
-
306
- if min_temp < 120
307
- runner.registerWarning('Minimum tank temperature is very low; consider increasing to at least 120F.')
308
- runner.registerWarning('Do not store water for long periods at temperatures below 135-140F as those ' \
309
- 'conditions facilitate the growth of Legionella.')
310
- end
311
-
312
- if max_temp > 185
313
- runner.registerWarning('Maximum charging temperature exceeded practical limits; reset to 185F.')
314
- max_temp = 185.0
315
- end
316
-
317
- if max_temp > 170
318
- runner.registerWarning("#{max_temp}F is above or near the limit of the HP performance curves. If the " \
319
- 'simulation fails with cooling capacity less than 0, you have exceeded performance ' \
320
- 'limits. Consider setting max temp to less than 170F.')
321
- end
322
-
323
- # check selected schedule and set flag for later use
324
- sched_flag = false # flag for either creating new (false) or modifying existing (true) schedule
325
- if sched == '--Create New @ 140F--'
326
- runner.registerInfo('No reference water heater temperature setpoint schedule was selected; a new one ' \
327
- 'will be created.')
328
- else
329
- sched_flag = true
330
- runner.registerInfo("#{sched} will be used as the water heater temperature setpoint schedule.")
331
- end
332
-
333
- # parse flex_hrs into hours and minuts arrays
334
- idx = 0
335
- flex_hrs.each do |fh|
336
- if flex[idx] != 'None'
337
- data = fh.split(/[-:]/)
338
- data.each { |e| e.delete!(' ') }
339
- if data[2] > data[0]
340
- flex_type << flex[idx]
341
- hours << data[0]
342
- hours << data[2]
343
- minutes << data[1]
344
- minutes << data[3]
345
- else
346
- flex_type << flex[idx]
347
- flex_type << flex[idx]
348
- hours << 0
349
- hours << data[2]
350
- hours << data[0]
351
- hours << 24
352
- minutes << 0
353
- minutes << data[3]
354
- minutes << data[1]
355
- minutes << 0
356
- end
357
- end
358
- idx += 1
359
- end
360
-
361
- # convert hours and minutes into OS:Time objects
362
- idx = 0
363
- hours.each do |h|
364
- flex_times << OpenStudio::Time.new(0, h.to_i, minutes[idx].to_i, 0)
365
- idx += 1
366
- end
367
-
368
- # flex.delete('None')
369
-
370
- runner.registerInfo("A total of #{idx / 2} flex periods will be added to the selected water heater setpoint schedule.")
371
-
372
- # exit gracefully if errors registered above
373
- return false unless runner.result.errors.empty?
374
- ## END ARGUMENT VALIDATION -----------------------------------------------------------------------------------------
375
-
376
- ## CONTROLS: HEAT PUMP HEATING TEMPERATURE SETPOINT SCHEDULE -------------------------------------------------------
377
- # This section creates the heat pump heating temperature setpoint schedule with flex periods
378
- # The tank schedule is created here
379
-
380
- # find or create new reference temperature schedule based on sched_flag value
381
- if sched_flag # schedule already exists and must be modified
382
- # converts the STRING into a MODEL OBJECT, same variable name
383
- sched = model.getScheduleRulesetByName(sched).get.clone.to_ScheduleRuleset.get
384
- else
385
- # must create new water heater setpoint temperature schedule at 140F
386
- sched = OpenStudio::Model::ScheduleRuleset.new(model, 60)
387
- end
388
-
389
- # rename and duplicate for later modification
390
- sched.setName('Heat Pump Heating Temperature Setpoint')
391
- sched.defaultDaySchedule.setName('Heat Pump Heating Temperature Setpoint Default')
392
-
393
- # tank_sched = sched.clone.to_ScheduleRuleset.get
394
- tank_sched = OpenStudio::Model::ScheduleRuleset.new(model, 60 - (db_temp / 1.8 + 2))
395
- tank_sched.setName('Tank Electric Heater Setpoint')
396
- tank_sched.defaultDaySchedule.setName('Tank Electric Heater Setpoint Default')
397
-
398
- # grab default day and time-value pairs for modification
399
- d_day = sched.defaultDaySchedule
400
- old_times = d_day.times
401
- old_values = d_day.values
402
- new_values = Array.new(flex_times.size, 2)
403
-
404
- # find existing values in reference schedule and grab for use in new-rule creation
405
- flex_times.size.times do |i|
406
- if i.even?
407
- n = 0
408
- old_times.each do |ot|
409
- new_values[i] = old_values[n] if flex_times[i] <= ot
410
- n += 1
411
- end
412
- elsif flex_type[(i / 2).floor] == 'Charge - Heat Pump'
413
- new_values[i] = OpenStudio.convert(max_temp, 'F', 'C').get
414
- elsif flex_type[(i / 2).floor] == 'Float' || flex_type[(i / 2).floor] == 'Charge - Electric'
415
- new_values[i] = OpenStudio.convert(min_temp, 'F', 'C').get
416
- end
417
- end
418
-
419
- # create new rules and add to default day based on flex period options above
420
- idx = 0
421
- flex_times.each do |ft|
422
- d_day.addValue(ft, new_values[idx])
423
- idx += 1
424
- end
425
-
426
- ## END CONTROLS: HEAT PUMP HEATING TEMPERATURE SETPOINT SCHEDULE ---------------------------------------------------
427
-
428
- ## CONTROLS: TANK TEMPERATURE SETPOINT SCHEDULE (ELECTRIC BACKUP) --------------------------------------------------
429
- # This section creates the setpoint temperature schedule for the electric backup heating coils in the water tank
430
-
431
- # grab default day and time-value pairs for modification
432
- d_day = tank_sched.defaultDaySchedule
433
- old_times = d_day.times
434
- old_values = d_day.values
435
- new_values = Array.new(flex_times.size, 2)
436
-
437
- # find existing values in reference schedule and grab for use in new-rule creation
438
- flex_times.size.times do |i|
439
- if i.even?
440
- n = 0
441
- old_times.each do |ot|
442
- new_values[i] = old_values[n] if flex_times[i] <= ot
443
- n += 1
444
- end
445
- elsif flex_type[(i / 2).floor] == 'Charge - Electric'
446
- new_values[i] = OpenStudio.convert(max_temp, 'F', 'C').get
447
- elsif flex_type[(i / 2).floor] == 'Float' # || flex_type[(i/2).floor] == 'Charge - Heat Pump'
448
- new_values[i] = OpenStudio.convert(min_temp - db_temp, 'F', 'C').get
449
- elsif flex_type[(i / 2).floor] == 'Charge - Heat Pump'
450
- new_values[i] = 60 - (db_temp / 1.8)
451
- end
452
- end
453
-
454
- # create new rules and add to default day based on flex period options above
455
- idx = 0
456
- flex_times.each do |ft|
457
- d_day.addValue(ft, new_values[idx])
458
- idx += 1
459
- end
460
-
461
- ## END CONTROLS: TANK TEMPERATURE SETPOINT SCHEDULE (ELECTRIC BACKUP) ----------------------------------------------
462
-
463
- ## HARDWARE --------------------------------------------------------------------------------------------------------
464
- # This section adds the selected type of heat pump water heater to the supply side of the selected loop. If
465
- # selected, measure will remove any existing water heaters on the supply side of the loop. If old heater(s) are left
466
- # in place, the new HPWH tank will be placed in front (to the left) of them.
467
-
468
- # use OS standards build - arbitrary selection, but NZE Ready seems appropriate
469
- std = Standard.build('NREL ZNE Ready 2017')
470
-
471
- #####
472
- # get the selected water heaters
473
- whtrs = []
474
- model.getWaterHeaterMixeds.each do |w|
475
- if wh == 'All Water Heaters (Simplified Only)'
476
- # exclude booster tanks (<10gal):
477
- if w.tankVolume.to_f < 0.037854
478
- next
479
- else
480
- whtrs << w
481
- end
482
- elsif w.name.to_s == wh
483
- whtrs << w
484
- end
485
- end
486
-
487
- whtrs.each do |wh|
488
- # create empty arrays and initialize variables for later use
489
- old_heater = []
490
- count = 0
491
-
492
- # get the appropriate plant loop
493
- loop = ''
494
- loops = model.getPlantLoops
495
- loops.each do |l|
496
- l.supplyComponents.each do |c|
497
- if c.name.to_s == wh.name.to_s
498
- loop = l
499
- end
500
- end
501
- end
502
-
503
- # use existing tank volume unless otherwise specified
504
- # values between 0.0 and 5.0 are considered tank sizing multipliers
505
- if vol == 0
506
- v = wh.tankVolume
507
- elsif (vol > 0.0) && (vol < 5.0)
508
- v = wh.tankVolume.to_f * vol
509
- else
510
- v = OpenStudio.convert(vol, 'gal', 'm^3').get
511
- end
512
-
513
- inlet = wh.supplyInletModelObject.get.to_Node.get
514
- outlet = wh.supplyOutletModelObject.get.to_Node.get
515
-
516
- # Add heat pump water heater and attach to selected loop
517
- # Reference: https://github.com/NREL/openstudio-standards/blob/master/lib/
518
- # => openstudio-standards/prototypes/common/objects/Prototype.ServiceWaterHeating.rb
519
- if type != 'Simplified'
520
- # convert zone name from STRING into OS model OBJECT
521
- zone = model.getThermalZoneByName(zone).get
522
- hpwh = std.model_add_heatpump_water_heater(model, # model
523
- type: type, # type
524
- water_heater_capacity: (cap * 1000 / cop), # water_heater_capacity
525
- electric_backup_capacity: (bu_cap * 1000), # electric_backup_capacity
526
- water_heater_volume: v, # water_heater_volume
527
- service_water_temperature: OpenStudio.convert(140.0, 'F', 'C').get, # service_water_temperature
528
- parasitic_fuel_consumption_rate: 3.0, # parasitic_fuel_consumption_rate
529
- swh_temp_sch: sched, # swh_temp_sch
530
- cop: cop, # cop
531
- shr: 0.88, # shr
532
- tank_ua: 3.9, # tank_ua
533
- set_peak_use_flowrate: false, # set_peak_use_flowrate
534
- peak_flowrate: 0.0, # peak_flowrate
535
- flowrate_schedule: nil, # flowrate_schedule
536
- water_heater_thermal_zone: zone) # water_heater_thermal_zone
537
- else
538
- # zone = wh.ambientTemperatureThermalZone.get
539
- runner.registerWarning(wh.to_s)
540
- hpwh = std.model_add_water_heater(model, # model
541
- (cap * 1000), # water_heater_capacity
542
- v.to_f, # water_heater_volume
543
- 'HeatPump', # water_heater_fuel
544
- OpenStudio.convert(140.0, 'F', 'C').to_f, # service_water_temperature
545
- 3.0, # parasitic_fuel_consumption_rate
546
- sched, # swh_temp_sch
547
- false, # set_peak_use_flowrate
548
- 0.0, # peak_flowrate
549
- nil, # flowrate_schedule
550
- model.getThermalZones[0], # water_heater_thermal_zone
551
- 1) # number_water_heaters
552
- # set COP in PLF curve
553
- cop_curve = hpwh.partLoadFactorCurve.get
554
- cop_curve.setName(cop_curve.name.get.gsub('2.8', cop.to_s))
555
- cop_curve.setCoefficient1Constant(cop)
556
- end
557
-
558
- # add tank to appropriate branch and node (will be placed first in series if old tanks not removed)
559
- # modify objects as ncessary
560
- if type != 'Simplified'
561
- hpwh.tank.addToNode(inlet)
562
- hpwh.setDeadBandTemperatureDifference(db_temp / 1.8)
563
- runner.registerInfo("#{hpwh.tank.name} was added to the model on #{loop.name}")
564
- else
565
- hpwh.addToNode(inlet)
566
- hpwh.setMaximumTemperatureLimit(OpenStudio.convert(max_temp, 'F', 'C').get)
567
- runner.registerInfo("#{hpwh.name} was added to the model on #{loop.name}")
568
- end
569
-
570
- # remove old tank objects if necessary
571
- if remove_wh
572
- runner.registerInfo("#{wh.name} was removed from the model.")
573
- wh.remove
574
- end
575
-
576
- # CONTROLS MODIFICATIONS FOR TANK ---------------------------------------------------------------------------------
577
- # apply schedule to tank
578
- if type == 'PumpedCondenser'
579
- hpwh.tank.to_WaterHeaterMixed.get.setSetpointTemperatureSchedule(tank_sched)
580
- elsif type == 'WrappedCondenser'
581
- hpwh.tank.to_WaterHeaterStratified.get.setHeater1SetpointTemperatureSchedule(tank_sched)
582
- hpwh.tank.to_WaterHeaterStratified.get.setHeater2SetpointTemperatureSchedule(tank_sched)
583
- end
584
- # END CONTROLS MODIFICATIONS FOR TANK -----------------------------------------------------------------------------
585
- end
586
- ## END HARDWARE ----------------------------------------------------------------------------------------------------
587
-
588
- ## ADD REPORTED VARIABLES ------------------------------------------------------------------------------------------
589
-
590
- ovar_names = ['Cooling Coil Total Cooling Rate',
591
- 'Cooling Coil Total Water Heating Rate',
592
- 'Cooling Coil Water Heating Electric Power',
593
- 'Cooling Coil Crankcase Heater Electric Power',
594
- 'Water Heater Tank Temperature',
595
- 'Water Heater Heat Loss Rate',
596
- 'Water Heater Heating Rate',
597
- 'Water Heater Use Side Heat Transfer Rate',
598
- 'Water Heater Source Side Heat Transfer Rate',
599
- 'Water Heater Unmet Demand Heat Transfer Rate',
600
- 'Water Heater Electricity Rate',
601
- 'Water Heater Water Volume Flow Rate',
602
- 'Water Use Connections Hot Water Temperature']
603
-
604
- # Create new output variable objects
605
- ovars = []
606
- ovar_names.each do |nm|
607
- ovars << OpenStudio::Model::OutputVariable.new(nm, model)
608
- end
609
-
610
- # add temperate schedule outputs - clean up and put names into array, then loop over setting key values
611
- v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
612
- v.setKeyValue(sched.name.to_s)
613
- ovars << v
614
-
615
- v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
616
- v.setKeyValue(tank_sched.name.to_s)
617
- ovars << v
618
-
619
- if type != 'Simplified'
620
- v = OpenStudio::Model::OutputVariable.new('Schedule Value', model)
621
- v.setKeyValue(tank_sched.name.to_s)
622
- ovars << v
623
- end
624
-
625
- # Set variable reporting frequency for newly created output variables
626
- ovars.each do |var|
627
- var.setReportingFrequency('TimeStep')
628
- end
629
-
630
- # Register info re: output variables:
631
- runner.registerInfo("#{ovars.size} output variables were added to the model.")
632
- ## END ADD REPORTED VARIABLES --------------------------------------------------------------------------------------
633
-
634
- # Register final condition
635
- hpwh_fc = model.getWaterHeaterHeatPumps.size + model.getWaterHeaterHeatPumpWrappedCondensers.size
636
- tanks_fc = model.getWaterHeaterMixeds.size + model.getWaterHeaterStratifieds.size
637
- runner.registerFinalCondition("The building finshed with #{tanks_fc} water heater tank(s) and " \
638
- "#{hpwh_fc} heat pump water heater(s).")
639
-
640
- true
641
- end
642
- end
643
-
644
- # register the measure to be used by the application
645
- 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