-
Notifications
You must be signed in to change notification settings - Fork 12
/
waterheater.rb
1850 lines (1632 loc) · 92.9 KB
/
waterheater.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
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
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
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
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
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
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
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# frozen_string_literal: true
class Waterheater
def self.apply_tank(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds)
solar_fraction = get_water_heater_solar_fraction(water_heating_system, solar_thermal_system)
t_set_c = get_t_set_c(water_heating_system.temperature, water_heating_system.water_heater_type)
loop = create_new_loop(model, t_set_c, eri_version, unit_multiplier)
act_vol = calc_storage_tank_actual_vol(water_heating_system.tank_volume, water_heating_system.fuel_type)
u, ua, eta_c = calc_tank_UA(act_vol, water_heating_system, solar_fraction, nbeds)
new_heater = create_new_heater(name: Constants.ObjectNameWaterHeater,
water_heating_system: water_heating_system,
act_vol: act_vol,
t_set_c: t_set_c,
loc_space: loc_space,
loc_schedule: loc_schedule,
model: model,
runner: runner,
u: u,
ua: ua,
eta_c: eta_c,
schedules_file: schedules_file,
unavailable_periods: unavailable_periods,
unit_multiplier: unit_multiplier)
loop.addSupplyBranchForComponent(new_heater)
add_ec_adj(model, new_heater, ec_adj, loc_space, water_heating_system, unit_multiplier)
add_desuperheater(model, runner, water_heating_system, new_heater, loc_space, loc_schedule, loop, unit_multiplier)
return loop
end
def self.apply_tankless(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds)
water_heating_system.heating_capacity = 100000000000.0 * unit_multiplier
solar_fraction = get_water_heater_solar_fraction(water_heating_system, solar_thermal_system)
t_set_c = get_t_set_c(water_heating_system.temperature, water_heating_system.water_heater_type)
loop = create_new_loop(model, t_set_c, eri_version, unit_multiplier)
act_vol = 1.0 * unit_multiplier
_u, ua, eta_c = calc_tank_UA(act_vol, water_heating_system, solar_fraction, nbeds)
new_heater = create_new_heater(name: Constants.ObjectNameWaterHeater,
water_heating_system: water_heating_system,
act_vol: act_vol,
t_set_c: t_set_c,
loc_space: loc_space,
loc_schedule: loc_schedule,
model: model,
runner: runner,
ua: ua,
eta_c: eta_c,
schedules_file: schedules_file,
unavailable_periods: unavailable_periods,
unit_multiplier: unit_multiplier)
loop.addSupplyBranchForComponent(new_heater)
add_ec_adj(model, new_heater, ec_adj, loc_space, water_heating_system, unit_multiplier)
add_desuperheater(model, runner, water_heating_system, new_heater, loc_space, loc_schedule, loop, unit_multiplier)
return loop
end
def self.apply_heatpump(model, runner, loc_space, loc_schedule, elevation, water_heating_system, ec_adj, solar_thermal_system, conditioned_zone, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds)
obj_name_hpwh = Constants.ObjectNameWaterHeater
solar_fraction = get_water_heater_solar_fraction(water_heating_system, solar_thermal_system)
t_set_c = get_t_set_c(water_heating_system.temperature, water_heating_system.water_heater_type)
loop = create_new_loop(model, t_set_c, eri_version, unit_multiplier)
h_tank = 0.0188 * water_heating_system.tank_volume + 0.0935 # Linear relationship that gets GE height at 50 gal and AO Smith height at 80 gal
# Add in schedules for Tamb, RHamb, and the compressor
hpwh_tamb = OpenStudio::Model::ScheduleConstant.new(model)
hpwh_tamb.setName("#{obj_name_hpwh} Tamb act")
hpwh_tamb.setValue(23)
hpwh_rhamb = OpenStudio::Model::ScheduleConstant.new(model)
hpwh_rhamb.setName("#{obj_name_hpwh} RHamb act")
hpwh_rhamb.setValue(0.5)
# Note: These get overwritten by EMS later, see HPWH Control program
top_element_setpoint_schedule = OpenStudio::Model::ScheduleConstant.new(model)
top_element_setpoint_schedule.setName("#{obj_name_hpwh} TopElementSetpoint")
bottom_element_setpoint_schedule = OpenStudio::Model::ScheduleConstant.new(model)
bottom_element_setpoint_schedule.setName("#{obj_name_hpwh} BottomElementSetpoint")
setpoint_schedule = nil
if not schedules_file.nil?
# To handle variable setpoints, need one schedule that gets sensed and a new schedule that gets actuated
# Sensed schedule
setpoint_schedule = schedules_file.create_schedule_file(model, col_name: SchedulesFile::Columns[:WaterHeaterSetpoint].name)
if not setpoint_schedule.nil?
Schedule.set_schedule_type_limits(model, setpoint_schedule, Constants.ScheduleTypeLimitsTemperature)
# Actuated schedule
control_setpoint_schedule = ScheduleConstant.new(model, "#{obj_name_hpwh} ControlSetpoint", 0.0, Constants.ScheduleTypeLimitsTemperature, unavailable_periods: unavailable_periods)
control_setpoint_schedule = control_setpoint_schedule.schedule
end
end
if setpoint_schedule.nil?
setpoint_schedule = ScheduleConstant.new(model, Constants.ObjectNameWaterHeaterSetpoint, t_set_c, Constants.ScheduleTypeLimitsTemperature, unavailable_periods: unavailable_periods)
setpoint_schedule = setpoint_schedule.schedule
control_setpoint_schedule = setpoint_schedule
else
runner.registerWarning("Both '#{SchedulesFile::Columns[:WaterHeaterSetpoint].name}' schedule file and setpoint temperature provided; the latter will be ignored.") if !t_set_c.nil?
end
airflow_rate = 181.0 # cfm
min_temp = 42.0 # F
max_temp = 120.0 # F
# Coil:WaterHeating:AirToWaterHeatPump:Wrapped
coil = setup_hpwh_dxcoil(model, runner, water_heating_system, elevation, obj_name_hpwh, airflow_rate, unit_multiplier)
# WaterHeater:Stratified
tank = setup_hpwh_stratified_tank(model, water_heating_system, obj_name_hpwh, h_tank, solar_fraction, hpwh_tamb, bottom_element_setpoint_schedule, top_element_setpoint_schedule, unit_multiplier, nbeds)
loop.addSupplyBranchForComponent(tank)
add_desuperheater(model, runner, water_heating_system, tank, loc_space, loc_schedule, loop, unit_multiplier)
# Fan:SystemModel
fan = setup_hpwh_fan(model, water_heating_system, obj_name_hpwh, airflow_rate, unit_multiplier)
# WaterHeater:HeatPump:WrappedCondenser
hpwh = setup_hpwh_wrapped_condenser(model, obj_name_hpwh, coil, tank, fan, h_tank, airflow_rate, hpwh_tamb, hpwh_rhamb, min_temp, max_temp, control_setpoint_schedule, unit_multiplier)
# Amb temp & RH sensors, temp sensor shared across programs
amb_temp_sensor, amb_rh_sensors = get_loc_temp_rh_sensors(model, obj_name_hpwh, loc_schedule, loc_space, conditioned_zone)
hpwh_inlet_air_program = add_hpwh_inlet_air_and_zone_heat_gain_program(model, obj_name_hpwh, loc_space, hpwh_tamb, hpwh_rhamb, tank, coil, fan, amb_temp_sensor, amb_rh_sensors, unit_multiplier)
# EMS for the HPWH control logic
op_mode = water_heating_system.operating_mode
hpwh_ctrl_program = add_hpwh_control_program(model, runner, obj_name_hpwh, amb_temp_sensor, top_element_setpoint_schedule, bottom_element_setpoint_schedule, min_temp, max_temp, op_mode, setpoint_schedule, control_setpoint_schedule, schedules_file)
# ProgramCallingManagers
program_calling_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
program_calling_manager.setName("#{obj_name_hpwh} ProgramManager")
program_calling_manager.setCallingPoint('InsideHVACSystemIterationLoop')
program_calling_manager.addProgram(hpwh_ctrl_program)
program_calling_manager.addProgram(hpwh_inlet_air_program)
add_ec_adj(model, hpwh, ec_adj, loc_space, water_heating_system, unit_multiplier)
return loop
end
def self.apply_combi(model, runner, loc_space, loc_schedule, water_heating_system, ec_adj, solar_thermal_system, eri_version, schedules_file, unavailable_periods, unit_multiplier, nbeds)
solar_fraction = get_water_heater_solar_fraction(water_heating_system, solar_thermal_system)
boiler, boiler_plant_loop = get_combi_boiler_and_plant_loop(model, water_heating_system.related_hvac_idref)
boiler.setName('combi boiler')
boiler.additionalProperties.setFeature('HPXML_ID', water_heating_system.id) # Used by reporting measure
boiler.additionalProperties.setFeature('IsCombiBoiler', true) # Used by reporting measure
obj_name_combi = Constants.ObjectNameWaterHeater
if water_heating_system.water_heater_type == HPXML::WaterHeaterTypeCombiStorage
if water_heating_system.standby_loss_value <= 0
fail 'A negative indirect water heater standby loss was calculated, double check water heater inputs.'
end
act_vol = calc_storage_tank_actual_vol(water_heating_system.tank_volume, nil)
a_side = calc_tank_areas(act_vol)[1]
ua = calc_indirect_ua_with_standbyloss(act_vol, water_heating_system, a_side, solar_fraction, nbeds)
else
ua = 0.0
act_vol = 1.0
end
t_set_c = get_t_set_c(water_heating_system.temperature, water_heating_system.water_heater_type)
loop = create_new_loop(model, t_set_c, eri_version, unit_multiplier)
# Create water heater
new_heater = create_new_heater(name: obj_name_combi,
water_heating_system: water_heating_system,
act_vol: act_vol,
t_set_c: t_set_c,
loc_space: loc_space,
loc_schedule: loc_schedule,
model: model,
runner: runner,
ua: ua,
is_combi: true,
schedules_file: schedules_file,
unavailable_periods: unavailable_periods,
unit_multiplier: unit_multiplier)
new_heater.setSourceSideDesignFlowRate(100 * unit_multiplier) # set one large number, override by EMS
# Create alternate setpoint schedule for source side flow request
alternate_stp_sch = new_heater.setpointTemperatureSchedule.get.clone(model).to_Schedule.get
alternate_stp_sch.setName("#{obj_name_combi} Alt Spt")
new_heater.setIndirectAlternateSetpointTemperatureSchedule(alternate_stp_sch)
# Create setpoint schedule to specify source side temperature
source_stp_sch = OpenStudio::Model::ScheduleConstant.new(model)
source_stp_sch.setName("#{obj_name_combi} Source Spt")
boiler_spt_mngr = model.getSetpointManagerScheduleds.find { |spt_mngr| spt_mngr.setpointNode.get == boiler_plant_loop.loopTemperatureSetpointNode }
boiler_heating_spt = boiler_spt_mngr.to_SetpointManagerScheduled.get.schedule.to_ScheduleConstant.get.value
# tank source side inlet temperature, degree C
source_stp_sch.setValue(boiler_heating_spt)
# reset dhw boiler setpoint
boiler_spt_mngr.to_SetpointManagerScheduled.get.setSchedule(source_stp_sch)
boiler_plant_loop.autosizeMaximumLoopFlowRate()
# change loop equipment operation scheme to heating load
scheme_dhw = OpenStudio::Model::PlantEquipmentOperationHeatingLoad.new(model)
scheme_dhw.addEquipment(1000000000, new_heater)
loop.setPrimaryPlantEquipmentOperationScheme(scheme_dhw)
# Add dhw boiler to the load distribution scheme
scheme = OpenStudio::Model::PlantEquipmentOperationHeatingLoad.new(model)
scheme.addEquipment(1000000000, boiler)
boiler_plant_loop.setPrimaryPlantEquipmentOperationScheme(scheme)
boiler_plant_loop.addDemandBranchForComponent(new_heater)
boiler_plant_loop.setPlantLoopVolume(0.001 * unit_multiplier) # Cannot be auto-calculated because of large default tank source side mfr(set to be overwritten by EMS)
loop.addSupplyBranchForComponent(new_heater)
add_ec_adj(model, new_heater, ec_adj, loc_space, water_heating_system, unit_multiplier, boiler)
return loop
end
def self.apply_combi_system_EMS(model, water_heating_systems, plantloop_map)
water_heating_systems.select { |wh|
[HPXML::WaterHeaterTypeCombiStorage,
HPXML::WaterHeaterTypeCombiTankless].include? wh.water_heater_type
}.each do |water_heating_system|
combi_sys_id = water_heating_system.id
# EMS for modulate source side mass flow rate
# Initialization
equipment_peaks = {}
equipment_sch_sensors = {}
equipment_target_temp_sensors = {}
tank_volume, deadband, tank_source_temp = 0.0, 0.0, 0.0
alt_spt_sch = nil
tank_temp_sensor, tank_spt_sensor, tank_loss_energy_sensor = nil, nil, nil
altsch_actuator, pump_actuator = nil, nil
water_heater = nil
# Create sensors and actuators
plant_loop = plantloop_map[combi_sys_id]
plant_loop.components.each do |c|
next unless c.to_WaterHeaterMixed.is_initialized
water_heater = c.to_WaterHeaterMixed.get
tank_volume = water_heater.tankVolume.get
deadband = water_heater.deadbandTemperatureDifference
# Sensors and actuators related to OS water heater object
tank_temp_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Water Heater Tank Temperature')
tank_temp_sensor.setName("#{combi_sys_id} Tank Temp")
tank_temp_sensor.setKeyName(water_heater.name.to_s)
tank_loss_energy_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Water Heater Heat Loss Energy')
tank_loss_energy_sensor.setName("#{combi_sys_id} Tank Loss Energy")
tank_loss_energy_sensor.setKeyName(water_heater.name.to_s)
tank_spt_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
tank_spt_sensor.setName("#{combi_sys_id} Setpoint Temperature")
tank_spt_sensor.setKeyName(water_heater.setpointTemperatureSchedule.get.name.to_s)
alt_spt_sch = water_heater.indirectAlternateSetpointTemperatureSchedule.get
if alt_spt_sch.to_ScheduleConstant.is_initialized
altsch_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(alt_spt_sch, *EPlus::EMSActuatorScheduleConstantValue)
elsif alt_spt_sch.to_ScheduleRuleset.is_initialized
altsch_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(alt_spt_sch, *EPlus::EMSActuatorScheduleYearValue)
else
altsch_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(alt_spt_sch, *EPlus::EMSActuatorScheduleFileValue)
end
altsch_actuator.setName("#{combi_sys_id} AltSchedOverride")
end
plant_loop.components.each do |c|
next unless c.to_WaterUseConnections.is_initialized
wuc = c.to_WaterUseConnections.get
wuc.waterUseEquipment.each do |wu|
# water use equipment peak mass flow rate
wu_peak = wu.waterUseEquipmentDefinition.peakFlowRate
equipment_peaks[wu.name.to_s] = wu_peak
# mfr fraction schedule sensors
wu_sch_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
wu_sch_sensor.setName("#{wu.name} sch value")
wu_sch_sensor.setKeyName(wu.flowRateFractionSchedule.get.name.to_s)
equipment_sch_sensors[wu.name.to_s] = wu_sch_sensor
# water use equipment target temperature schedule sensors
if wu.waterUseEquipmentDefinition.targetTemperatureSchedule.is_initialized
target_temp_sch = wu.waterUseEquipmentDefinition.targetTemperatureSchedule.get
else
target_temp_sch = water_heater.setpointTemperatureSchedule.get
end
target_temp_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
target_temp_sensor.setName("#{wu.name} target temp")
target_temp_sensor.setKeyName(target_temp_sch.name.to_s)
equipment_target_temp_sensors[wu.name.to_s] = target_temp_sensor
end
end
dhw_source_loop = model.getPlantLoops.find { |l| l.demandComponents.include? water_heater }
dhw_source_loop.components.each do |c|
next unless c.to_PumpVariableSpeed.is_initialized
pump = c.to_PumpVariableSpeed.get
pump_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(pump, *EPlus::EMSActuatorPumpMassFlowRate)
pump_actuator.setName("#{combi_sys_id} Pump MFR")
end
dhw_source_loop.supplyOutletNode.setpointManagers.each do |setpoint_manager|
if setpoint_manager.to_SetpointManagerScheduled.is_initialized
tank_source_temp = setpoint_manager.to_SetpointManagerScheduled.get.schedule.to_ScheduleConstant.get.value
end
end
mains_temp_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Site Mains Water Temperature')
mains_temp_sensor.setName('Mains Temperature')
mains_temp_sensor.setKeyName('Environment')
# Program
combi_ctrl_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
combi_ctrl_program.setName("#{combi_sys_id} Source MFR Control")
combi_ctrl_program.addLine("Set Rho = @RhoH2O #{tank_temp_sensor.name}")
combi_ctrl_program.addLine("Set Cp = @CpHW #{tank_temp_sensor.name}")
combi_ctrl_program.addLine("Set Tank_Water_Mass = #{tank_volume} * Rho")
combi_ctrl_program.addLine("Set DeltaT = #{tank_source_temp} - #{tank_spt_sensor.name}")
combi_ctrl_program.addLine("Set WU_Hot_Temp = #{tank_temp_sensor.name}")
combi_ctrl_program.addLine("Set WU_Cold_Temp = #{mains_temp_sensor.name}")
combi_ctrl_program.addLine('Set Tank_Use_Total_MFR = 0.0')
equipment_peaks.each do |wu_name, peak|
wu_id = wu_name.gsub(' ', '_')
combi_ctrl_program.addLine("Set #{wu_id}_Peak = #{peak}")
combi_ctrl_program.addLine("Set #{wu_id}_MFR_Total = #{wu_id}_Peak * #{equipment_sch_sensors[wu_name].name} * Rho")
combi_ctrl_program.addLine("If #{equipment_target_temp_sensors[wu_name].name} > WU_Hot_Temp")
combi_ctrl_program.addLine("Set #{wu_id}_MFR_Hot = #{wu_id}_MFR_Total")
combi_ctrl_program.addLine('Else')
combi_ctrl_program.addLine("Set #{wu_id}_MFR_Hot = #{wu_id}_MFR_Total * (#{equipment_target_temp_sensors[wu_name].name} - WU_Cold_Temp)/(WU_Hot_Temp - WU_Cold_Temp)")
combi_ctrl_program.addLine('EndIf')
combi_ctrl_program.addLine("Set Tank_Use_Total_MFR = Tank_Use_Total_MFR + #{wu_id}_MFR_Hot")
end
combi_ctrl_program.addLine("Set WH_Loss = - #{tank_loss_energy_sensor.name}")
combi_ctrl_program.addLine("Set WH_Use = Tank_Use_Total_MFR * Cp * (#{tank_temp_sensor.name} - #{mains_temp_sensor.name}) * ZoneTimeStep * 3600")
combi_ctrl_program.addLine("Set WH_HeatToLowSetpoint = Tank_Water_Mass * Cp * (#{tank_temp_sensor.name} - #{tank_spt_sensor.name} + #{deadband})")
combi_ctrl_program.addLine("Set WH_HeatToHighSetpoint = Tank_Water_Mass * Cp * (#{tank_temp_sensor.name} - #{tank_spt_sensor.name})")
combi_ctrl_program.addLine('Set WH_Energy_Demand = WH_Use + WH_Loss - WH_HeatToLowSetpoint')
combi_ctrl_program.addLine('Set WH_Energy_Heat = WH_Use + WH_Loss - WH_HeatToHighSetpoint')
combi_ctrl_program.addLine('If WH_Energy_Demand > 0')
combi_ctrl_program.addLine("Set #{pump_actuator.name} = WH_Energy_Heat / (Cp * DeltaT * 3600 * ZoneTimeStep)")
combi_ctrl_program.addLine("Set #{altsch_actuator.name} = 100") # Set the alternate setpoint temperature to highest level to ensure maximum source side flow rate
combi_ctrl_program.addLine('Else')
combi_ctrl_program.addLine("Set #{pump_actuator.name} = 0")
combi_ctrl_program.addLine("Set #{altsch_actuator.name} = NULL")
combi_ctrl_program.addLine('EndIf')
# ProgramCallingManagers
program_calling_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
program_calling_manager.setName("#{combi_sys_id} ProgramManager")
program_calling_manager.setCallingPoint('BeginZoneTimestepAfterInitHeatBalance')
program_calling_manager.addProgram(combi_ctrl_program)
end
end
def self.apply_solar_thermal(model, loc_space, loc_schedule, solar_thermal_system, plantloop_map, unit_multiplier)
if [HPXML::WaterHeaterTypeCombiStorage, HPXML::WaterHeaterTypeCombiTankless].include? solar_thermal_system.water_heating_system.water_heater_type
fail "Water heating system '#{solar_thermal_system.water_heating_system.id}' connected to solar thermal system '#{solar_thermal_system.id}' cannot be a space-heating boiler."
end
if solar_thermal_system.water_heating_system.uses_desuperheater
fail "Water heating system '#{solar_thermal_system.water_heating_system.id}' connected to solar thermal system '#{solar_thermal_system.id}' cannot be attached to a desuperheater."
end
dhw_loop = plantloop_map[solar_thermal_system.water_heating_system.id]
obj_name = Constants.ObjectNameSolarHotWater
if [HPXML::SolarThermalTypeEvacuatedTube].include? solar_thermal_system.collector_type
iam_coeff2 = 0.3023 # IAM coeff1=1 by definition, values based on a system listed by SRCC with values close to the average
iam_coeff3 = -0.3057
elsif [HPXML::SolarThermalTypeSingleGlazing, HPXML::SolarThermalTypeDoubleGlazing].include? solar_thermal_system.collector_type
iam_coeff2 = 0.1
iam_coeff3 = 0
elsif [HPXML::SolarThermalTypeICS].include? solar_thermal_system.collector_type
iam_coeff2 = 0.1
iam_coeff3 = 0
end
if [HPXML::SolarThermalLoopTypeIndirect].include? solar_thermal_system.collector_loop_type
fluid_type = Constants.FluidPropyleneGlycol
heat_ex_eff = 0.7
elsif [HPXML::SolarThermalLoopTypeDirect, HPXML::SolarThermalLoopTypeThermosyphon].include? solar_thermal_system.collector_loop_type
fluid_type = Constants.FluidWater
heat_ex_eff = 1.0
end
collector_area = solar_thermal_system.collector_area * unit_multiplier
storage_volume = solar_thermal_system.storage_volume * unit_multiplier
if solar_thermal_system.collector_loop_type == HPXML::SolarThermalLoopTypeThermosyphon
pump_power = 0.0
else
pump_power = 0.8 * collector_area
end
test_flow = 55.0 / UnitConversions.convert(1.0, 'lbm/min', 'kg/hr') / Liquid.H2O_l.rho * UnitConversions.convert(1.0, 'ft^2', 'm^2') # cfm/ft^2
coll_flow = test_flow * collector_area # cfm
if fluid_type == Constants.FluidWater # Direct, make the storage tank a dummy tank with 0 tank losses
u_tank = 0.0
else
r_tank = 10.0 # Btu/(hr-ft2-F)
u_tank = UnitConversions.convert(1.0 / r_tank, 'Btu/(hr*ft^2*F)', 'W/(m^2*K)') # W/m2-K
end
# Get water heater and setpoint temperature schedules from loop
water_heater = nil
setpoint_schedule_one = nil
setpoint_schedule_two = nil
dhw_loop.supplyComponents.each do |supply_component|
if supply_component.to_WaterHeaterMixed.is_initialized
water_heater = supply_component.to_WaterHeaterMixed.get
setpoint_schedule_one = water_heater.setpointTemperatureSchedule.get
setpoint_schedule_two = water_heater.setpointTemperatureSchedule.get
elsif supply_component.to_WaterHeaterStratified.is_initialized
water_heater = supply_component.to_WaterHeaterStratified.get
setpoint_schedule_one = water_heater.heater1SetpointTemperatureSchedule
setpoint_schedule_two = water_heater.heater2SetpointTemperatureSchedule
end
end
dhw_setpoint_manager = nil
dhw_loop.supplyOutletNode.setpointManagers.each do |setpoint_manager|
if setpoint_manager.to_SetpointManagerScheduled.is_initialized
dhw_setpoint_manager = setpoint_manager.to_SetpointManagerScheduled.get
end
end
plant_loop = OpenStudio::Model::PlantLoop.new(model)
plant_loop.setName('solar hot water loop')
if fluid_type == Constants.FluidWater
plant_loop.setFluidType('Water')
else
plant_loop.setFluidType('PropyleneGlycol')
plant_loop.setGlycolConcentration(50)
end
plant_loop.setMaximumLoopTemperature(100)
plant_loop.setMinimumLoopTemperature(0)
plant_loop.setMinimumLoopFlowRate(0)
plant_loop.setLoadDistributionScheme('Optimal')
plant_loop.setPlantEquipmentOperationHeatingLoadSchedule(model.alwaysOnDiscreteSchedule)
sizing_plant = plant_loop.sizingPlant
sizing_plant.setLoopType('Heating')
sizing_plant.setDesignLoopExitTemperature(dhw_loop.sizingPlant.designLoopExitTemperature)
sizing_plant.setLoopDesignTemperatureDifference(UnitConversions.convert(10.0, 'deltaF', 'deltaC'))
setpoint_manager = OpenStudio::Model::SetpointManagerScheduled.new(model, dhw_setpoint_manager.schedule)
setpoint_manager.setName(obj_name + ' setpoint mgr')
setpoint_manager.setControlVariable('Temperature')
pump = OpenStudio::Model::PumpConstantSpeed.new(model)
pump.setName(obj_name + ' pump')
pump.setRatedPumpHead(90000)
pump.setRatedPowerConsumption(pump_power)
pump.setMotorEfficiency(0.3)
pump.setFractionofMotorInefficienciestoFluidStream(0.2)
pump.setPumpControlType('Intermittent')
pump.setRatedFlowRate(UnitConversions.convert(coll_flow, 'cfm', 'm^3/s'))
pump.addToNode(plant_loop.supplyInletNode)
pump.additionalProperties.setFeature('HPXML_ID', solar_thermal_system.water_heating_system.id) # Used by reporting measure
pump.additionalProperties.setFeature('ObjectType', Constants.ObjectNameSolarHotWater) # Used by reporting measure
panel_length = UnitConversions.convert(collector_area, 'ft^2', 'm^2')**0.5
run = Math::cos(solar_thermal_system.collector_tilt * Math::PI / 180) * panel_length
offset = 1000.0 # prevent shading
vertices = OpenStudio::Point3dVector.new
vertices << OpenStudio::Point3d.new(offset, offset, 0)
vertices << OpenStudio::Point3d.new(offset + panel_length, offset, 0)
vertices << OpenStudio::Point3d.new(offset + panel_length, offset + run, (panel_length**2 - run**2)**0.5)
vertices << OpenStudio::Point3d.new(offset, offset + run, (panel_length**2 - run**2)**0.5)
m = OpenStudio::Matrix.new(4, 4, 0)
azimuth = Float(solar_thermal_system.collector_azimuth)
m[0, 0] = Math::cos((180 - azimuth) * Math::PI / 180)
m[1, 1] = Math::cos((180 - azimuth) * Math::PI / 180)
m[0, 1] = -Math::sin((180 - azimuth) * Math::PI / 180)
m[1, 0] = Math::sin((180 - azimuth) * Math::PI / 180)
m[2, 2] = 1
m[3, 3] = 1
transformation = OpenStudio::Transformation.new(m)
vertices = transformation * vertices
shading_surface_group = OpenStudio::Model::ShadingSurfaceGroup.new(model)
shading_surface_group.setName(obj_name + ' shading group')
shading_surface = OpenStudio::Model::ShadingSurface.new(vertices, model)
shading_surface.setName(obj_name + ' shading surface')
shading_surface.setShadingSurfaceGroup(shading_surface_group)
if solar_thermal_system.collector_type == HPXML::SolarThermalTypeICS
collector_plate = OpenStudio::Model::SolarCollectorIntegralCollectorStorage.new(model)
collector_plate.setName(obj_name + ' coll plate')
collector_plate.setSurface(shading_surface)
collector_plate.setMaximumFlowRate(UnitConversions.convert(coll_flow, 'cfm', 'm^3/s'))
ics_performance = collector_plate.solarCollectorPerformance
# Values are based on spec sheet + OG-100 listing for Solarheart ICS collectors
ics_performance.setName(obj_name + ' coll perf')
ics_performance.setGrossArea(UnitConversions.convert(collector_area, 'ft^2', 'm^2'))
ics_performance.setCollectorWaterVolume(UnitConversions.convert(storage_volume, 'gal', 'm^3'))
ics_performance.setBottomHeatLossConductance(1.902) # Spec sheet
ics_performance.setSideHeatLossConductance(1.268)
ics_performance.setAspectRatio(0.721)
ics_performance.setCollectorSideHeight(0.17272)
ics_performance.setNumberOfCovers(1)
ics_performance.setAbsorptanceOfAbsorberPlate(0.94)
ics_performance.setEmissivityOfAbsorberPlate(0.56)
collector_plate.setSolarCollectorPerformance(ics_performance)
else
collector_plate = OpenStudio::Model::SolarCollectorFlatPlateWater.new(model)
collector_plate.setName(obj_name + ' coll plate')
collector_plate.setSurface(shading_surface)
collector_plate.setMaximumFlowRate(UnitConversions.convert(coll_flow, 'cfm', 'm^3/s'))
collector_performance = collector_plate.solarCollectorPerformance
collector_performance.setName(obj_name + ' coll perf')
collector_performance.setGrossArea(UnitConversions.convert(collector_area, 'ft^2', 'm^2'))
collector_performance.setTestFluid('Water')
collector_performance.setTestFlowRate(UnitConversions.convert(coll_flow, 'cfm', 'm^3/s'))
collector_performance.setTestCorrelationType('Inlet')
collector_performance.setCoefficient1ofEfficiencyEquation(solar_thermal_system.collector_frta)
collector_performance.setCoefficient2ofEfficiencyEquation(-UnitConversions.convert(solar_thermal_system.collector_frul, 'Btu/(hr*ft^2*F)', 'W/(m^2*K)'))
collector_performance.setCoefficient2ofIncidentAngleModifier(-iam_coeff2)
collector_performance.setCoefficient3ofIncidentAngleModifier(iam_coeff3)
end
plant_loop.addSupplyBranchForComponent(collector_plate)
pipe_supply_bypass = OpenStudio::Model::PipeAdiabatic.new(model)
pipe_supply_outlet = OpenStudio::Model::PipeAdiabatic.new(model)
pipe_demand_bypass = OpenStudio::Model::PipeAdiabatic.new(model)
pipe_demand_inlet = OpenStudio::Model::PipeAdiabatic.new(model)
pipe_demand_outlet = OpenStudio::Model::PipeAdiabatic.new(model)
plant_loop.addSupplyBranchForComponent(pipe_supply_bypass)
pump.addToNode(plant_loop.supplyInletNode)
pipe_supply_outlet.addToNode(plant_loop.supplyOutletNode)
setpoint_manager.addToNode(plant_loop.supplyOutletNode)
plant_loop.addDemandBranchForComponent(pipe_demand_bypass)
pipe_demand_inlet.addToNode(plant_loop.demandInletNode)
pipe_demand_outlet.addToNode(plant_loop.demandOutletNode)
storage_tank = OpenStudio::Model::WaterHeaterStratified.new(model)
storage_tank.setName(obj_name + ' storage tank')
storage_tank.setSourceSideEffectiveness(heat_ex_eff)
storage_tank.setTankShape('VerticalCylinder')
if (solar_thermal_system.collector_type == HPXML::SolarThermalTypeICS) || (fluid_type == Constants.FluidWater) # Use a 60 gal tank dummy tank for direct systems, storage volume for ICS is assumed to be collector volume
tank_volume = UnitConversions.convert(60 * unit_multiplier, 'gal', 'm^3')
else
tank_volume = UnitConversions.convert(storage_volume, 'gal', 'm^3')
end
tank_height = UnitConversions.convert(4.5, 'ft', 'm')
storage_tank.setTankVolume(tank_volume)
storage_tank.setTankHeight(tank_height)
storage_tank.setUseSideOutletHeight(tank_height)
storage_tank.setSourceSideInletHeight(tank_height / 3.0)
storage_tank.setMaximumTemperatureLimit(99)
storage_tank.heater1SetpointTemperatureSchedule.remove
storage_tank.setHeater1SetpointTemperatureSchedule(setpoint_schedule_one)
storage_tank.setHeater1Capacity(0)
storage_tank.setHeater1Height(0)
storage_tank.heater2SetpointTemperatureSchedule.remove
storage_tank.setHeater2SetpointTemperatureSchedule(setpoint_schedule_two)
storage_tank.setHeater2Capacity(0)
storage_tank.setHeater2Height(0)
storage_tank.setHeaterFuelType(EPlus::FuelTypeElectricity)
storage_tank.setHeaterThermalEfficiency(1)
storage_tank.ambientTemperatureSchedule.get.remove
set_wh_ambient(loc_space, loc_schedule, storage_tank)
storage_tank.setSkinLossFractiontoZone(1.0 / unit_multiplier) # Tank losses are multiplied by E+ zone multiplier, so need to compensate here
storage_tank.setOffCycleFlueLossFractiontoZone(1.0 / unit_multiplier)
storage_tank.setUseSideEffectiveness(1)
storage_tank.setUseSideInletHeight(0)
storage_tank.setSourceSideOutletHeight(0)
storage_tank.setInletMode('Fixed')
storage_tank.setIndirectWaterHeatingRecoveryTime(1.5)
storage_tank.setNumberofNodes(8)
storage_tank.setAdditionalDestratificationConductivity(0)
storage_tank.setSourceSideDesignFlowRate(UnitConversions.convert(coll_flow, 'cfm', 'm^3/s'))
storage_tank.setOnCycleParasiticFuelConsumptionRate(0)
storage_tank.setOffCycleParasiticFuelConsumptionRate(0)
storage_tank.setUseSideDesignFlowRate(UnitConversions.convert(storage_volume, 'gal', 'm^3') / 60.1) # Sized to ensure that E+ never autosizes the design flow rate to be larger than the tank volume getting drawn out in a hour (60 minutes)
set_stratified_tank_ua(storage_tank, u_tank, unit_multiplier)
storage_tank.additionalProperties.setFeature('HPXML_ID', solar_thermal_system.water_heating_system.id) # Used by reporting measure
storage_tank.additionalProperties.setFeature('ObjectType', Constants.ObjectNameSolarHotWater) # Used by reporting measure
plant_loop.addDemandBranchForComponent(storage_tank)
dhw_loop.addSupplyBranchForComponent(storage_tank)
water_heater.addToNode(storage_tank.supplyOutletModelObject.get.to_Node.get)
availability_manager = OpenStudio::Model::AvailabilityManagerDifferentialThermostat.new(model)
availability_manager.setName(obj_name + ' useful energy')
availability_manager.setHotNode(collector_plate.outletModelObject.get.to_Node.get)
availability_manager.setColdNode(storage_tank.demandOutletModelObject.get.to_Node.get)
availability_manager.setTemperatureDifferenceOnLimit(0)
availability_manager.setTemperatureDifferenceOffLimit(0)
plant_loop.addAvailabilityManager(availability_manager)
# Add EMS code for SWH control (keeps the WH for the last hour if there's useful energy that can be delivered, E+ wouldn't always do this by default)
# Sensors
coll_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'System Node Temperature')
coll_sensor.setName("#{obj_name} Collector Outlet")
coll_sensor.setKeyName("#{collector_plate.outletModelObject.get.to_Node.get.name}")
tank_source_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'System Node Temperature')
tank_source_sensor.setName("#{obj_name} Tank Source Inlet")
tank_source_sensor.setKeyName("#{storage_tank.demandOutletModelObject.get.to_Node.get.name}")
# Actuators
swh_pump_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(pump, *EPlus::EMSActuatorPumpMassFlowRate)
swh_pump_actuator.setName("#{obj_name}_pump")
# Program
swh_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
swh_program.setName("#{obj_name} Controller")
swh_program.addLine("If #{coll_sensor.name} > #{tank_source_sensor.name}")
swh_program.addLine("Set #{swh_pump_actuator.name} = 100 * #{unit_multiplier}")
swh_program.addLine('Else')
swh_program.addLine("Set #{swh_pump_actuator.name} = 0")
swh_program.addLine('EndIf')
# ProgramCallingManager
program_calling_manager = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model)
program_calling_manager.setName("#{obj_name} Control")
program_calling_manager.setCallingPoint('InsideHVACSystemIterationLoop')
program_calling_manager.addProgram(swh_program)
end
private
def self.setup_hpwh_wrapped_condenser(model, obj_name_hpwh, coil, tank, fan, h_tank, airflow_rate, hpwh_tamb, hpwh_rhamb, min_temp, max_temp, setpoint_schedule, unit_multiplier)
h_condtop = (1.0 - (5.5 / 12.0)) * h_tank # in the 6th node of the tank (counting from top)
h_condbot = 0.01 * unit_multiplier # bottom node
h_hpctrl_up = (1.0 - (2.5 / 12.0)) * h_tank # in the 3rd node of the tank
h_hpctrl_low = (1.0 - (8.5 / 12.0)) * h_tank # in the 9th node of the tank
hpwh = OpenStudio::Model::WaterHeaterHeatPumpWrappedCondenser.new(model, coil, tank, fan, setpoint_schedule, model.alwaysOnDiscreteSchedule)
hpwh.setName("#{obj_name_hpwh} hpwh")
hpwh.setDeadBandTemperatureDifference(3.89)
hpwh.setCondenserBottomLocation(h_condbot)
hpwh.setCondenserTopLocation(h_condtop)
hpwh.setEvaporatorAirFlowRate(UnitConversions.convert(airflow_rate * unit_multiplier, 'ft^3/min', 'm^3/s'))
hpwh.setInletAirConfiguration('Schedule')
hpwh.setInletAirTemperatureSchedule(hpwh_tamb)
hpwh.setInletAirHumiditySchedule(hpwh_rhamb)
hpwh.setMinimumInletAirTemperatureforCompressorOperation(UnitConversions.convert(min_temp, 'F', 'C'))
hpwh.setMaximumInletAirTemperatureforCompressorOperation(UnitConversions.convert(max_temp, 'F', 'C'))
hpwh.setCompressorLocation('Schedule')
hpwh.setCompressorAmbientTemperatureSchedule(hpwh_tamb)
hpwh.setFanPlacement('DrawThrough')
hpwh.setOnCycleParasiticElectricLoad(0)
hpwh.setOffCycleParasiticElectricLoad(0)
hpwh.setParasiticHeatRejectionLocation('Outdoors')
hpwh.setTankElementControlLogic('MutuallyExclusive')
hpwh.setControlSensor1HeightInStratifiedTank(h_hpctrl_up)
hpwh.setControlSensor1Weight(0.75)
hpwh.setControlSensor2HeightInStratifiedTank(h_hpctrl_low)
return hpwh
end
def self.setup_hpwh_dxcoil(model, runner, water_heating_system, elevation, obj_name_hpwh, airflow_rate, unit_multiplier)
# Curves
hpwh_cap = OpenStudio::Model::CurveBiquadratic.new(model)
hpwh_cap.setName('HPWH-Cap-fT')
hpwh_cap.setCoefficient1Constant(0.563)
hpwh_cap.setCoefficient2x(0.0437)
hpwh_cap.setCoefficient3xPOW2(0.000039)
hpwh_cap.setCoefficient4y(0.0055)
hpwh_cap.setCoefficient5yPOW2(-0.000148)
hpwh_cap.setCoefficient6xTIMESY(-0.000145)
hpwh_cap.setMinimumValueofx(0)
hpwh_cap.setMaximumValueofx(100)
hpwh_cap.setMinimumValueofy(0)
hpwh_cap.setMaximumValueofy(100)
hpwh_cop = OpenStudio::Model::CurveBiquadratic.new(model)
hpwh_cop.setName('HPWH-COP-fT')
hpwh_cop.setCoefficient1Constant(1.1332)
hpwh_cop.setCoefficient2x(0.063)
hpwh_cop.setCoefficient3xPOW2(-0.0000979)
hpwh_cop.setCoefficient4y(-0.00972)
hpwh_cop.setCoefficient5yPOW2(-0.0000214)
hpwh_cop.setCoefficient6xTIMESY(-0.000686)
hpwh_cop.setMinimumValueofx(0)
hpwh_cop.setMaximumValueofx(100)
hpwh_cop.setMinimumValueofy(0)
hpwh_cop.setMaximumValueofy(100)
# Assumptions and values
cap = 0.5 * unit_multiplier # kW
shr = 0.88 # unitless
# Calculate an altitude adjusted rated evaporator wetbulb temperature
rated_ewb_F = 56.4
rated_edb_F = 67.5
p_atm = UnitConversions.convert(1.0, 'atm', 'psi')
rated_edb = UnitConversions.convert(rated_edb_F, 'F', 'C')
w_rated = Psychrometrics.w_fT_Twb_P(rated_edb_F, rated_ewb_F, p_atm)
dp_rated = Psychrometrics.Tdp_fP_w(runner, p_atm, w_rated)
p_atm = Psychrometrics.Pstd_fZ(elevation)
w_adj = Psychrometrics.w_fT_Twb_P(dp_rated, dp_rated, p_atm)
twb_adj = Psychrometrics.Twb_fT_w_P(runner, rated_edb_F, w_adj, p_atm)
# Calculate the COP based on EF
if not water_heating_system.energy_factor.nil?
uef = (0.60522 + water_heating_system.energy_factor) / 1.2101
cop = 1.174536058 * uef # Based on simulation of the UEF test procedure at varying COPs
elsif not water_heating_system.uniform_energy_factor.nil?
uef = water_heating_system.uniform_energy_factor
if water_heating_system.usage_bin == HPXML::WaterHeaterUsageBinVerySmall
fail 'It is unlikely that a heat pump water heater falls into the very small bin of the First Hour Rating (FHR) test. Double check input.'
elsif water_heating_system.usage_bin == HPXML::WaterHeaterUsageBinLow
cop = 1.0005 * uef - 0.0789
elsif water_heating_system.usage_bin == HPXML::WaterHeaterUsageBinMedium
cop = 1.0909 * uef - 0.0868
elsif water_heating_system.usage_bin == HPXML::WaterHeaterUsageBinHigh
cop = 1.1022 * uef - 0.0877
end
end
coil = OpenStudio::Model::CoilWaterHeatingAirToWaterHeatPumpWrapped.new(model)
coil.setName("#{obj_name_hpwh} coil")
coil.setRatedHeatingCapacity(UnitConversions.convert(cap, 'kW', 'W') * cop)
coil.setRatedCOP(cop)
coil.setRatedSensibleHeatRatio(shr)
coil.setRatedEvaporatorInletAirDryBulbTemperature(rated_edb)
coil.setRatedEvaporatorInletAirWetBulbTemperature(UnitConversions.convert(twb_adj, 'F', 'C'))
coil.setRatedCondenserWaterTemperature(48.89)
coil.setRatedEvaporatorAirFlowRate(UnitConversions.convert(airflow_rate * unit_multiplier, 'ft^3/min', 'm^3/s'))
coil.setEvaporatorFanPowerIncludedinRatedCOP(true)
coil.setEvaporatorAirTemperatureTypeforCurveObjects('WetBulbTemperature')
coil.setHeatingCapacityFunctionofTemperatureCurve(hpwh_cap)
coil.setHeatingCOPFunctionofTemperatureCurve(hpwh_cop)
coil.setMaximumAmbientTemperatureforCrankcaseHeaterOperation(0)
coil.additionalProperties.setFeature('HPXML_ID', water_heating_system.id) # Used by reporting measure
return coil
end
def self.setup_hpwh_stratified_tank(model, water_heating_system, obj_name_hpwh, h_tank, solar_fraction, hpwh_tamb, hpwh_bottom_element_sp, hpwh_top_element_sp, unit_multiplier, nbeds)
# Calculate some geometry parameters for UA, the location of sensors and heat sources in the tank
v_actual = calc_storage_tank_actual_vol(water_heating_system.tank_volume, water_heating_system.fuel_type) # gal
a_tank, a_side = calc_tank_areas(v_actual, UnitConversions.convert(h_tank, 'm', 'ft')) # sqft
e_cap = 4.5 # kW
parasitics = 3.0 # W
# Based on Ecotope lab testing of most recent AO Smith HPWHs (series HPTU)
if water_heating_system.tank_volume <= 58.0
tank_ua = 3.6 # Btu/h-R
elsif water_heating_system.tank_volume <= 73.0
tank_ua = 4.0 # Btu/h-R
else
tank_ua = 4.7 # Btu/h-R
end
tank_ua = apply_tank_jacket(water_heating_system, tank_ua, a_side)
tank_ua = apply_shared_adjustment(water_heating_system, tank_ua, nbeds) # shared losses
u_tank = ((5.678 * tank_ua) / a_tank) * (1.0 - solar_fraction)
v_actual *= unit_multiplier
e_cap *= unit_multiplier
parasitics *= unit_multiplier
h_UE = (1.0 - (3.5 / 12.0)) * h_tank # in the 3rd node of the tank (counting from top)
h_LE = (1.0 - (9.5 / 12.0)) * h_tank # in the 10th node of the tank (counting from top)
tank = OpenStudio::Model::WaterHeaterStratified.new(model)
tank.setName("#{obj_name_hpwh} tank")
tank.setEndUseSubcategory('Domestic Hot Water')
tank.setTankVolume(UnitConversions.convert(v_actual, 'gal', 'm^3'))
tank.setTankHeight(h_tank)
tank.setMaximumTemperatureLimit(90)
tank.setHeaterPriorityControl('MasterSlave')
tank.heater1SetpointTemperatureSchedule.remove
tank.setHeater1SetpointTemperatureSchedule(hpwh_top_element_sp)
tank.setHeater1Capacity(UnitConversions.convert(e_cap, 'kW', 'W'))
tank.setHeater1Height(h_UE)
tank.setHeater1DeadbandTemperatureDifference(18.5)
tank.heater2SetpointTemperatureSchedule.remove
tank.setHeater2SetpointTemperatureSchedule(hpwh_bottom_element_sp)
tank.setHeater2Capacity(UnitConversions.convert(e_cap, 'kW', 'W'))
tank.setHeater2Height(h_LE)
tank.setHeater2DeadbandTemperatureDifference(3.89)
tank.setHeaterFuelType(EPlus::FuelTypeElectricity)
tank.setHeaterThermalEfficiency(1)
tank.setOffCycleParasiticFuelConsumptionRate(parasitics)
tank.setOffCycleParasiticFuelType(EPlus::FuelTypeElectricity)
tank.setOnCycleParasiticFuelConsumptionRate(parasitics)
tank.setOnCycleParasiticFuelType(EPlus::FuelTypeElectricity)
tank.ambientTemperatureSchedule.get.remove
tank.setAmbientTemperatureSchedule(hpwh_tamb)
tank.setNumberofNodes(6)
tank.setAdditionalDestratificationConductivity(0)
tank.setUseSideDesignFlowRate(UnitConversions.convert(v_actual, 'gal', 'm^3') / 60.1) # Sized to ensure that E+ never autosizes the design flow rate to be larger than the tank volume getting drawn out in a hour (60 minutes)
tank.setSourceSideDesignFlowRate(0)
tank.setSourceSideFlowControlMode('')
tank.setSourceSideInletHeight(0)
tank.setSourceSideOutletHeight(0)
tank.setSkinLossFractiontoZone(1.0 / unit_multiplier) # Tank losses are multiplied by E+ zone multiplier, so need to compensate here
tank.setOffCycleFlueLossFractiontoZone(1.0 / unit_multiplier)
set_stratified_tank_ua(tank, u_tank, unit_multiplier)
tank.additionalProperties.setFeature('HPXML_ID', water_heating_system.id) # Used by reporting measure
return tank
end
def self.setup_hpwh_fan(model, water_heating_system, obj_name_hpwh, airflow_rate, unit_multiplier)
fan_power = 0.0462 # W/cfm, Based on 1st gen AO Smith HPWH, could be updated but pretty minor impact
fan = OpenStudio::Model::FanSystemModel.new(model)
fan.setSpeedControlMethod('Discrete')
fan.setDesignPowerSizingMethod('PowerPerFlow')
fan.setElectricPowerPerUnitFlowRate(fan_power / UnitConversions.convert(1.0, 'cfm', 'm^3/s'))
fan.setAvailabilitySchedule(model.alwaysOnDiscreteSchedule)
fan.setName(obj_name_hpwh + ' fan')
fan.setEndUseSubcategory('Domestic Hot Water')
fan.setMotorEfficiency(1.0)
fan.setMotorInAirStreamFraction(1.0)
fan.setDesignMaximumAirFlowRate(UnitConversions.convert(airflow_rate * unit_multiplier, 'ft^3/min', 'm^3/s'))
fan.additionalProperties.setFeature('HPXML_ID', water_heating_system.id) # Used by reporting measure
fan.additionalProperties.setFeature('ObjectType', Constants.ObjectNameWaterHeater) # Used by reporting measure
return fan
end
def self.get_loc_temp_rh_sensors(model, obj_name_hpwh, loc_schedule, loc_space, conditioned_zone)
rh_sensors = []
if not loc_schedule.nil?
amb_temp_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
amb_temp_sensor.setName("#{obj_name_hpwh} amb temp")
amb_temp_sensor.setKeyName(loc_schedule.name.to_s)
if loc_schedule.name.get == HPXML::LocationOtherNonFreezingSpace
amb_rh_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Site Outdoor Air Relative Humidity')
amb_rh_sensor.setName("#{obj_name_hpwh} amb rh")
amb_rh_sensor.setKeyName('Environment')
rh_sensors << amb_rh_sensor
elsif loc_schedule.name.get == HPXML::LocationOtherHousingUnit
amb_rh_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Air Relative Humidity')
amb_rh_sensor.setName("#{obj_name_hpwh} amb rh")
amb_rh_sensor.setKeyName(conditioned_zone.name.to_s)
rh_sensors << amb_rh_sensor
else
amb_rh_sensor1 = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Site Outdoor Air Relative Humidity')
amb_rh_sensor1.setName("#{obj_name_hpwh} amb1 rh")
amb_rh_sensor1.setKeyName('Environment')
amb_rh_sensor2 = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Air Relative Humidity')
amb_rh_sensor2.setName("#{obj_name_hpwh} amb2 rh")
amb_rh_sensor2.setKeyName(conditioned_zone.name.to_s)
rh_sensors << amb_rh_sensor1
rh_sensors << amb_rh_sensor2
end
elsif not loc_space.nil?
amb_temp_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Mean Air Temperature')
amb_temp_sensor.setName("#{obj_name_hpwh} amb temp")
amb_temp_sensor.setKeyName(loc_space.thermalZone.get.name.to_s)
amb_rh_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Zone Air Relative Humidity')
amb_rh_sensor.setName("#{obj_name_hpwh} amb rh")
amb_rh_sensor.setKeyName(loc_space.thermalZone.get.name.to_s)
rh_sensors << amb_rh_sensor
else # Located outside
amb_temp_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Site Outdoor Air Drybulb Temperature')
amb_temp_sensor.setName("#{obj_name_hpwh} amb temp")
amb_temp_sensor.setKeyName('Environment')
amb_rh_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Site Outdoor Air Relative Humidity')
amb_rh_sensor.setName("#{obj_name_hpwh} amb rh")
amb_rh_sensor.setKeyName('Environment')
rh_sensors << amb_rh_sensor
end
return amb_temp_sensor, rh_sensors
end
def self.add_hpwh_inlet_air_and_zone_heat_gain_program(model, obj_name_hpwh, loc_space, hpwh_tamb, hpwh_rhamb, tank, coil, fan, amb_temp_sensor, amb_rh_sensors, unit_multiplier)
# EMS Actuators: Inlet T & RH, sensible and latent gains to the space
tamb_act_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(hpwh_tamb, *EPlus::EMSActuatorScheduleConstantValue)
tamb_act_actuator.setName("#{obj_name_hpwh} Tamb act")
rhamb_act_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(hpwh_rhamb, *EPlus::EMSActuatorScheduleConstantValue)
rhamb_act_actuator.setName("#{obj_name_hpwh} RHamb act")
if not loc_space.nil? # If located in space
# Add in other equipment objects for sensible/latent gains
hpwh_sens_def = OpenStudio::Model::OtherEquipmentDefinition.new(model)
hpwh_sens_def.setName("#{obj_name_hpwh} sens")
hpwh_sens = OpenStudio::Model::OtherEquipment.new(hpwh_sens_def)
hpwh_sens.setName(hpwh_sens_def.name.to_s)
hpwh_sens.setSpace(loc_space)
hpwh_sens_def.setDesignLevel(0)
hpwh_sens_def.setFractionRadiant(0)
hpwh_sens_def.setFractionLatent(0)
hpwh_sens_def.setFractionLost(0)
hpwh_sens.setSchedule(model.alwaysOnDiscreteSchedule)
hpwh_lat_def = OpenStudio::Model::OtherEquipmentDefinition.new(model)
hpwh_lat_def.setName("#{obj_name_hpwh} lat")
hpwh_lat = OpenStudio::Model::OtherEquipment.new(hpwh_lat_def)
hpwh_lat.setName(hpwh_lat_def.name.to_s)
hpwh_lat.setSpace(loc_space)
hpwh_lat_def.setDesignLevel(0)
hpwh_lat_def.setFractionRadiant(0)
hpwh_lat_def.setFractionLatent(1)
hpwh_lat_def.setFractionLost(0)
hpwh_lat.setSchedule(model.alwaysOnDiscreteSchedule)
sens_act_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(hpwh_sens, *EPlus::EMSActuatorOtherEquipmentPower, hpwh_sens.space.get)
sens_act_actuator.setName("#{hpwh_sens.name} act")
lat_act_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(hpwh_lat, *EPlus::EMSActuatorOtherEquipmentPower, hpwh_lat.space.get)
lat_act_actuator.setName("#{hpwh_lat.name} act")
end
# EMS Sensors: HP sens and latent loads, tank losses, fan power
tl_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Water Heater Heat Loss Rate')
tl_sensor.setName("#{obj_name_hpwh} tl")
tl_sensor.setKeyName(tank.name.to_s)
sens_cool_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Cooling Coil Sensible Cooling Rate')
sens_cool_sensor.setName("#{obj_name_hpwh} sens cool")
sens_cool_sensor.setKeyName(coil.name.to_s)
lat_cool_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Cooling Coil Latent Cooling Rate')
lat_cool_sensor.setName("#{obj_name_hpwh} lat cool")
lat_cool_sensor.setKeyName(coil.name.to_s)
fan_power_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, "Fan #{EPlus::FuelTypeElectricity} Rate")
fan_power_sensor.setName("#{obj_name_hpwh} fan pwr")
fan_power_sensor.setKeyName(fan.name.to_s)
hpwh_inlet_air_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
hpwh_inlet_air_program.setName("#{obj_name_hpwh} InletAir")
hpwh_inlet_air_program.addLine("Set #{tamb_act_actuator.name} = #{amb_temp_sensor.name}")
# Average relative humidity for mf spaces: other multifamily buffer space & other heated space
hpwh_inlet_air_program.addLine("Set #{rhamb_act_actuator.name} = 0")
amb_rh_sensors.each do |amb_rh_sensor|
hpwh_inlet_air_program.addLine("Set #{rhamb_act_actuator.name} = #{rhamb_act_actuator.name} + (#{amb_rh_sensor.name} / 100) / #{amb_rh_sensors.size}")
end
if not loc_space.nil?
# Sensible/latent heat gain to the space
# Tank losses are multiplied by E+ zone multiplier, so need to compensate here
hpwh_inlet_air_program.addLine("Set #{sens_act_actuator.name} = (0 - #{sens_cool_sensor.name} - (#{tl_sensor.name} + #{fan_power_sensor.name})) / #{unit_multiplier}")
hpwh_inlet_air_program.addLine("Set #{lat_act_actuator.name} = (0 - #{lat_cool_sensor.name}) / #{unit_multiplier}")
end
return hpwh_inlet_air_program
end
def self.add_hpwh_control_program(model, runner, obj_name_hpwh, amb_temp_sensor, hpwh_top_element_sp, hpwh_bottom_element_sp, min_temp, max_temp, op_mode, setpoint_schedule, control_setpoint_schedule, schedules_file)
# Lower element is enabled if the ambient air temperature prevents the HP from running
leschedoverride_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(hpwh_bottom_element_sp, *EPlus::EMSActuatorScheduleConstantValue)
leschedoverride_actuator.setName("#{obj_name_hpwh} LESchedOverride")
# Upper element is enabled unless mode is HP_only
ueschedoverride_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(hpwh_top_element_sp, *EPlus::EMSActuatorScheduleConstantValue)
ueschedoverride_actuator.setName("#{obj_name_hpwh} UESchedOverride")
# Actuator for setpoint schedule
if control_setpoint_schedule.to_ScheduleConstant.is_initialized
hpwhschedoverride_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(control_setpoint_schedule, *EPlus::EMSActuatorScheduleConstantValue)
elsif control_setpoint_schedule.to_ScheduleRuleset.is_initialized
hpwhschedoverride_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(control_setpoint_schedule, *EPlus::EMSActuatorScheduleYearValue)
end
hpwhschedoverride_actuator.setName("#{obj_name_hpwh} HPWHSchedOverride")
# EMS for the HPWH control logic
t_set_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
t_set_sensor.setName("#{obj_name_hpwh} T_set")
t_set_sensor.setKeyName(setpoint_schedule.name.to_s)
op_mode_schedule = nil
if not schedules_file.nil?
op_mode_schedule = schedules_file.create_schedule_file(model, col_name: SchedulesFile::Columns[:WaterHeaterOperatingMode].name)
end
# Sensor on op_mode_schedule
if not op_mode_schedule.nil?
Schedule.set_schedule_type_limits(model, op_mode_schedule, Constants.ScheduleTypeLimitsFraction)
op_mode_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Schedule Value')
op_mode_sensor.setName("#{obj_name_hpwh} op_mode")
op_mode_sensor.setKeyName(op_mode_schedule.name.to_s)
runner.registerWarning("Both '#{SchedulesFile::Columns[:WaterHeaterOperatingMode].name}' schedule file and operating mode provided; the latter will be ignored.") if !op_mode.nil?
end
t_offset = 9.0 # deg-C
min_temp_c = UnitConversions.convert(min_temp, 'F', 'C').round(2)
max_temp_c = UnitConversions.convert(max_temp, 'F', 'C').round(2)
hpwh_ctrl_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model)
hpwh_ctrl_program.setName("#{obj_name_hpwh} Control")
hpwh_ctrl_program.addLine("Set #{hpwhschedoverride_actuator.name} = #{t_set_sensor.name}")
# If in HP only mode: still enable elements if ambient temperature is out of bounds, otherwise disable elements
if op_mode == HPXML::WaterHeaterOperatingModeHeatPumpOnly
hpwh_ctrl_program.addLine("If (#{amb_temp_sensor.name}<#{min_temp_c}) || (#{amb_temp_sensor.name}>#{max_temp_c})")
hpwh_ctrl_program.addLine("Set #{leschedoverride_actuator.name} = #{t_set_sensor.name}")
hpwh_ctrl_program.addLine("Set #{ueschedoverride_actuator.name} = #{t_set_sensor.name}")
hpwh_ctrl_program.addLine('Else')