lib/measures/AddElectricVehicleChargingLoad/measure.rb in openstudio-geb-0.4.0 vs lib/measures/AddElectricVehicleChargingLoad/measure.rb in openstudio-geb-0.5.0

- old
+ new

@@ -28,10 +28,11 @@ # 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 @@ -51,10 +52,11 @@ # 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 @@ -81,10 +83,28 @@ 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 @@ -104,21 +124,26 @@ @occupied = false # vacant: 0, occupied: 1 @level = 1 # Level1: 1, Level2: 2, DC charger: 3 @charging_power = nil @connected_ev = nil @occupied_until_time = nil + #TODO for workplace need to use the list instead of single time too, same as commercial station + @occupied_until_time_list = Array.new # for commercial station use this @occupied_start_time = nil + @occupied_start_time_list = Array.new # for commercial station use this @charged_ev_list = Array.new end attr_accessor :name # Type: string. Name. attr_accessor :occupied # Type: boolean attr_accessor :level attr_accessor :charging_power # Type: float, unit: kW attr_accessor :connected_ev # Type: ElectricVehicle attr_accessor :occupied_until_time # Type: Time. Daily end charging time + attr_accessor :occupied_until_time_list # Type: Array of Time. List of daily end charging time attr_accessor :occupied_start_time # Type: Time. Daily start charging time + attr_accessor :occupied_start_time_list # Type: Array of Time. List of daily start charging time attr_accessor :charged_ev_list # Type: Array end class ElectricVehicle def initialize(name) @@ -159,10 +184,12 @@ 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}" @@ -190,10 +217,18 @@ 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 @@ -212,15 +247,19 @@ # 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 = 7.0 + charger.charging_power = 9.6 # C2C expert match input when 'DC charger' - charger.charging_power = 50.0 + # 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'.") + 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 @@ -256,17 +295,17 @@ 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 + 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 + 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 @@ -287,11 +326,11 @@ 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) + ev_sch.defaultDaySchedule.addValue(time, (ev_load/max_charging_power).round(2)) ev_load = ev_load_new end end if charge_on_sat @@ -321,22 +360,98 @@ 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, avg_leave_time, avg_charge_hours, charge_on_sat, charge_on_sun) + 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(-30...30) * 60 # TODO make sure time format is working correctly, Ruby Times "+" adopts seconds + 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(-60...60) / 60.0 # +- 1 hour + 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 @@ -408,10 +523,100 @@ 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 @@ -474,25 +679,46 @@ 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, avg_leave_time, avg_charge_hours, charge_on_sat, charge_on_sun) + 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 - # 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 # Converting from kW to watts - ev_charger_def.setName("#{ev_charger_level} w EV Charging Definition") - ev_charger_def.setDesignLevel(ev_charger_level) + 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') + # 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.")