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