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