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