openstudio-load-flexibility-measures 0.1.1

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