example_files/resources/hpxml-measures/HPXMLtoOpenStudio/resources/battery.rb in urbanopt-cli-0.10.0 vs example_files/resources/hpxml-measures/HPXMLtoOpenStudio/resources/battery.rb in urbanopt-cli-0.11.0

- old
+ new

@@ -1,54 +1,64 @@ +# ********************************************************************************* +# URBANopt (tm), Copyright (c) Alliance for Sustainable Energy, LLC. +# See also https://github.com/urbanopt/urbanopt-cli/blob/develop/LICENSE.md +# ********************************************************************************* + # frozen_string_literal: true class Battery - def self.apply(runner, model, pv_systems, battery, schedules_file) + def self.apply(runner, model, pv_systems, battery, schedules_file, unit_multiplier) charging_schedule = nil discharging_schedule = nil if not schedules_file.nil? - charging_schedule = schedules_file.create_schedule_file(col_name: SchedulesFile::ColumnBatteryCharging) - discharging_schedule = schedules_file.create_schedule_file(col_name: SchedulesFile::ColumnBatteryDischarging) + charging_schedule = schedules_file.create_schedule_file(model, col_name: SchedulesFile::ColumnBatteryCharging) + discharging_schedule = schedules_file.create_schedule_file(model, col_name: SchedulesFile::ColumnBatteryDischarging) end if pv_systems.empty? && charging_schedule.nil? && discharging_schedule.nil? runner.registerWarning('Battery without PV specified, and no charging/discharging schedule provided; battery is assumed to operate as backup and will not be modeled.') return end obj_name = battery.id rated_power_output = battery.rated_power_output # W - nominal_voltage = battery.nominal_voltage # V if not battery.nominal_capacity_kwh.nil? if battery.usable_capacity_kwh.nil? fail "UsableCapacity and NominalCapacity for Battery '#{battery.id}' must be in the same units." end nominal_capacity_kwh = battery.nominal_capacity_kwh # kWh - usable_fraction = battery.usable_capacity_kwh / nominal_capacity_kwh + usable_capacity_kwh = battery.usable_capacity_kwh + usable_fraction = usable_capacity_kwh / nominal_capacity_kwh else if battery.usable_capacity_ah.nil? fail "UsableCapacity and NominalCapacity for Battery '#{battery.id}' must be in the same units." end - nominal_capacity_kwh = get_kWh_from_Ah(battery.nominal_capacity_ah, nominal_voltage) # kWh + nominal_capacity_kwh = get_kWh_from_Ah(battery.nominal_capacity_ah, battery.nominal_voltage) # kWh + usable_capacity_kwh = get_kWh_from_Ah(battery.usable_capacity_ah, battery.nominal_voltage) # kWh usable_fraction = battery.usable_capacity_ah / battery.nominal_capacity_ah end - return if rated_power_output <= 0 || nominal_capacity_kwh <= 0 || nominal_voltage <= 0 + return if rated_power_output <= 0 || nominal_capacity_kwh <= 0 || battery.nominal_voltage <= 0 + nominal_capacity_kwh *= unit_multiplier + usable_capacity_kwh *= unit_multiplier + rated_power_output *= unit_multiplier + is_outside = (battery.location == HPXML::LocationOutside) if not is_outside frac_sens = 1.0 else # Internal gains outside unit frac_sens = 0.0 end default_nominal_cell_voltage = 3.342 # V, EnergyPlus default default_cell_capacity = 3.2 # Ah, EnergyPlus default - number_of_cells_in_series = Integer((nominal_voltage / default_nominal_cell_voltage).round) + number_of_cells_in_series = Integer((battery.nominal_voltage / default_nominal_cell_voltage).round) number_of_strings_in_parallel = Integer(((nominal_capacity_kwh * 1000.0) / ((default_nominal_cell_voltage * number_of_cells_in_series) * default_cell_capacity)).round) battery_mass = (nominal_capacity_kwh / 10.0) * 99.0 # kg battery_surface_area = 0.306 * (nominal_capacity_kwh**(2.0 / 3.0)) # m^2 # Assuming 3/4 of unusable charge is minimum SOC and 1/4 of unusable charge is maximum SOC, based on SAM defaults @@ -62,11 +72,11 @@ voltage_dependence = true end elcs = OpenStudio::Model::ElectricLoadCenterStorageLiIonNMCBattery.new(model, number_of_cells_in_series, number_of_strings_in_parallel, battery_mass, battery_surface_area) elcs.setName("#{obj_name} li ion") - unless is_outside + if not is_outside elcs.setThermalZone(battery.additional_properties.space.thermalZone.get) end elcs.setRadiativeFraction(0.9 * frac_sens) # elcs.setLifetimeModel(battery.lifetime_model) elcs.setLifetimeModel(HPXML::BatteryLifetimeModelNone) @@ -78,11 +88,11 @@ elcs.setDefaultNominalCellVoltage(default_nominal_cell_voltage) elcs.setFullyChargedCellCapacity(default_cell_capacity) elcs.setCellVoltageatEndofNominalZone(default_nominal_cell_voltage) if not voltage_dependence elcs.setBatteryCellInternalElectricalResistance(0.002) # 2 mOhm/cell, based on OCHRE defaults (which are based on fitting to lab results) - # FIXME: if the voltage reported during charge/discharge is different, energy may not balance + # Note: if the voltage reported during charge/discharge is different, energy may not balance # elcs.setFullyChargedCellVoltage(default_nominal_cell_voltage) # elcs.setCellVoltageatEndofExponentialZone(default_nominal_cell_voltage) end elcs.setFullyChargedCellVoltage(default_nominal_cell_voltage) elcs.setCellVoltageatEndofExponentialZone(default_nominal_cell_voltage) @@ -128,43 +138,47 @@ # Replace this when the first item in https://github.com/NREL/EnergyPlus/issues/9176 is fixed. charge_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Electric Storage Charge Energy') charge_sensor.setName('battery_charge') charge_sensor.setKeyName(elcs.name.to_s) + discharge_sensor = OpenStudio::Model::EnergyManagementSystemSensor.new(model, 'Electric Storage Discharge Energy') + discharge_sensor.setName('battery_discharge') + discharge_sensor.setKeyName(elcs.name.to_s) + loss_adj_object_def = OpenStudio::Model::OtherEquipmentDefinition.new(model) loss_adj_object = OpenStudio::Model::OtherEquipment.new(loss_adj_object_def) - obj_name = Constants.ObjectNameBatteryLossesAdjustment(elcs.name) + obj_name = Constants.ObjectNameBatteryLossesAdjustment loss_adj_object.setName(obj_name) loss_adj_object.setEndUseSubcategory(obj_name) loss_adj_object.setFuelType(EPlus.fuel_type(HPXML::FuelTypeElectricity)) loss_adj_object.setSpace(space) loss_adj_object_def.setName(obj_name) loss_adj_object_def.setDesignLevel(0.01) loss_adj_object_def.setFractionRadiant(0) loss_adj_object_def.setFractionLatent(0) loss_adj_object_def.setFractionLost(frac_lost) loss_adj_object.setSchedule(model.alwaysOnDiscreteSchedule) + loss_adj_object.additionalProperties.setFeature('ObjectType', Constants.ObjectNameBatteryLossesAdjustment) battery_adj_actuator = OpenStudio::Model::EnergyManagementSystemActuator.new(loss_adj_object, *EPlus::EMSActuatorOtherEquipmentPower, loss_adj_object.space.get) battery_adj_actuator.setName('battery loss_adj_act') battery_losses_program = OpenStudio::Model::EnergyManagementSystemProgram.new(model) battery_losses_program.setName('battery_losses') - battery_losses_program.addLine("Set losses = -1 * #{charge_sensor.name} * (1 - #{battery.round_trip_efficiency})") + battery_losses_program.addLine("Set charge_losses = (-1 * #{charge_sensor.name} * (1 - (#{battery.round_trip_efficiency} ^ 0.5))) / #{unit_multiplier}") + battery_losses_program.addLine("Set discharge_losses = (-1 * #{discharge_sensor.name} * (1 - (#{battery.round_trip_efficiency} ^ 0.5))) / #{unit_multiplier}") + battery_losses_program.addLine('Set losses = charge_losses + discharge_losses') battery_losses_program.addLine("Set #{battery_adj_actuator.name} = -1 * losses / ( 3600 * SystemTimeStep )") battery_losses_pcm = OpenStudio::Model::EnergyManagementSystemProgramCallingManager.new(model) battery_losses_pcm.setName('battery_losses') battery_losses_pcm.setCallingPoint('EndOfSystemTimestepBeforeHVACReporting') battery_losses_pcm.addProgram(battery_losses_program) - battery_losses_output_var = OpenStudio::Model::EnergyManagementSystemOutputVariable.new(model, 'losses') - battery_losses_output_var.setName("#{Constants.ObjectNameBatteryLossesAdjustment(elcs.name)} outvar") - battery_losses_output_var.setTypeOfDataInVariable('Summed') - battery_losses_output_var.setUpdateFrequency('SystemTimestep') - battery_losses_output_var.setEMSProgramOrSubroutineName(battery_losses_program) - battery_losses_output_var.setUnits('J') + elcd.additionalProperties.setFeature('HPXML_ID', battery.id) + elcs.additionalProperties.setFeature('HPXML_ID', battery.id) + elcs.additionalProperties.setFeature('UsableCapacity_kWh', Float(usable_capacity_kwh)) end def self.get_battery_default_values(has_garage = false) if has_garage location = HPXML::LocationGarage @@ -183,7 +197,18 @@ return nominal_capacity_kwh * 1000.0 / nominal_voltage end def self.get_kWh_from_Ah(nominal_capacity_ah, nominal_voltage) return nominal_capacity_ah * nominal_voltage / 1000.0 + end + + def self.get_usable_capacity_kWh(battery) + usable_capacity_kwh = battery.usable_capacity_kwh + if usable_capacity_kwh.nil? + usable_capacity_kwh = get_kWh_from_Ah(battery.usable_capacity_ah, battery.nominal_voltage) # kWh + end + return usable_capacity_kwh + end + + def self.get_min_max_state_of_charge() end end