Class: AddElectricVehicleChargingLoad

Inherits:
OpenStudio::Measure::ModelMeasure
  • Object
show all
Defined in:
lib/measures/AddElectricVehicleChargingLoad/measure.rb

Overview

******************************************************************************* OpenStudio®, Copyright © Alliance for Sustainable Energy, LLC. See also openstudio.net/license *******************************************************************************

Defined Under Namespace

Classes: EVcharger, ElectricVehicle

Instance Method Summary collapse

Instance Method Details

#arguments(model) ⇒ Object

define the arguments that the user will input



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
# File 'lib/measures/AddElectricVehicleChargingLoad/measure.rb', line 26

def arguments(model)
  args = OpenStudio::Measure::OSArgumentVector.new

  # building use type, choice argument, 'home' or 'workplace'
  bldg_use_type_chs = OpenStudio::StringVector.new
  bldg_use_type_chs << 'home'
  bldg_use_type_chs << 'workplace'
  bldg_use_type_chs << 'commercial station'

  bldg_use_type = OpenStudio::Measure::OSArgument.makeChoiceArgument('bldg_use_type', bldg_use_type_chs, true)
  bldg_use_type.setDisplayName('Building Use Type')
  bldg_use_type.setDefaultValue('home')
  args << bldg_use_type

  # Number of EV chargers
  num_ev_chargers = OpenStudio::Measure::OSArgument.makeIntegerArgument('num_ev_chargers', true)
  num_ev_chargers.setDisplayName('Number of EV Chargers')
  num_ev_chargers.setDefaultValue(1)
  args << num_ev_chargers

  # Number of Electric Vehicles
  num_evs = OpenStudio::Measure::OSArgument.makeIntegerArgument('num_evs', true)
  num_evs.setDisplayName('Number of Electric Vehicles')
  num_evs.setDefaultValue(1)
  args << num_evs

  # EV charger level, choice argument
  charger_level_chs = OpenStudio::StringVector.new
  charger_level_chs << 'Level 1'
  charger_level_chs << 'Level 2'
  charger_level_chs << 'DC charger'
  charger_level_chs << 'Supercharger'

  charger_level = OpenStudio::Measure::OSArgument.makeChoiceArgument('charger_level', charger_level_chs, true)
  charger_level.setDisplayName('EV Charger Level')
  charger_level.setDefaultValue('Level 2')
  args << charger_level

  # average arrival time, applicable for workplace only
  avg_arrival_time = OpenStudio::Measure::OSArgument.makeStringArgument('avg_arrival_time', false)
  avg_arrival_time.setDisplayName('Average arrival time, applicable for workplace only')
  avg_arrival_time.setDefaultValue('08:30')
  args << avg_arrival_time

  # average arrival time, applicable for workplace only
  avg_leave_time = OpenStudio::Measure::OSArgument.makeStringArgument('avg_leave_time', false)
  avg_leave_time.setDisplayName('Average leave time, applicable for workplace only')
  avg_leave_time.setDefaultValue('17:30')
  args << avg_leave_time

  # start charging time, required for home
  start_charge_time = OpenStudio::Measure::OSArgument.makeStringArgument('start_charge_time', false)
  start_charge_time.setDisplayName('Start charging time, required for home only')
  start_charge_time.setDefaultValue('21:00')
  args << start_charge_time

  # average needed hours to charge to full. This should vary with the charger level.
  avg_charge_hours = OpenStudio::Measure::OSArgument.makeDoubleArgument('avg_charge_hours', true)
  avg_charge_hours.setDisplayName('Average needed hours to charge to full (should vary with charger level)')
  avg_charge_hours.setDefaultValue(4)
  args << avg_charge_hours

  # variation of arrival time in minutes
  arrival_time_variation_in_mins = OpenStudio::Measure::OSArgument.makeDoubleArgument('arrival_time_variation_in_mins', false)
  arrival_time_variation_in_mins.setDescription('Actual arrival time can vary a certain period before and after the average arrival time. '\
                                                  'This parameter describes this absolute time delta. '\
                                                  'In other words, average arrival time plus/minus this parameter constitutes the arrival time range. ')
  arrival_time_variation_in_mins.setDisplayName('Variation of arrival time in minutes')
  arrival_time_variation_in_mins.setDefaultValue(30)
  args << arrival_time_variation_in_mins

  # variation of charge time in minutes
  charge_time_variation_in_mins = OpenStudio::Measure::OSArgument.makeDoubleArgument('charge_time_variation_in_mins', false)
  charge_time_variation_in_mins.setDescription('Actual charge time can vary a certain period around the average charge hours. '\
                                                  'This parameter describes this absolute time delta. '\
                                                  'In other words, average charge hours plus/minus this parameter constitutes the charge time range. ')
  charge_time_variation_in_mins.setDisplayName('Variation of charge time in minutes')
  charge_time_variation_in_mins.setDefaultValue(60)
  args << charge_time_variation_in_mins

  # if EVs are charged on Saturday
  charge_on_sat = OpenStudio::Measure::OSArgument.makeBoolArgument('charge_on_sat', false)
  charge_on_sat.setDisplayName('EVs are charged on Saturday')
  charge_on_sat.setDefaultValue(true)
  args << charge_on_sat

  # if EVs are charged on Sunday
  charge_on_sun = OpenStudio::Measure::OSArgument.makeBoolArgument('charge_on_sun', false)
  charge_on_sun.setDisplayName('EVs are charged on Sunday')
  charge_on_sun.setDefaultValue(true)
  args << charge_on_sun

  return args
end

#create_ev_sch(model, ev_chargers, max_charging_power, charge_on_sat, charge_on_sun) ⇒ Object



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
# File 'lib/measures/AddElectricVehicleChargingLoad/measure.rb', line 267

def create_ev_sch(model, ev_chargers, max_charging_power, charge_on_sat, charge_on_sun)
  # create the schedule
  # Creating a schedule:ruleset
  ev_sch = OpenStudio::Model::ScheduleRuleset.new(model)
  ev_sch.setName('EV Charging Power Draw')
  ev_sch.defaultDaySchedule.setName('EV Charging Default')
  day_start_time = Time.strptime("00:00", '%H:%M')
  # initial EV load depends on if each charger charges overnight
  ev_load = 0   # kW
  ev_chargers.each do |ev_charger|
    puts "#{ev_charger.name}"
    puts "ev_charger.occupied_until_time - day_start_time - 24*60*60)/60: #{(ev_charger.occupied_until_time - day_start_time - 24*60*60)/60}"
    puts "ev_charger.occupied_start_time - day_start_time)/60: #{(ev_charger.occupied_start_time - day_start_time)/60}"
    puts "ev_charger.occupied_start_time: #{ev_charger.occupied_start_time}"
    puts "ev_charger.occupied_until_time: #{ev_charger.occupied_until_time}"
    puts "ev_charger.occupied_start_time.day: #{ev_charger.occupied_start_time.day}"
    puts "ev_charger.occupied_until_time.day: #{ev_charger.occupied_until_time.day}"
    ev_load += ev_charger.charging_power if ev_charger.occupied_start_time.day != ev_charger.occupied_until_time.day
  end
  ev_load_new = ev_load # kW
  puts "******Initial******"
  puts "ev_load: #{ev_load}"
  puts "ev_load_new: #{ev_load_new}"

  # iterate through 1440 minutes in one day
  for min in 1..24*60
    ev_chargers.each do |ev_charger|
      # charging on the same day
      if ev_charger.occupied_start_time.day == ev_charger.occupied_until_time.day
        if ((ev_charger.occupied_start_time - day_start_time)/60).to_i == min
          if ev_load_new == ev_load
            ev_load_new = ev_load + ev_charger.charging_power
          else  # if more than one chargers change status at this time point
            ev_load_new += ev_charger.charging_power
          end
        elsif ((ev_charger.occupied_until_time - day_start_time)/60).to_i == min
          if ev_load_new == ev_load
            ev_load_new = ev_load - ev_charger.charging_power
          else  # if more than one chargers change status at this time point
            ev_load_new -= ev_charger.charging_power
          end
        end
      else   # charging overnight
      if ((ev_charger.occupied_until_time - day_start_time - 24*60*60)/60).to_i == min
        if ev_load_new == ev_load
          ev_load_new = ev_load - ev_charger.charging_power
        else  # if more than one chargers change status at this time point
        ev_load_new -= ev_charger.charging_power
        end
      elsif ((ev_charger.occupied_start_time - day_start_time)/60).to_i == min
        if ev_load_new == ev_load
          ev_load_new = ev_load + ev_charger.charging_power
        else  # if more than one chargers change status at this time point
        ev_load_new += ev_charger.charging_power
        end
      end
      end
    end
    # if any change, add to schedule
    if ev_load_new != ev_load || min == 24*60
      puts "****after****"
      puts "ev_load_new: #{ev_load_new}"
      puts "ev_load: #{ev_load}"
      time = OpenStudio::Time.new(0, 0, min) # OpenStudio::Time.new(day,hr of day, minute of hr, seconds of hr?)
      ev_sch.defaultDaySchedule.addValue(time, (ev_load/max_charging_power).round(2))
      ev_load = ev_load_new
    end
  end

  if charge_on_sat
    ev_sch_sat = OpenStudio::Model::ScheduleRule.new(ev_sch, ev_sch.defaultDaySchedule)
    ev_sch_sat.setName('EV Charging Power Saturday')
    ev_sch_sat.setApplySaturday(true)
  else
    ev_sch_sat_rule = OpenStudio::Model::ScheduleRule.new(ev_sch)
    ev_sch_sat_rule.setName('EV Charging Power Saturday')
    ev_sch_sat_rule.setApplySaturday(true)
    ev_sch_sat = ev_sch_sat_rule.daySchedule
    ev_sch_sat.setName('EV Charging Saturday')
    ev_sch_sat.addValue(OpenStudio::Time.new(0,24,0), 0)
  end

  if charge_on_sun
    ev_sch_sun = OpenStudio::Model::ScheduleRule.new(ev_sch, ev_sch.defaultDaySchedule)
    ev_sch_sun.setName('EV Charging Power Sunday')
    ev_sch_sun.setApplySunday(true)
  else
    ev_sch_sun_rule = OpenStudio::Model::ScheduleRule.new(ev_sch)
    ev_sch_sun_rule.setName('EV Charging Power Sunday')
    ev_sch_sun_rule.setApplySunday(true)
    ev_sch_sun = ev_sch_sun_rule.daySchedule
    ev_sch_sun.setName('EV Charging Sunday')
    ev_sch_sun.addValue(OpenStudio::Time.new(0,24,0), 0)
  end

  return ev_sch
end

#create_ev_sch_for_commercial_charge_station(model, ev_chargers, max_charging_power, num_evs, avg_arrival_time, arrival_time_variation_in_mins, avg_charge_hours, charge_time_variation_in_mins, charge_on_sat, charge_on_sun) ⇒ Object



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
# File 'lib/measures/AddElectricVehicleChargingLoad/measure.rb', line 528

def create_ev_sch_for_commercial_charge_station(model, ev_chargers, max_charging_power, num_evs, avg_arrival_time, arrival_time_variation_in_mins, avg_charge_hours, charge_time_variation_in_mins, charge_on_sat, charge_on_sun)
  ev_list = []
  for j in 1..num_evs
    ev = ElectricVehicle.new("ev_#{j.to_s}")
    ev.arrival_time = avg_arrival_time + rand(-arrival_time_variation_in_mins...arrival_time_variation_in_mins) * 60  # TODO make sure time format is working correctly, Ruby Times "+" adopts seconds
    ev.needed_charge_hours = avg_charge_hours + rand(-charge_time_variation_in_mins...charge_time_variation_in_mins) / 60.0   # +- variation minutes
    ev_list << ev
  end

  # find the earliest arrival time
  arrival_time_earliest = Time.strptime("23:00", '%H:%M') + 3600  # initial: 24:00
  ev_list.each do |this_ev|
    if this_ev.arrival_time < arrival_time_earliest
      arrival_time_earliest = this_ev.arrival_time
    end
  end

  # For workplace: iterate through time, check status of each charger, if vacant, find the EV that has the earliest arrival time within uncharged EVs.
  # if this EV's leaving time is later than the current time, start charging until fully charged or leaving time, whichever comes first
  # when no EV is found any more, charging on this day ends, conclude the charging profile
  # 23 represent 23:00-24:00, corresponding to E+ schedule Until: 24:00
  ev_sch_list = []
  for hour in 0..23
    current_time = Time.strptime("#{hour}:00", '%H:%M') + 3600   # %H: 00..23, 23 should represent the period 23:00-24:00, so add 1 hour to be the check point
    next if arrival_time_earliest > current_time
    ev_chargers.each do |ev_charger|
      if ev_charger.occupied
        if ev_charger.connected_ev.class.to_s != 'AddElectricVehicleChargingLoad::ElectricVehicle'
          runner.registerError("EV charger #{ev_charger.name.to_s} shows occupied, but no EV is connected.")
          return false
        end
        # disconnect EV if charged to full. Only check if expected end time is earlier than current time, otherwise check in next iteration.
        # Time addition uses seconds, so needs to multiple 3600
        if ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600 <= current_time
          ev_charger.occupied_until_time_list << ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600
          ev_charger.connected_ev.end_charge_time = ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600
          ev_charger.occupied = false
          ev_charger.connected_ev.has_been_charged = true
          ev_charger.connected_ev.connected_to_charger = false
          ev_charger.connected_ev = nil
        end
      end
      # continue to check if charger not occupied, then connect to an EV
      unless ev_charger.occupied
        next_ev_to_charge = nil
        wait_list_time_earliest = Time.strptime("23:00", '%H:%M') + 3600  # initial: 24:00
        ev_list.each do |this_ev|
          # skip this EV if it is being charged or is being charged or already left
          next if this_ev.has_been_charged
          next if this_ev.connected_to_charger
          # get the uncharged, earliest arrival EV (so front in wait list)
          if this_ev.arrival_time < wait_list_time_earliest
            wait_list_time_earliest = this_ev.arrival_time
            next_ev_to_charge = this_ev
          end
        end
        # skip if no EV is on the wait list
        next if next_ev_to_charge.nil?
        if ev_charger.charged_ev_list.empty?
          ev_charger.occupied_start_time_list << wait_list_time_earliest
          next_ev_to_charge.start_charge_time = wait_list_time_earliest
        else
          if next_ev_to_charge.arrival_time < ev_charger.occupied_until_time_list[-1]
            next_ev_to_charge.start_charge_time = ev_charger.occupied_until_time_list[-1]
            ev_charger.occupied_start_time_list << ev_charger.occupied_until_time_list[-1]
          else
            next_ev_to_charge.start_charge_time = next_ev_to_charge.arrival_time
            ev_charger.occupied_start_time_list << next_ev_to_charge.arrival_time
          end
        end
        ev_charger.occupied = true
        next_ev_to_charge.connected_to_charger = true
        ev_charger.connected_ev = next_ev_to_charge
        ev_charger.charged_ev_list << next_ev_to_charge
      end
    end
  end

  ev_chargers.each do |ev_charger|
    # create schedule for each ev_charger
    # charger.charging_power
    ev_sch = create_ev_sch_single(model, ev_charger, charge_on_sat, charge_on_sun)
    ev_sch_list << ev_sch
  end

  # ev_sch = create_ev_sch(model, ev_chargers, max_charging_power, charge_on_sat, charge_on_sun)
  return ev_sch_list
end

#create_ev_sch_for_home(model, ev_chargers, max_charging_power, num_evs, start_charge_time, avg_charge_hours, charge_on_sat, charge_on_sun) ⇒ Object



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
# File 'lib/measures/AddElectricVehicleChargingLoad/measure.rb', line 618

def create_ev_sch_for_home(model, ev_chargers, max_charging_power, num_evs, start_charge_time, avg_charge_hours, charge_on_sat, charge_on_sun)
  ev_list = []
  for j in 1..num_evs
    ev = ElectricVehicle.new("ev_#{j.to_s}")
    ev.needed_charge_hours = avg_charge_hours + rand(-60...60) / 60.0   # +- 1 hour
    ev_list << ev
  end

  # for homes, EV charging could go overnight, so iterate 48 hours
  for hour in 0..47
    if hour <= 23
      current_time = Time.strptime("#{hour}:00", '%H:%M') + 3600   # %H: 00..23, 23 should represent the period 23:00-24:00, so add 1 hour to be the check point
    else
      current_time = Time.strptime("#{hour-24}:00", '%H:%M') + 24*60*60 + 3600   # %H: the second day (for overnight). still 00..23, 23 should represent the period 23:00-24:00, so add 1 hour to be the check point
    end
    next if start_charge_time > current_time
    ev_chargers.each do |ev_charger|
      if ev_charger.occupied
        if ev_charger.connected_ev.class.to_s != 'AddElectricVehicleChargingLoad::ElectricVehicle'
          runner.registerError("EV charger #{ev_charger.name.to_s} shows occupied, but no EV is connected.")
          return false
        end
        # Time addition uses seconds, so needs to multiple 3600
        if ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600 <= current_time
          ev_charger.connected_ev.end_charge_time = ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600
          ev_charger.occupied_until_time = ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600
          ev_charger.occupied = false
          ev_charger.connected_ev.has_been_charged = true
          ev_charger.connected_ev.connected_to_charger = false
          ev_charger.connected_ev = nil
        end
      end
      # continue to check if charger not occupied, then connect to an EV
      unless ev_charger.occupied
        # no need of waitlist, just connect to whichever EV that hasn't been charged
        next_ev_to_charge = nil
        ev_list.each do |this_ev|
          # skip this EV if it is being charged or is being charged
          next if this_ev.has_been_charged
          next if this_ev.connected_to_charger
          next_ev_to_charge = this_ev
          break
        end
        # skip if no EV is on the wait list
        next if next_ev_to_charge.nil?
        if ev_charger.charged_ev_list.empty?
          ev_charger.occupied_start_time = start_charge_time
          next_ev_to_charge.start_charge_time = start_charge_time
        else
          next_ev_to_charge.start_charge_time = ev_charger.occupied_until_time
        end
        ev_charger.occupied = true
        next_ev_to_charge.connected_to_charger = true
        ev_charger.connected_ev = next_ev_to_charge
        ev_charger.charged_ev_list << next_ev_to_charge
      end
    end
  end

  ev_sch = create_ev_sch(model, ev_chargers, max_charging_power, charge_on_sat, charge_on_sun)
  return ev_sch
end

#create_ev_sch_for_workplace(model, ev_chargers, max_charging_power, num_evs, avg_arrival_time, arrival_time_variation_in_mins, avg_leave_time, avg_charge_hours, charge_time_variation_in_mins, charge_on_sat, charge_on_sun) ⇒ Object

********************************************* for workplace waitlist is only applicable to workplace. For homes, charging is scheduled with start_charge_time create all EV chargers



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
# File 'lib/measures/AddElectricVehicleChargingLoad/measure.rb', line 445

def create_ev_sch_for_workplace(model, ev_chargers, max_charging_power, num_evs, avg_arrival_time, arrival_time_variation_in_mins, avg_leave_time, avg_charge_hours, charge_time_variation_in_mins, charge_on_sat, charge_on_sun)
  ev_list = []
  for j in 1..num_evs
    ev = ElectricVehicle.new("ev_#{j.to_s}")
    ev.arrival_time = avg_arrival_time + rand(-arrival_time_variation_in_mins...arrival_time_variation_in_mins) * 60  # TODO make sure time format is working correctly, Ruby Times "+" adopts seconds
    ev.leave_time = avg_leave_time + rand(-30...30) * 60  # TODO make sure time format is working correctly, Ruby Times "+" adopts seconds
    ev.leave_time = Time.strptime("23:00", '%H:%M') + 3600 if ev.leave_time > Time.strptime("23:00", '%H:%M') + 3600  # fix leave time at 24:00 if later than 24:00
    ev.needed_charge_hours = avg_charge_hours + rand(-charge_time_variation_in_mins...charge_time_variation_in_mins) / 60.0   # +- variation charge time
    ev_list << ev
  end

  # find the earliest arrival time
  arrival_time_earliest = Time.strptime("23:00", '%H:%M') + 3600  # initial: 24:00
  ev_list.each do |this_ev|
    if this_ev.arrival_time < arrival_time_earliest
      arrival_time_earliest = this_ev.arrival_time
    end
  end

  # For workplace: iterate through time, check status of each charger, if vacant, find the EV that has the earliest arrival time within uncharged EVs.
  # if this EV's leaving time is later than the current time, start charging until fully charged or leaving time, whichever comes first
  # when no EV is found any more, charging on this day ends, conclude the charging profile
  # 23 represent 23:00-24:00, corresponding to E+ schedule Until: 24:00
  for hour in 0..23
    current_time = Time.strptime("#{hour}:00", '%H:%M') + 3600   # %H: 00..23, 23 should represent the period 23:00-24:00, so add 1 hour to be the check point
    next if arrival_time_earliest > current_time
    ev_chargers.each do |ev_charger|
      if ev_charger.occupied
        if ev_charger.connected_ev.class.to_s != 'AddElectricVehicleChargingLoad::ElectricVehicle'
          runner.registerError("EV charger #{ev_charger.name.to_s} shows occupied, but no EV is connected.")
          return false
        end
        # disconnect EV if charged to full or till leave time. Only check if expected end time or leave is earlier than current time, otherwise check in next iteration.
        # Time addition uses seconds, so needs to multiple 3600
        if ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600 <= current_time || ev_charger.connected_ev.leave_time <= current_time
          if ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600 > ev_charger.connected_ev.leave_time
            ev_charger.occupied_until_time = ev_charger.connected_ev.leave_time
            ev_charger.connected_ev.end_charge_time = ev_charger.connected_ev.leave_time
          else
            ev_charger.occupied_until_time = ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600
            ev_charger.connected_ev.end_charge_time = ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600
          end
          ev_charger.occupied = false
          ev_charger.connected_ev.has_been_charged = true
          ev_charger.connected_ev.connected_to_charger = false
          ev_charger.connected_ev = nil
        end
      end
      # continue to check if charger not occupied, then connect to an EV
      unless ev_charger.occupied
        next_ev_to_charge = nil
        wait_list_time_earliest = Time.strptime("23:00", '%H:%M') + 3600  # initial: 24:00
        ev_list.each do |this_ev|
          # skip this EV if it is being charged or is being charged or already left
          next if this_ev.has_been_charged
          next if this_ev.connected_to_charger
          next if this_ev.leave_time <= current_time
          # get the uncharged, earliest arrival EV (so front in wait list)
          if this_ev.arrival_time < wait_list_time_earliest
            wait_list_time_earliest = this_ev.arrival_time
            next_ev_to_charge = this_ev
          end
        end
        # skip if no EV is on the wait list
        next if next_ev_to_charge.nil?
        if ev_charger.charged_ev_list.empty?
          ev_charger.occupied_start_time = wait_list_time_earliest
          next_ev_to_charge.start_charge_time = wait_list_time_earliest
        else
          next_ev_to_charge.start_charge_time = ev_charger.occupied_until_time
        end
        ev_charger.occupied = true
        next_ev_to_charge.connected_to_charger = true
        ev_charger.connected_ev = next_ev_to_charge
        ev_charger.charged_ev_list << next_ev_to_charge
      end
    end
  end

  ev_sch = create_ev_sch(model, ev_chargers, max_charging_power, charge_on_sat, charge_on_sun)
  return ev_sch
end

#create_ev_sch_single(model, ev_charger, charge_on_sat, charge_on_sun) ⇒ Object



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
# File 'lib/measures/AddElectricVehicleChargingLoad/measure.rb', line 365

def create_ev_sch_single(model, ev_charger, charge_on_sat, charge_on_sun)
  # create the schedule
  # Creating a schedule:ruleset
  ev_sch = OpenStudio::Model::ScheduleRuleset.new(model)
  ev_sch.setName("EV Charging Power Draw for charger #{ev_charger.name.to_s}")
  ev_sch.defaultDaySchedule.setName("EV Charging Default for charger #{ev_charger.name.to_s}")
  day_start_time = Time.strptime("00:00", '%H:%M')

  puts "ev_charger.occupied_start_time_list: #{ev_charger.occupied_start_time_list}"
  puts "ev_charger.occupied_until_time_list: #{ev_charger.occupied_until_time_list}"
  occupied_start_time_list = ev_charger.occupied_start_time_list
  occupied_until_time_list = ev_charger.occupied_until_time_list

  occupied_start_time_list.each_with_index do |occupied_start_time, idx|
    occupied_until_time = occupied_until_time_list[idx]
    if occupied_start_time.day == occupied_until_time.day
      # charging on the same day
      if idx > 0 && occupied_start_time == occupied_until_time_list[idx-1]
        # car charging are continuous without vacancy period
        end_time = OpenStudio::Time.new(0, 0, ((occupied_until_time - day_start_time)/60).to_i) # OpenStudio::Time.new(day,hr of day, minute of hr, seconds of hr?)
        ev_sch.defaultDaySchedule.addValue(end_time, 1)
      else
        # there are vacancy period between cars
        start_time = OpenStudio::Time.new(0, 0, ((occupied_start_time - day_start_time)/60).to_i) # OpenStudio::Time.new(day,hr of day, minute of hr, seconds of hr?)
        ev_sch.defaultDaySchedule.addValue(start_time, 0)
        end_time = OpenStudio::Time.new(0, 0, ((occupied_until_time - day_start_time)/60).to_i) # OpenStudio::Time.new(day,hr of day, minute of hr, seconds of hr?)
        ev_sch.defaultDaySchedule.addValue(end_time, 1)
      end
    else   # charging overnight
      if idx > 0 && occupied_start_time == occupied_until_time_list[idx-1]
        # car charging are continuous without vacancy period
        end_time_1 = OpenStudio::Time.new(0, 24, 0, 0)  # first till the end of the day
        end_time_2 = OpenStudio::Time.new(0, 0, ((occupied_until_time - day_start_time)/60).to_i) # OpenStudio::Time.new(day,hr of day, minute of hr, seconds of hr?)
        ev_sch.defaultDaySchedule.addValue(end_time_1, 1)
        ev_sch.defaultDaySchedule.addValue(end_time_2, 1)
      else
        # there are vacancy period between cars
        start_time = OpenStudio::Time.new(0, 0, ((occupied_start_time - day_start_time)/60).to_i) # OpenStudio::Time.new(day,hr of day, minute of hr, seconds of hr?)
        ev_sch.defaultDaySchedule.addValue(start_time, 0)
        end_time_1 = OpenStudio::Time.new(0, 24, 0, 0)  # first till the end of the day
        end_time_2 = OpenStudio::Time.new(0, 0, ((occupied_until_time - day_start_time)/60).to_i) # OpenStudio::Time.new(day,hr of day, minute of hr, seconds of hr?)
        ev_sch.defaultDaySchedule.addValue(end_time_1, 1)
        ev_sch.defaultDaySchedule.addValue(end_time_2, 1)
      end
    end
  end

  if charge_on_sat
    ev_sch_sat = OpenStudio::Model::ScheduleRule.new(ev_sch, ev_sch.defaultDaySchedule)
    ev_sch_sat.setName('EV Charging Power Saturday')
    ev_sch_sat.setApplySaturday(true)
  else
    ev_sch_sat_rule = OpenStudio::Model::ScheduleRule.new(ev_sch)
    ev_sch_sat_rule.setName('EV Charging Power Saturday')
    ev_sch_sat_rule.setApplySaturday(true)
    ev_sch_sat = ev_sch_sat_rule.daySchedule
    ev_sch_sat.setName('EV Charging Saturday')
    ev_sch_sat.addValue(OpenStudio::Time.new(0,24,0), 0)
  end

  if charge_on_sun
    ev_sch_sun = OpenStudio::Model::ScheduleRule.new(ev_sch, ev_sch.defaultDaySchedule)
    ev_sch_sun.setName('EV Charging Power Sunday')
    ev_sch_sun.setApplySunday(true)
  else
    ev_sch_sun_rule = OpenStudio::Model::ScheduleRule.new(ev_sch)
    ev_sch_sun_rule.setName('EV Charging Power Sunday')
    ev_sch_sun_rule.setApplySunday(true)
    ev_sch_sun = ev_sch_sun_rule.daySchedule
    ev_sch_sun.setName('EV Charging Sunday')
    ev_sch_sun.addValue(OpenStudio::Time.new(0,24,0), 0)
  end

  return ev_sch
end

#descriptionObject

human readable description



16
17
18
# File 'lib/measures/AddElectricVehicleChargingLoad/measure.rb', line 16

def description
  return 'This measure adds electric vehicle charging load to the building. The user can specify the level of charger, number of chargers, number of EVs charging daily, start time, average number of hours to fully charge. '
end

#modeler_descriptionObject

human readable description of modeling approach



21
22
23
# File 'lib/measures/AddElectricVehicleChargingLoad/measure.rb', line 21

def modeler_description
  return 'This measure will add electric vehicle charging load as exterior electric equipment. The user inputs of level of chargers, number of chargers, and number of EVs charging daily will be used to determine the load level, and the inputs of start time and average number of hours to fully charge will be used to determine load schedule.'
end

#nameObject

human readable name



10
11
12
13
# File 'lib/measures/AddElectricVehicleChargingLoad/measure.rb', line 10

def name
  # Measure name should be the title case of the class name.
  return 'AddElectricVehicleChargingLoad'
end

#run(model, runner, user_arguments) ⇒ Object

define what happens when the measure is run



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
# File 'lib/measures/AddElectricVehicleChargingLoad/measure.rb', line 172

def run(model, runner, user_arguments)
  super(model, runner, user_arguments)

  # use the built-in error checking
  if !runner.validateUserArguments(arguments(model), user_arguments)
    return false
  end

  # assign the user inputs to variables
  bldg_use_type = runner.getStringArgumentValue('bldg_use_type', user_arguments)
  num_ev_chargers = runner.getIntegerArgumentValue('num_ev_chargers', user_arguments)
  num_evs = runner.getIntegerArgumentValue('num_evs', user_arguments)
  charger_level = runner.getStringArgumentValue('charger_level', user_arguments)
  avg_arrival_time = runner.getStringArgumentValue('avg_arrival_time', user_arguments)
  avg_leave_time = runner.getStringArgumentValue('avg_leave_time', user_arguments)
  start_charge_time = runner.getStringArgumentValue('start_charge_time', user_arguments)
  avg_charge_hours = runner.getDoubleArgumentValue('avg_charge_hours', user_arguments)
  arrival_time_variation_in_mins = runner.getDoubleArgumentValue('arrival_time_variation_in_mins', user_arguments)
  charge_time_variation_in_mins = runner.getDoubleArgumentValue('charge_time_variation_in_mins', user_arguments)
  charge_on_sat = runner.getBoolArgumentValue('charge_on_sat', user_arguments)
  charge_on_sun = runner.getBoolArgumentValue('charge_on_sun', user_arguments)

  puts "num_ev_chargers: #{num_ev_chargers.inspect}"
  puts "avg_arrival_time: #{avg_arrival_time.inspect}"
  puts "avg_charge_hours: #{avg_charge_hours.inspect}"
  puts "charge_on_sat: #{charge_on_sat.inspect}"
  puts "charge_on_sun: #{charge_on_sun.inspect}"

  if bldg_use_type == 'workplace'
    # check avg_arrival_time and avg_leave_time should be in correct Time format
    begin
      avg_arrival_time = Time.strptime(avg_arrival_time, '%H:%M')
      avg_leave_time = Time.strptime(avg_leave_time, '%H:%M')
    rescue ArgumentError
      runner.registerError('For workplaces, average arrival and leave time are required, and should be in format of %H:%M, e.g., 16:00.')
      return false
    end
    # check avg_leave_time should be later than avg_arrival_time
    if avg_leave_time <= avg_arrival_time
      runner.registerError('For workplaces, average arrival time should be earlier than average leave time.')
      return false
    end
  elsif bldg_use_type == 'home'
    # check start_charge_time should be in correct Time format
    begin
      start_charge_time = Time.strptime(start_charge_time, '%H:%M')
    rescue ArgumentError
      runner.registerError('For homes, start charging time is required, and should be in format of %H:%M, e.g., 16:00.')
      return false
    end
  elsif bldg_use_type == 'commercial station'
    # check avg_arrival_time should be in correct Time format
    begin
      avg_arrival_time = Time.strptime(avg_arrival_time, '%H:%M')
    rescue ArgumentError
      runner.registerError('For commercial station, average arrival time is required, and should be in format of %H:%M, e.g., 10:00.')
      return false
    end
  else
    runner.registerError("Wrong building use type, available options: 'workplace' and 'home'.")
    return false
  end

  # report initial condition of model
  runner.registerInitialCondition("Starting to add electric vehicle to the building.")

  # Initialize the EV chargers
  ev_chargers = []
  max_charging_power = 0  # initial
  for i in 1..num_ev_chargers
    charger = EVcharger.new("EVcharger_#{i.to_s}")
    charger.level = charger_level
    # charging power references:
    # https://calevip.org/electric-vehicle-charging-101
    # https://rmi.org/electric-vehicle-charging-for-dummies/
    # https://freewiretech.com/difference-between-ev-charging-levels/#:~:text=Level%201%20Charging&text=L1%20chargers%20plug%20directly%20into,is%20sufficient%20for%20many%20commuters.
    case charger_level
    when 'Level 1'
      charger.charging_power = 1.5
    when 'Level 2'
      # charger.charging_power = 7.0
      charger.charging_power = 9.6   # C2C expert match input
    when 'DC charger'
      # charger.charging_power = 50.0
      charger.charging_power = 54.0  # C2C expert match input
    when 'Supercharger'
      charger.charging_power = 185
    else
      runner.registerError("Wrong EV charging level, available options: 'Level 1', 'Level 2', 'DC charger', 'Supercharger'.")
      return false
    end
    max_charging_power += charger.charging_power
    ev_chargers << charger
  end

  def create_ev_sch(model, ev_chargers, max_charging_power, charge_on_sat, charge_on_sun)
    # create the schedule
    # Creating a schedule:ruleset
    ev_sch = OpenStudio::Model::ScheduleRuleset.new(model)
    ev_sch.setName('EV Charging Power Draw')
    ev_sch.defaultDaySchedule.setName('EV Charging Default')
    day_start_time = Time.strptime("00:00", '%H:%M')
    # initial EV load depends on if each charger charges overnight
    ev_load = 0   # kW
    ev_chargers.each do |ev_charger|
      puts "#{ev_charger.name}"
      puts "ev_charger.occupied_until_time - day_start_time - 24*60*60)/60: #{(ev_charger.occupied_until_time - day_start_time - 24*60*60)/60}"
      puts "ev_charger.occupied_start_time - day_start_time)/60: #{(ev_charger.occupied_start_time - day_start_time)/60}"
      puts "ev_charger.occupied_start_time: #{ev_charger.occupied_start_time}"
      puts "ev_charger.occupied_until_time: #{ev_charger.occupied_until_time}"
      puts "ev_charger.occupied_start_time.day: #{ev_charger.occupied_start_time.day}"
      puts "ev_charger.occupied_until_time.day: #{ev_charger.occupied_until_time.day}"
      ev_load += ev_charger.charging_power if ev_charger.occupied_start_time.day != ev_charger.occupied_until_time.day
    end
    ev_load_new = ev_load # kW
    puts "******Initial******"
    puts "ev_load: #{ev_load}"
    puts "ev_load_new: #{ev_load_new}"

    # iterate through 1440 minutes in one day
    for min in 1..24*60
      ev_chargers.each do |ev_charger|
        # charging on the same day
        if ev_charger.occupied_start_time.day == ev_charger.occupied_until_time.day
          if ((ev_charger.occupied_start_time - day_start_time)/60).to_i == min
            if ev_load_new == ev_load
              ev_load_new = ev_load + ev_charger.charging_power
            else  # if more than one chargers change status at this time point
              ev_load_new += ev_charger.charging_power
            end
          elsif ((ev_charger.occupied_until_time - day_start_time)/60).to_i == min
            if ev_load_new == ev_load
              ev_load_new = ev_load - ev_charger.charging_power
            else  # if more than one chargers change status at this time point
              ev_load_new -= ev_charger.charging_power
            end
          end
        else   # charging overnight
        if ((ev_charger.occupied_until_time - day_start_time - 24*60*60)/60).to_i == min
          if ev_load_new == ev_load
            ev_load_new = ev_load - ev_charger.charging_power
          else  # if more than one chargers change status at this time point
          ev_load_new -= ev_charger.charging_power
          end
        elsif ((ev_charger.occupied_start_time - day_start_time)/60).to_i == min
          if ev_load_new == ev_load
            ev_load_new = ev_load + ev_charger.charging_power
          else  # if more than one chargers change status at this time point
          ev_load_new += ev_charger.charging_power
          end
        end
        end
      end
      # if any change, add to schedule
      if ev_load_new != ev_load || min == 24*60
        puts "****after****"
        puts "ev_load_new: #{ev_load_new}"
        puts "ev_load: #{ev_load}"
        time = OpenStudio::Time.new(0, 0, min) # OpenStudio::Time.new(day,hr of day, minute of hr, seconds of hr?)
        ev_sch.defaultDaySchedule.addValue(time, (ev_load/max_charging_power).round(2))
        ev_load = ev_load_new
      end
    end

    if charge_on_sat
      ev_sch_sat = OpenStudio::Model::ScheduleRule.new(ev_sch, ev_sch.defaultDaySchedule)
      ev_sch_sat.setName('EV Charging Power Saturday')
      ev_sch_sat.setApplySaturday(true)
    else
      ev_sch_sat_rule = OpenStudio::Model::ScheduleRule.new(ev_sch)
      ev_sch_sat_rule.setName('EV Charging Power Saturday')
      ev_sch_sat_rule.setApplySaturday(true)
      ev_sch_sat = ev_sch_sat_rule.daySchedule
      ev_sch_sat.setName('EV Charging Saturday')
      ev_sch_sat.addValue(OpenStudio::Time.new(0,24,0), 0)
    end

    if charge_on_sun
      ev_sch_sun = OpenStudio::Model::ScheduleRule.new(ev_sch, ev_sch.defaultDaySchedule)
      ev_sch_sun.setName('EV Charging Power Sunday')
      ev_sch_sun.setApplySunday(true)
    else
      ev_sch_sun_rule = OpenStudio::Model::ScheduleRule.new(ev_sch)
      ev_sch_sun_rule.setName('EV Charging Power Sunday')
      ev_sch_sun_rule.setApplySunday(true)
      ev_sch_sun = ev_sch_sun_rule.daySchedule
      ev_sch_sun.setName('EV Charging Sunday')
      ev_sch_sun.addValue(OpenStudio::Time.new(0,24,0), 0)
    end

    return ev_sch
  end

  def create_ev_sch_single(model, ev_charger, charge_on_sat, charge_on_sun)
    # create the schedule
    # Creating a schedule:ruleset
    ev_sch = OpenStudio::Model::ScheduleRuleset.new(model)
    ev_sch.setName("EV Charging Power Draw for charger #{ev_charger.name.to_s}")
    ev_sch.defaultDaySchedule.setName("EV Charging Default for charger #{ev_charger.name.to_s}")
    day_start_time = Time.strptime("00:00", '%H:%M')

    puts "ev_charger.occupied_start_time_list: #{ev_charger.occupied_start_time_list}"
    puts "ev_charger.occupied_until_time_list: #{ev_charger.occupied_until_time_list}"
    occupied_start_time_list = ev_charger.occupied_start_time_list
    occupied_until_time_list = ev_charger.occupied_until_time_list

    occupied_start_time_list.each_with_index do |occupied_start_time, idx|
      occupied_until_time = occupied_until_time_list[idx]
      if occupied_start_time.day == occupied_until_time.day
        # charging on the same day
        if idx > 0 && occupied_start_time == occupied_until_time_list[idx-1]
          # car charging are continuous without vacancy period
          end_time = OpenStudio::Time.new(0, 0, ((occupied_until_time - day_start_time)/60).to_i) # OpenStudio::Time.new(day,hr of day, minute of hr, seconds of hr?)
          ev_sch.defaultDaySchedule.addValue(end_time, 1)
        else
          # there are vacancy period between cars
          start_time = OpenStudio::Time.new(0, 0, ((occupied_start_time - day_start_time)/60).to_i) # OpenStudio::Time.new(day,hr of day, minute of hr, seconds of hr?)
          ev_sch.defaultDaySchedule.addValue(start_time, 0)
          end_time = OpenStudio::Time.new(0, 0, ((occupied_until_time - day_start_time)/60).to_i) # OpenStudio::Time.new(day,hr of day, minute of hr, seconds of hr?)
          ev_sch.defaultDaySchedule.addValue(end_time, 1)
        end
      else   # charging overnight
        if idx > 0 && occupied_start_time == occupied_until_time_list[idx-1]
          # car charging are continuous without vacancy period
          end_time_1 = OpenStudio::Time.new(0, 24, 0, 0)  # first till the end of the day
          end_time_2 = OpenStudio::Time.new(0, 0, ((occupied_until_time - day_start_time)/60).to_i) # OpenStudio::Time.new(day,hr of day, minute of hr, seconds of hr?)
          ev_sch.defaultDaySchedule.addValue(end_time_1, 1)
          ev_sch.defaultDaySchedule.addValue(end_time_2, 1)
        else
          # there are vacancy period between cars
          start_time = OpenStudio::Time.new(0, 0, ((occupied_start_time - day_start_time)/60).to_i) # OpenStudio::Time.new(day,hr of day, minute of hr, seconds of hr?)
          ev_sch.defaultDaySchedule.addValue(start_time, 0)
          end_time_1 = OpenStudio::Time.new(0, 24, 0, 0)  # first till the end of the day
          end_time_2 = OpenStudio::Time.new(0, 0, ((occupied_until_time - day_start_time)/60).to_i) # OpenStudio::Time.new(day,hr of day, minute of hr, seconds of hr?)
          ev_sch.defaultDaySchedule.addValue(end_time_1, 1)
          ev_sch.defaultDaySchedule.addValue(end_time_2, 1)
        end
      end
    end

    if charge_on_sat
      ev_sch_sat = OpenStudio::Model::ScheduleRule.new(ev_sch, ev_sch.defaultDaySchedule)
      ev_sch_sat.setName('EV Charging Power Saturday')
      ev_sch_sat.setApplySaturday(true)
    else
      ev_sch_sat_rule = OpenStudio::Model::ScheduleRule.new(ev_sch)
      ev_sch_sat_rule.setName('EV Charging Power Saturday')
      ev_sch_sat_rule.setApplySaturday(true)
      ev_sch_sat = ev_sch_sat_rule.daySchedule
      ev_sch_sat.setName('EV Charging Saturday')
      ev_sch_sat.addValue(OpenStudio::Time.new(0,24,0), 0)
    end

    if charge_on_sun
      ev_sch_sun = OpenStudio::Model::ScheduleRule.new(ev_sch, ev_sch.defaultDaySchedule)
      ev_sch_sun.setName('EV Charging Power Sunday')
      ev_sch_sun.setApplySunday(true)
    else
      ev_sch_sun_rule = OpenStudio::Model::ScheduleRule.new(ev_sch)
      ev_sch_sun_rule.setName('EV Charging Power Sunday')
      ev_sch_sun_rule.setApplySunday(true)
      ev_sch_sun = ev_sch_sun_rule.daySchedule
      ev_sch_sun.setName('EV Charging Sunday')
      ev_sch_sun.addValue(OpenStudio::Time.new(0,24,0), 0)
    end

    return ev_sch
  end

  # *********************************************
  # for workplace
  # waitlist is only applicable to workplace. For homes, charging is scheduled with start_charge_time
  # create all EV chargers
  def create_ev_sch_for_workplace(model, ev_chargers, max_charging_power, num_evs, avg_arrival_time, arrival_time_variation_in_mins, avg_leave_time, avg_charge_hours, charge_time_variation_in_mins, charge_on_sat, charge_on_sun)
    ev_list = []
    for j in 1..num_evs
      ev = ElectricVehicle.new("ev_#{j.to_s}")
      ev.arrival_time = avg_arrival_time + rand(-arrival_time_variation_in_mins...arrival_time_variation_in_mins) * 60  # TODO make sure time format is working correctly, Ruby Times "+" adopts seconds
      ev.leave_time = avg_leave_time + rand(-30...30) * 60  # TODO make sure time format is working correctly, Ruby Times "+" adopts seconds
      ev.leave_time = Time.strptime("23:00", '%H:%M') + 3600 if ev.leave_time > Time.strptime("23:00", '%H:%M') + 3600  # fix leave time at 24:00 if later than 24:00
      ev.needed_charge_hours = avg_charge_hours + rand(-charge_time_variation_in_mins...charge_time_variation_in_mins) / 60.0   # +- variation charge time
      ev_list << ev
    end

    # find the earliest arrival time
    arrival_time_earliest = Time.strptime("23:00", '%H:%M') + 3600  # initial: 24:00
    ev_list.each do |this_ev|
      if this_ev.arrival_time < arrival_time_earliest
        arrival_time_earliest = this_ev.arrival_time
      end
    end

    # For workplace: iterate through time, check status of each charger, if vacant, find the EV that has the earliest arrival time within uncharged EVs.
    # if this EV's leaving time is later than the current time, start charging until fully charged or leaving time, whichever comes first
    # when no EV is found any more, charging on this day ends, conclude the charging profile
    # 23 represent 23:00-24:00, corresponding to E+ schedule Until: 24:00
    for hour in 0..23
      current_time = Time.strptime("#{hour}:00", '%H:%M') + 3600   # %H: 00..23, 23 should represent the period 23:00-24:00, so add 1 hour to be the check point
      next if arrival_time_earliest > current_time
      ev_chargers.each do |ev_charger|
        if ev_charger.occupied
          if ev_charger.connected_ev.class.to_s != 'AddElectricVehicleChargingLoad::ElectricVehicle'
            runner.registerError("EV charger #{ev_charger.name.to_s} shows occupied, but no EV is connected.")
            return false
          end
          # disconnect EV if charged to full or till leave time. Only check if expected end time or leave is earlier than current time, otherwise check in next iteration.
          # Time addition uses seconds, so needs to multiple 3600
          if ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600 <= current_time || ev_charger.connected_ev.leave_time <= current_time
            if ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600 > ev_charger.connected_ev.leave_time
              ev_charger.occupied_until_time = ev_charger.connected_ev.leave_time
              ev_charger.connected_ev.end_charge_time = ev_charger.connected_ev.leave_time
            else
              ev_charger.occupied_until_time = ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600
              ev_charger.connected_ev.end_charge_time = ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600
            end
            ev_charger.occupied = false
            ev_charger.connected_ev.has_been_charged = true
            ev_charger.connected_ev.connected_to_charger = false
            ev_charger.connected_ev = nil
          end
        end
        # continue to check if charger not occupied, then connect to an EV
        unless ev_charger.occupied
          next_ev_to_charge = nil
          wait_list_time_earliest = Time.strptime("23:00", '%H:%M') + 3600  # initial: 24:00
          ev_list.each do |this_ev|
            # skip this EV if it is being charged or is being charged or already left
            next if this_ev.has_been_charged
            next if this_ev.connected_to_charger
            next if this_ev.leave_time <= current_time
            # get the uncharged, earliest arrival EV (so front in wait list)
            if this_ev.arrival_time < wait_list_time_earliest
              wait_list_time_earliest = this_ev.arrival_time
              next_ev_to_charge = this_ev
            end
          end
          # skip if no EV is on the wait list
          next if next_ev_to_charge.nil?
          if ev_charger.charged_ev_list.empty?
            ev_charger.occupied_start_time = wait_list_time_earliest
            next_ev_to_charge.start_charge_time = wait_list_time_earliest
          else
            next_ev_to_charge.start_charge_time = ev_charger.occupied_until_time
          end
          ev_charger.occupied = true
          next_ev_to_charge.connected_to_charger = true
          ev_charger.connected_ev = next_ev_to_charge
          ev_charger.charged_ev_list << next_ev_to_charge
        end
      end
    end

    ev_sch = create_ev_sch(model, ev_chargers, max_charging_power, charge_on_sat, charge_on_sun)
    return ev_sch
  end

  def create_ev_sch_for_commercial_charge_station(model, ev_chargers, max_charging_power, num_evs, avg_arrival_time, arrival_time_variation_in_mins, avg_charge_hours, charge_time_variation_in_mins, charge_on_sat, charge_on_sun)
    ev_list = []
    for j in 1..num_evs
      ev = ElectricVehicle.new("ev_#{j.to_s}")
      ev.arrival_time = avg_arrival_time + rand(-arrival_time_variation_in_mins...arrival_time_variation_in_mins) * 60  # TODO make sure time format is working correctly, Ruby Times "+" adopts seconds
      ev.needed_charge_hours = avg_charge_hours + rand(-charge_time_variation_in_mins...charge_time_variation_in_mins) / 60.0   # +- variation minutes
      ev_list << ev
    end

    # find the earliest arrival time
    arrival_time_earliest = Time.strptime("23:00", '%H:%M') + 3600  # initial: 24:00
    ev_list.each do |this_ev|
      if this_ev.arrival_time < arrival_time_earliest
        arrival_time_earliest = this_ev.arrival_time
      end
    end

    # For workplace: iterate through time, check status of each charger, if vacant, find the EV that has the earliest arrival time within uncharged EVs.
    # if this EV's leaving time is later than the current time, start charging until fully charged or leaving time, whichever comes first
    # when no EV is found any more, charging on this day ends, conclude the charging profile
    # 23 represent 23:00-24:00, corresponding to E+ schedule Until: 24:00
    ev_sch_list = []
    for hour in 0..23
      current_time = Time.strptime("#{hour}:00", '%H:%M') + 3600   # %H: 00..23, 23 should represent the period 23:00-24:00, so add 1 hour to be the check point
      next if arrival_time_earliest > current_time
      ev_chargers.each do |ev_charger|
        if ev_charger.occupied
          if ev_charger.connected_ev.class.to_s != 'AddElectricVehicleChargingLoad::ElectricVehicle'
            runner.registerError("EV charger #{ev_charger.name.to_s} shows occupied, but no EV is connected.")
            return false
          end
          # disconnect EV if charged to full. Only check if expected end time is earlier than current time, otherwise check in next iteration.
          # Time addition uses seconds, so needs to multiple 3600
          if ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600 <= current_time
            ev_charger.occupied_until_time_list << ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600
            ev_charger.connected_ev.end_charge_time = ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600
            ev_charger.occupied = false
            ev_charger.connected_ev.has_been_charged = true
            ev_charger.connected_ev.connected_to_charger = false
            ev_charger.connected_ev = nil
          end
        end
        # continue to check if charger not occupied, then connect to an EV
        unless ev_charger.occupied
          next_ev_to_charge = nil
          wait_list_time_earliest = Time.strptime("23:00", '%H:%M') + 3600  # initial: 24:00
          ev_list.each do |this_ev|
            # skip this EV if it is being charged or is being charged or already left
            next if this_ev.has_been_charged
            next if this_ev.connected_to_charger
            # get the uncharged, earliest arrival EV (so front in wait list)
            if this_ev.arrival_time < wait_list_time_earliest
              wait_list_time_earliest = this_ev.arrival_time
              next_ev_to_charge = this_ev
            end
          end
          # skip if no EV is on the wait list
          next if next_ev_to_charge.nil?
          if ev_charger.charged_ev_list.empty?
            ev_charger.occupied_start_time_list << wait_list_time_earliest
            next_ev_to_charge.start_charge_time = wait_list_time_earliest
          else
            if next_ev_to_charge.arrival_time < ev_charger.occupied_until_time_list[-1]
              next_ev_to_charge.start_charge_time = ev_charger.occupied_until_time_list[-1]
              ev_charger.occupied_start_time_list << ev_charger.occupied_until_time_list[-1]
            else
              next_ev_to_charge.start_charge_time = next_ev_to_charge.arrival_time
              ev_charger.occupied_start_time_list << next_ev_to_charge.arrival_time
            end
          end
          ev_charger.occupied = true
          next_ev_to_charge.connected_to_charger = true
          ev_charger.connected_ev = next_ev_to_charge
          ev_charger.charged_ev_list << next_ev_to_charge
        end
      end
    end

    ev_chargers.each do |ev_charger|
      # create schedule for each ev_charger
      # charger.charging_power
      ev_sch = create_ev_sch_single(model, ev_charger, charge_on_sat, charge_on_sun)
      ev_sch_list << ev_sch
    end

    # ev_sch = create_ev_sch(model, ev_chargers, max_charging_power, charge_on_sat, charge_on_sun)
    return ev_sch_list
  end


  def create_ev_sch_for_home(model, ev_chargers, max_charging_power, num_evs, start_charge_time, avg_charge_hours, charge_on_sat, charge_on_sun)
    ev_list = []
    for j in 1..num_evs
      ev = ElectricVehicle.new("ev_#{j.to_s}")
      ev.needed_charge_hours = avg_charge_hours + rand(-60...60) / 60.0   # +- 1 hour
      ev_list << ev
    end

    # for homes, EV charging could go overnight, so iterate 48 hours
    for hour in 0..47
      if hour <= 23
        current_time = Time.strptime("#{hour}:00", '%H:%M') + 3600   # %H: 00..23, 23 should represent the period 23:00-24:00, so add 1 hour to be the check point
      else
        current_time = Time.strptime("#{hour-24}:00", '%H:%M') + 24*60*60 + 3600   # %H: the second day (for overnight). still 00..23, 23 should represent the period 23:00-24:00, so add 1 hour to be the check point
      end
      next if start_charge_time > current_time
      ev_chargers.each do |ev_charger|
        if ev_charger.occupied
          if ev_charger.connected_ev.class.to_s != 'AddElectricVehicleChargingLoad::ElectricVehicle'
            runner.registerError("EV charger #{ev_charger.name.to_s} shows occupied, but no EV is connected.")
            return false
          end
          # Time addition uses seconds, so needs to multiple 3600
          if ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600 <= current_time
            ev_charger.connected_ev.end_charge_time = ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600
            ev_charger.occupied_until_time = ev_charger.connected_ev.start_charge_time + ev_charger.connected_ev.needed_charge_hours * 3600
            ev_charger.occupied = false
            ev_charger.connected_ev.has_been_charged = true
            ev_charger.connected_ev.connected_to_charger = false
            ev_charger.connected_ev = nil
          end
        end
        # continue to check if charger not occupied, then connect to an EV
        unless ev_charger.occupied
          # no need of waitlist, just connect to whichever EV that hasn't been charged
          next_ev_to_charge = nil
          ev_list.each do |this_ev|
            # skip this EV if it is being charged or is being charged
            next if this_ev.has_been_charged
            next if this_ev.connected_to_charger
            next_ev_to_charge = this_ev
            break
          end
          # skip if no EV is on the wait list
          next if next_ev_to_charge.nil?
          if ev_charger.charged_ev_list.empty?
            ev_charger.occupied_start_time = start_charge_time
            next_ev_to_charge.start_charge_time = start_charge_time
          else
            next_ev_to_charge.start_charge_time = ev_charger.occupied_until_time
          end
          ev_charger.occupied = true
          next_ev_to_charge.connected_to_charger = true
          ev_charger.connected_ev = next_ev_to_charge
          ev_charger.charged_ev_list << next_ev_to_charge
        end
      end
    end

    ev_sch = create_ev_sch(model, ev_chargers, max_charging_power, charge_on_sat, charge_on_sun)
    return ev_sch
  end

  # create EV load schedule (normalized)
  case bldg_use_type
  when 'workplace'
    ev_sch = create_ev_sch_for_workplace(model, ev_chargers, max_charging_power, num_evs, avg_arrival_time, arrival_time_variation_in_mins, avg_leave_time, avg_charge_hours, charge_time_variation_in_mins, charge_on_sat, charge_on_sun)
  when 'home'
    ev_sch = create_ev_sch_for_home(model, ev_chargers, max_charging_power, num_evs, start_charge_time, avg_charge_hours, charge_on_sat, charge_on_sun)
  when 'commercial station'
    ev_sch_list = create_ev_sch_for_commercial_charge_station(model, ev_chargers, max_charging_power, num_evs, avg_arrival_time, arrival_time_variation_in_mins, avg_charge_hours, charge_time_variation_in_mins, charge_on_sat, charge_on_sun)
  end

  case bldg_use_type
  when 'workplace', 'home'
    # Adding an EV charger definition and instance for the regular EV charging.
    ev_charger_def = OpenStudio::Model::ExteriorFuelEquipmentDefinition.new(model)
    ev_charger_level = (max_charging_power * 1000).round(0) # Converting from kW to watts
    ev_charger_def.setName("#{ev_charger_level}w EV Charging Definition")
    ev_charger_def.setDesignLevel(ev_charger_level)

    # creating EV charger object for the regular EV charging.
    ev_charger = OpenStudio::Model::ExteriorFuelEquipment.new(ev_charger_def, ev_sch)
    ev_charger.setName("#{ev_charger_level}w EV Charger")
    ev_charger.setFuelType('Electricity')
    ev_charger.setEndUseSubcategory('Electric Vehicles')
  when 'commercial station'
    ev_chargers.each_with_index do |ev_charger, idx|
      # Adding an EV charger definition and instance for the regular EV charging.
      ev_charger_def = OpenStudio::Model::ExteriorFuelEquipmentDefinition.new(model)
      ev_charger_level = (ev_charger.charging_power * 1000).round(0)  # Converting from kW to watts
      ev_charger_def.setName("#{ev_charger_level}w EV Charging Definition")
      ev_charger_def.setDesignLevel(ev_charger_level)

      # creating EV charger object for the regular EV charging.
      ev_charger = OpenStudio::Model::ExteriorFuelEquipment.new(ev_charger_def, ev_sch_list[idx])
      ev_charger.setName("#{ev_charger_level}w EV Charger")
      ev_charger.setFuelType('Electricity')
      ev_charger.setEndUseSubcategory('Electric Vehicles')
    end

  end


  runner.registerInfo("multiplier (kW) = #{max_charging_power}}")

  # echo the new space's name back to the user
  runner.registerInfo("EV load with #{num_ev_chargers} EV chargers and #{num_evs} EVs was added.")

  # report final condition of model
  runner.registerFinalCondition("The building completed adding EV load.")

  return true
end