openstudio-load-flexibility-measures 0.3.1 → 0.5.0

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